Skip to content

Commit

Permalink
✨ first working basic generator
Browse files Browse the repository at this point in the history
Signed-off-by: David Bernard <david.bernard.31@gmail.com>
  • Loading branch information
davidB committed Jan 10, 2024
1 parent 67ec47a commit 5174e2d
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .gitmodules
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
18 changes: 18 additions & 0 deletions Cargo.toml
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}}"
3 changes: 0 additions & 3 deletions cargo.toml

This file was deleted.

22 changes: 22 additions & 0 deletions generator/Cargo.toml
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",
] }
22 changes: 22 additions & 0 deletions generator/README.md
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"
```
119 changes: 119 additions & 0 deletions generator/src/main.rs
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!(),
}
});
14 changes: 14 additions & 0 deletions generator/templates/mod.hbs
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}}
}
11 changes: 11 additions & 0 deletions generator/templates/subject.hbs
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}}
}

0 comments on commit 5174e2d

Please sign in to comment.