diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ed8598b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "cdevents-spec"] + path = cdevents-spec + url = git@github.com:cdevents/spec.git + branch = spec-v0.3 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..51b1af6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +resolver = "2" +members = [ "cdevents","generator"] + +[workspace.package] +edition = "2021" +version = "0.1.0" +authors = ["David Bernard"] +license = "ASL-2.0" +repository = "https://github.com/cdevents/sdk-rust" +rust-version = "1.75" +publish = false + +[workspace.metadata.release] +pre-release-commit-message = "🚀 (cargo-release) version {{version}}" +tag-prefix = "" +tag-name = "{{prefix}}{{version}}" +tag-message = "🔖 {{version}}" diff --git a/cargo.toml b/cargo.toml deleted file mode 100644 index 4ad03dd..0000000 --- a/cargo.toml +++ /dev/null @@ -1,3 +0,0 @@ -[workspace] - -members = [] diff --git a/generator/Cargo.toml b/generator/Cargo.toml new file mode 100644 index 0000000..831dc1e --- /dev/null +++ b/generator/Cargo.toml @@ -0,0 +1,22 @@ +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[package] +name = "generator" +description = "generate cdevents type from json schema on cdevents-spec" +edition.workspace = true +version.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +anyhow = "1.0" +clap = { version = "4", features = ["derive"] } +cruet = "0.14" +handlebars = { version = "5", features = ["dir_source"] } +serde_json = "*" +url = "2.5" +handlebars_misc_helpers = { version = "0.15", default-features = false, features = [ + "string", + "json", +] } diff --git a/generator/README.md b/generator/README.md new file mode 100644 index 0000000..9a1b3ee --- /dev/null +++ b/generator/README.md @@ -0,0 +1,22 @@ +# cdevents rust code generator + +Goals: generate rust code for cdevents from jsonschema provided as part of cdevents specs. + +- The generator take read jsonschema as json apply them to a set of templates +- The generator is very basic (no json schema semantic, no `$ref` resolution) like [eventuallyconsultant/codegenr: Fast handlebars templates based code generator, ingesting swagger/openapi and other json/yaml documents with $refs, or graphql schema, outputs whatever you template](https://github.com/eventuallyconsultant/codegenr/) +- The generator is currently used to generated Subjects + +## Why not use a jsonschema to rust generator? + +- I tried some (like ) and they failed (no error), maybe too early, not support for the version of jsonschema used by cdevents (often they support jsonschema draft-4) +- The json schema (v0.3) are not connected, so lot of duplication (context,...), so classical generator will create as many Context type as Event type,... + +## Run + +To generate the `subjects` into sibling crate `cdevents/src/generated` from content of `cdevents-spec/schemas`, from root workspace + +```sh +cargo run -p generator -- --help +cargo run -p generator -- --templates-dir "generator/templates" --jsonschema-dir "cdevents-spec/schemas" --dest +"cdevents/src/generated" +``` \ No newline at end of file diff --git a/generator/src/main.rs b/generator/src/main.rs new file mode 100644 index 0000000..3dba149 --- /dev/null +++ b/generator/src/main.rs @@ -0,0 +1,119 @@ +use anyhow::{anyhow, Context, Result}; +use clap::Parser; +use handlebars::{handlebars_helper, DirectorySourceOptions, Handlebars}; +use serde_json::{json, Value}; +use std::{fs, path::PathBuf}; + +/// generator of part of the rust code of cdevents from spec +#[derive(Parser, Debug)] +struct Settings { + /// directory with handlebars templates + #[arg(long, default_value = "templates")] + templates_dir: PathBuf, + + /// directory with json schemas of events to generate + #[arg(long, default_value = "../cdevents-spec/schemas")] + jsonschema_dir: PathBuf, + + /// destination directory where to generate code + #[arg(long, default_value = "../cdevents/src/generated")] + dest: PathBuf, +} + +fn main() -> Result<()> { + let settings = Settings::parse(); + + let mut hbs = Handlebars::new(); + hbs.set_strict_mode(true); + hbs.register_escape_fn(handlebars::no_escape); + //hbs.unregister_escape_fn(); + hbs.register_helper("type_of", Box::new(type_of)); + hbs.register_helper("normalize_ident", Box::new(normalize_ident)); + handlebars_misc_helpers::register(&mut hbs); + hbs.register_templates_directory(settings.templates_dir, DirectorySourceOptions::default())?; + + fs::create_dir_all(&settings.dest)?; + + let mut subjects = vec![]; + let mut jsonfiles = + std::fs::read_dir(settings.jsonschema_dir)?.collect::, _>>()?; + jsonfiles.sort_by_key(|v| v.file_name()); + for entry in jsonfiles { + let path = entry.path(); + if let Some(extension) = path.extension() { + if extension == "json" { + let json = serde_json::from_str(&std::fs::read_to_string(&path)?)?; + let (type_name, code) = generate_subject(&hbs, json) + .with_context(|| format!("failed to generate subject on {:?}", &path))?; + let file = settings + .dest + .join(cruet::to_snake_case(&type_name)) + .with_extension("rs"); + fs::write(file, code)?; + subjects.push(type_name); + } + } + } + + let (type_name, code) = + generate_module(&hbs, &subjects).with_context(|| "failed to generate module")?; + let file = settings + .dest + .join(cruet::to_snake_case(&type_name)) + .with_extension("rs"); + fs::write(file, code)?; + + Ok(()) +} + +fn generate_subject(hbs: &Handlebars, jsonschema: Value) -> Result<(String, String)> { + let id = jsonschema["$id"] + .as_str() + .ok_or(anyhow!("$id not found or not a string")) + .and_then(|s| url::Url::parse(s).with_context(|| format!("failed to parse: {}", s)))?; + let type_name = id + .path_segments() + .and_then(|v| v.last()) + .map(cruet::to_class_case) + .ok_or(anyhow!("no path in $id"))? + .replace("Event", "Subject"); + let mut data = jsonschema.clone(); + data.as_object_mut().and_then(|m| { + m.insert( + "type_name".to_string(), + serde_json::to_value(&type_name).unwrap(), + ) + }); + let code = hbs.render("subject", &data)?; + Ok((type_name.to_string(), code)) +} + +fn generate_module(hbs: &Handlebars, subjects: &[String]) -> Result<(String, String)> { + let data = json!({ + "subjects": subjects + }); + let code = hbs.render("mod", &data)?; + Ok(("mod".to_string(), code)) +} + +//TODO helper to convert into type +//TODO helper to check if optionnal +handlebars_helper!(type_of: |field_name: Value, def: Value, required: Value| { + let mut t = match def["type"].as_str() { + Some("string") => "String", + Some("object") => "serde_json::Map", + x => todo!("impl type {:?}", x), + }.to_string(); + if required.as_array().map(|a| a.contains(&field_name)).unwrap_or(false) { + t = format!("Option<{}>", t); + } + t +}); + +handlebars_helper!(normalize_ident: |v: Value| { + match v.as_str() { + Some("type") => "tpe", + Some(x) => x, + None => unimplemented!(), + } +}); diff --git a/generator/templates/mod.hbs b/generator/templates/mod.hbs new file mode 100644 index 0000000..2df24c6 --- /dev/null +++ b/generator/templates/mod.hbs @@ -0,0 +1,14 @@ +// code generated by cdevents/sdk-rust/generator (mod.hbs) +{{#each subjects }} +mod {{to_snake_case this}}; +{{/each}} + +use serde::{Serialize, Deserialize}; + +#[derive(Debug,Clone,Serialize,Deserialize)] +#[serde(untagged)] // TODO how to use content of context.type as discriminator ? +pub enum Subject { + {{#each subjects }} + {{this}}({{to_snake_case this}}::{{this}}), + {{/each}} +} diff --git a/generator/templates/subject.hbs b/generator/templates/subject.hbs new file mode 100644 index 0000000..b66e4a2 --- /dev/null +++ b/generator/templates/subject.hbs @@ -0,0 +1,11 @@ +// code generated by cdevents/sdk-rust/generator (subject.hbs) +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct {{ type_name }} { + {{assign "required" properties.subject.required }} + {{#each properties.subject.properties }} + #[serde(rename = "{{ @key }}")] + pub {{normalize_ident @key }}: {{type_of @key this required }}, + {{/each}} +}