Skip to content

Instantly share code, notes, and snippets.

@bweis
Created November 11, 2023 19:17
Show Gist options
  • Save bweis/353adf147803f2320efd144c7a96a990 to your computer and use it in GitHub Desktop.
Save bweis/353adf147803f2320efd144c7a96a990 to your computer and use it in GitHub Desktop.
Generating ts types from rust
[package]
name = "typegen"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.4.7", features = ["derive"] }
# -- App Libs
lib-base = { path = "../../libs/lib-base" }
lib-core = { path = "../../libs/lib-core" }
schemars = { version = "0.8", features = ["uuid1"] }
# -- External Libs
serde_json = "1.0"
import * as fs from "node:fs/promises";
import * as path from "path";
import * as url from "url";
import { compile } from "json-schema-to-typescript";
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
async function main() {
let schemasPath = path.join(dirname, "jsonschemas");
let schemaFiles = (await fs.readdir(schemasPath)).filter((x) =>
x.endsWith(".json"),
);
// Compile all types, stripping out duplicates. This is a bit dumb but the easiest way to
// do it since we can't suppress generation of definition references.
let compiledTypes = new Set();
for (let filename of schemaFiles) {
let filePath = path.join(schemasPath, filename);
let schema = JSON.parse(await fs.readFile(filePath));
let compiled = await compile(schema, schema.title, {
bannerComment: "",
additionalProperties: false,
});
let typeDefinitions = compiled.split("export");
typeDefinitions = hackToDeduplicateTypes(typeDefinitions);
for (let type of typeDefinitions) {
if (!type) {
continue;
}
compiledTypes.add("export " + type.trim());
}
}
console.log({ compiledTypes });
let output = Array.from(compiledTypes).join("\n\n");
let outputPath = path.join(dirname, "types.ts");
try {
let existing = await fs.readFile(outputPath);
if (existing == output) {
// Skip writing if it hasn't changed, so that we don't confuse any sort of incremental builds.
// This check isn't ideal but the script runs quickly enough and rarely enough that it doesn't matter.
console.log("Schemas are up to date");
return;
}
} catch (e) {
// It's fine if there's no output from a previous run.
if (e.code !== "ENOENT") {
throw e;
}
}
await fs.writeFile(outputPath, output);
console.log(`Wrote Typescript types to ${outputPath}`);
}
/*
* This is a hack to deduplicate types. It's not ideal but it's the easiest way to do it since we can't trust
* the schemars library with cyclic types.
*/
function hackToDeduplicateTypes(typeDefinitions) {
const re = new RegExp("(?<prefix>[a-zA-Z]+)(?<suffix>[0-9]*)");
const deDuplications = Object.entries(
typeDefinitions
// split on spaces
.map((t) => t.split(" ").filter((t) => t !== ""))
// add index and split type
.map((t, index) => {
const { groups } = re.exec(t[1]);
return {
type: t[1],
index,
...groups,
};
})
// Group by prefix.
// When NodeJS supports Object.groupBy, we can use that instead.
.reduce(
(acc, value, _, __, k = value.prefix) => (
(acc[k] || (acc[k] = [])).push(value), acc
),
{},
),
)
// Filter out undefined values and singletons (no replacement needed)
.filter(([key, value]) => !key || value.length > 1)
// Map to the find/replace/indicesToDelete format
.map(([key, value]) => {
const find = value.filter((v) => v.type !== key);
const indicesToDelete = find.map((f) => f.index);
indicesToDelete.sort((a, b) => b - a); // sorted back to front so we don't mess up the indices
return {
find: find.map((f) => f.type),
replace: key,
indicesToDelete,
};
});
// Get all the indices to delete and sort them in decreasing order to remove indices from eachType
deDuplications
.flatMap((d) => d.indicesToDelete)
.sort((a, b) => a - b)
.forEach((i) => typeDefinitions.splice(i, 1));
// Replace each instance of deduplication.find with deduplication.replace for each typeDefinition
const replaced = typeDefinitions
.filter((td) => td !== "")
.map((typeDefinition) => {
let replacedTypeDefinition = typeDefinition;
deDuplications.forEach((deDuplication) => {
if (deDuplication.find.length === 0) {
return;
}
const regex = new RegExp(`\\b${deDuplication.find.join("|")}\\b`, "gm");
replacedTypeDefinition = typeDefinition.replaceAll(
regex,
deDuplication.replace,
);
});
console.log({ before: typeDefinition, after: replacedTypeDefinition });
return replacedTypeDefinition;
});
return replaced;
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
use std::env;
use clap::Parser;
// use lib_core::model::user::{TestStruct, User, UserForCreate, UserForLogin};
use schemars::{schema::RootSchema, schema_for, JsonSchema};
fn write_schema(
dir: &std::path::Path,
name: &str,
schema: &RootSchema,
) -> std::io::Result<()> {
let output = serde_json::to_string_pretty(schema).unwrap();
let output_path = dir.join(format!("{}.json", name));
std::fs::write(output_path, output)
}
// TESTING
use std::{collections::BTreeSet, rc::Rc};
#[derive(JsonSchema)]
enum Role {
Foo,
Admin,
}
#[derive(JsonSchema)]
enum Gender {
Male,
Female,
Other,
}
#[derive(JsonSchema)]
struct User {
user_id: i32,
first_name: String,
last_name: String,
role: Role,
family: Vec<User>,
gender: Gender,
}
#[derive(JsonSchema)]
enum Vehicle {
Bicycle { color: String },
Car { brand: String, color: String },
}
#[derive(JsonSchema)]
struct Point<T> {
time: u64,
value: T,
}
#[derive(JsonSchema)]
struct Series {
points: Vec<Point<u64>>,
}
#[derive(JsonSchema)]
enum SimpleEnum {
A,
B,
}
#[derive(JsonSchema)]
enum ComplexEnum {
A,
B { foo: String, bar: f64 },
W(SimpleEnum),
F { nested: SimpleEnum },
V(Vec<Series>),
U(Box<User>),
}
#[derive(JsonSchema)]
enum InlineComplexEnum {
A,
B { foo: String, bar: f64 },
W(SimpleEnum),
F { nested: SimpleEnum },
V(Vec<Series>),
U(Box<User>),
}
#[derive(JsonSchema)]
struct ComplexStruct {
pub string_tree: Option<Rc<BTreeSet<String>>>,
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(short, long)]
output_path: Option<std::path::PathBuf>,
}
fn main() -> std::io::Result<()> {
let args = Args::parse();
let output_dir = args.output_path.unwrap_or(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("schemas"),
);
let e = std::fs::DirBuilder::new().create(&output_dir);
if let Err(e) = e {
if e.kind() != std::io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
// let schema = schema_for!(User);
// write_schema(&output_dir, "User", &schema)?;
// let schema = schema_for!(UserForCreate);
// write_schema(&output_dir, "UserForCreate", &schema)?;
// let schema = schema_for!(UserForLogin);
// write_schema(&output_dir, "UserForLogin", &schema)?;
// let schema = schema_for!(TestStruct);
// write_schema(&output_dir, "TestStruct", &schema)?;
let schema = schema_for!(Role);
write_schema(&output_dir, "Role", &schema)?;
let schema = schema_for!(Gender);
write_schema(&output_dir, "Gender", &schema)?;
let schema = schema_for!(User);
write_schema(&output_dir, "User", &schema)?;
let schema = schema_for!(Vehicle);
write_schema(&output_dir, "Vehicle", &schema)?;
let schema = schema_for!(Point<u64>);
write_schema(&output_dir, "Point", &schema)?;
let schema = schema_for!(SimpleEnum);
write_schema(&output_dir, "SimpleEnum", &schema)?;
let schema = schema_for!(ComplexEnum);
write_schema(&output_dir, "ComplexEnum", &schema)?;
let schema = schema_for!(InlineComplexEnum);
write_schema(&output_dir, "InlineComplexEnum", &schema)?;
let schema = schema_for!(ComplexStruct);
write_schema(&output_dir, "ComplexStruct", &schema)?;
Ok(())
}
{
"name": "typegen",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"generate": "cargo run --manifest-path ../../api/Cargo.toml -p typegen -- --output-path ./jsonschemas && node generate-ts-types.js",
"generate:clean": "rm -r ./jsonschemas; npm run generate",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"json-schema-to-typescript": "^13.1.1"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment