-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: David Bernard <david.bernard.31@gmail.com>
- Loading branch information
Showing
8 changed files
with
210 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
[submodule "cdevents-spec"] | ||
path = cdevents-spec | ||
url = git@github.com:cdevents/spec.git | ||
branch = spec-v0.3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}}" |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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::<Result<Vec<_>, _>>()?; | ||
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<String, serde_json::Value>", | ||
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!(), | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}} | ||
} |