Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cargo-typify: CLI for generating Rust Types #204

Merged
merged 33 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a127a20
Create a new binary package for `cargo-typify`
coreyja Feb 25, 2023
a8d1ced
First version
coreyja Feb 26, 2023
6078576
Just cleanup the Cargo.toml file a bit
coreyja Feb 26, 2023
cc49c4f
Unify all the schemars versions across the workspace
coreyja Feb 26, 2023
47f3074
Cleanup unused import and format Cargo.toml
coreyja Feb 26, 2023
2c89d5d
Remove this example schema since I can use the ones in other dirs
coreyja Feb 26, 2023
bcc29d5
Revert making schemars a workspace dep, that change should be in its …
coreyja Mar 11, 2023
c20f23b
Copy release.toml from typify-impl
coreyja Mar 11, 2023
d6202e6
Add the clippy bits at the beggining
coreyja Mar 11, 2023
0a1a267
Take the suggested doc comment, which will be the help text in the CLI
coreyja Mar 11, 2023
42e1aa2
Use Expectorate to setup a basic test for converting a file
coreyja Mar 11, 2023
74c0e19
Lets run rust_fmt on the input
coreyja Mar 11, 2023
cced055
Extract to a lib.rs and do tests in there
coreyja Mar 11, 2023
abde9fc
Make the input path to the cli a positional instead of an arg
coreyja Mar 11, 2023
a726932
Add arg to create builder interface and add test for it
coreyja Mar 11, 2023
26247ab
Add integration tests but they are really the same as the other tests…
coreyja Mar 11, 2023
851adc7
Test the stdout as well
coreyja Mar 11, 2023
accbf7e
Test a single additional derive
coreyja Mar 11, 2023
5e06e97
Get multiple derives working as well
coreyja Mar 11, 2023
c278744
Get something commited for type_mod but its not working
coreyja Mar 11, 2023
71c2b40
Add default run
coreyja Mar 11, 2023
ba40e53
Merge branch 'main' into ca/main/cargo-typify
coreyja Mar 11, 2023
ffe8a4e
Remove this todo since it doesn't change the output
coreyja Mar 11, 2023
407504d
Use the same version of schemars as the other crates
coreyja Mar 11, 2023
2dba827
We aren't actually using this one
coreyja Mar 11, 2023
497e788
Add test for the help output, and remove predicates dep since we can …
coreyja Mar 13, 2023
042ff53
Change the CLI so that we do a replace of the file with a new extensi…
coreyja Mar 13, 2023
6955c0e
Remove type_mod that wasn't doing anything to the output
coreyja Mar 13, 2023
cddde14
Rework this to work as a cargo sub-command
coreyja Mar 13, 2023
c457b6a
Remove non-integration specs and add some simple specs for the parsin…
coreyja Mar 13, 2023
b0f93e0
Merge branch 'main' into ca/main/cargo-typify
coreyja Mar 13, 2023
95241d2
Add positional arg as inverse of builder, builder is now the default
coreyja Mar 15, 2023
84491f6
Convert line endings for tests in Windows
coreyja Mar 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
507 changes: 505 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"typify-impl",
"typify-macro",
"typify-test",
"cargo-typify",
"example-build",
"example-macro",
]
Expand Down
21 changes: 21 additions & 0 deletions cargo-typify/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "cargo-typify"
version = "0.0.11-dev"
edition = "2021"

ahl marked this conversation as resolved.
Show resolved Hide resolved
default-run = "cargo-typify"

[dependencies]
typify = { version = "0.0.11-dev", path = "../typify" }

clap = { version = "4.1.6", features = ["derive"] }
ahl marked this conversation as resolved.
Show resolved Hide resolved
color-eyre = "0.6"
ahl marked this conversation as resolved.
Show resolved Hide resolved
serde_json = "1.0.93"
schemars = "0.8.12"
rustfmt-wrapper = "0.2.0"

[dev-dependencies]
assert_cmd = "2.0.8"
ahl marked this conversation as resolved.
Show resolved Hide resolved
expectorate = "1.0.6"
newline-converter = "0.2.2"
tempdir = "0.3.7"
1 change: 1 addition & 0 deletions cargo-typify/release.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pre-release-replacements = []
190 changes: 190 additions & 0 deletions cargo-typify/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use std::path::PathBuf;

use clap::{ArgGroup, Args};
use color_eyre::eyre::{Context, Result};
use schemars::schema::Schema;
use typify::{TypeSpace, TypeSpaceSettings};

/// A CLI for the `typify` crate that converts JSON Schema files to Rust code.
#[derive(Args)]
#[command(author, version, about)]
#[command(group(
ArgGroup::new("build")
.args(["builder", "positional"]),
))]
pub struct CliArgs {
/// The input file to read from
pub input: PathBuf,

/// Whether to include a builder-style interface, this is the default.
#[arg(short, long, default_value = "false", group = "build")]
pub builder: bool,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I didn't think of this earlier. I'd like to make the builder style the default. What do you think about two flags: --builder and --positional that are mutually exclusive?


/// Inverse of `--builder`. When set the builder-style interface will not be included.
#[arg(short, long, default_value = "false", group = "build")]
pub positional: bool,

/// Add an additional derive macro to apply to all defined types.
#[arg(short, long)]
pub additional_derives: Vec<String>,

/// The output file to write to. If not specified, the input file name will be used with a
/// `.rs` extension.
///
/// If `-` is specified, the output will be written to stdout.
#[arg(short, long)]
pub output: Option<PathBuf>,
}

impl CliArgs {
pub fn output_path(&self) -> Option<PathBuf> {
match &self.output {
Some(output_path) => {
if output_path == &PathBuf::from("-") {
None
} else {
Some(output_path.clone())
}
}
None => {
let mut output = self.input.clone();
output.set_extension("rs");
Some(output)
}
}
}

pub fn use_builder(&self) -> bool {
!self.positional
}
}

pub fn convert(args: &CliArgs) -> Result<String> {
let content = std::fs::read_to_string(&args.input)
.wrap_err_with(|| format!("Failed to open input file: {}", &args.input.display()))?;

let schema = serde_json::from_str::<schemars::schema::RootSchema>(&content)
.wrap_err("Failed to parse input file as JSON Schema")?;

let mut settings = &mut TypeSpaceSettings::default();
settings = settings.with_struct_builder(args.use_builder());

for derive in &args.additional_derives {
settings = settings.with_derive(derive.clone());
}

let mut type_space = TypeSpace::new(settings);
type_space
.add_ref_types(schema.definitions)
.wrap_err("Could not add ref types from the 'definitions' field in the JSON Schema")?;

let base_type = &schema.schema;

// Only convert the top-level type if it has a name
if let Some(base_title) = &(|| base_type.metadata.as_ref()?.title.as_ref())() {
let base_title = base_title.to_string();

type_space
.add_type(&Schema::Object(schema.schema))
.wrap_err_with(|| {
format!("Could not add the top level type `{base_title}` to the type space")
})?;
}

let intro = "#![allow(clippy::redundant_closure_call)]
#![allow(clippy::needless_lifetimes)]
#![allow(clippy::match_single_binding)]
#![allow(clippy::clone_on_copy)]

use serde::{Deserialize, Serialize};
";

let contents = format!("{intro}\n{}", type_space.to_string());

let contents = rustfmt_wrapper::rustfmt(contents).wrap_err("Failed to format Rust code")?;

Ok(contents)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_output_parsing_stdout() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: false,
additional_derives: vec![],
output: Some(PathBuf::from("-")),
positional: false,
};

assert_eq!(args.output_path(), None);
}

#[test]
fn test_output_parsing_file() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: false,
additional_derives: vec![],
output: Some(PathBuf::from("some_file.rs")),
positional: false,
};

assert_eq!(args.output_path(), Some(PathBuf::from("some_file.rs")));
}

#[test]
fn test_output_parsing_default() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: false,
additional_derives: vec![],
output: None,
positional: false,
};

assert_eq!(args.output_path(), Some(PathBuf::from("input.rs")));
}

#[test]
fn test_builder_as_default_style() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: false,
additional_derives: vec![],
output: None,
positional: false,
};

assert!(args.use_builder());
}

#[test]
fn test_positional_builder() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: false,
additional_derives: vec![],
output: None,
positional: true,
};

assert!(!args.use_builder());
}

#[test]
fn test_builder_opt_in() {
let args = CliArgs {
input: PathBuf::from("input.json"),
builder: true,
additional_derives: vec![],
output: None,
positional: false,
};

assert!(args.use_builder());
}
}
32 changes: 32 additions & 0 deletions cargo-typify/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use cargo_typify::{convert, CliArgs};
use clap::Parser;

use color_eyre::eyre::{Context, Result};

#[derive(Parser)] // requires `derive` feature
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
enum CargoCli {
Typify(CliArgs),
}
Comment on lines +6 to +11
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was added to make the cargo sub-command work nicely!

To run the cli locally you now need to do something like the following:

cargo run -- typify some_file.json

This style was taken from the Clap cookbook: https://docs.rs/clap/latest/clap/_derive/_cookbook/cargo_example_derive/index.html


fn main() -> Result<()> {
color_eyre::install()?;

let cli = CargoCli::parse();
let CargoCli::Typify(args) = cli;

let contents = convert(&args).wrap_err("Failed to convert JSON Schema to Rust code")?;

let output_path = args.output_path();

if let Some(output_path) = &output_path {
std::fs::write(output_path, contents).wrap_err_with(|| {
format!("Failed to write output to file: {}", output_path.display())
})?;
} else {
print!("{}", contents);
}

Ok(())
}
Loading