Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@

https://github.com/oxidecomputer/typify/compare/v0.8.0\...HEAD[Full list of commits]

**Big Release**

Type generation moves to the `typespace` crate. This impacts a lot of API
surface area for both builder and macro consumers including how settings are
configured and how rendered code is produced, including item ordering in the
rendered code.

Feedback is very welcome as we head to a proper release.

== 0.8.0 (released 2026-09-09)

https://github.com/oxidecomputer/typify/compare/v0.7.0\...v0.8.0[Full list of commits]
Expand Down
61 changes: 57 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ members = [
resolver = "2"

[workspace.dependencies]
typify = { version = "0.8.0", path = "typify" }
typify-impl = { version = "0.8.0", path = "typify-impl" }
typify-macro = { version = "0.8.0", path = "typify-macro" }
typify = { version = "0.10.0-alpha.1", path = "typify" }
typify-impl = { version = "0.10.0-alpha.1", path = "typify-impl" }
typify-macro = { version = "0.10.0-alpha.1", path = "typify-macro" }

typespace = "0.0.1-alpha.2"

assert_cmd = "2.2.2"
chrono = { version = "0.4.45", features = ["serde"] }
Expand Down
3 changes: 2 additions & 1 deletion cargo-typify/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cargo-typify"
version = "0.8.0"
version = "0.10.0-alpha.1"
edition = "2021"
license = "Apache-2.0"
description = "cargo command to generate Rust code from a JSON Schema"
Expand All @@ -12,6 +12,7 @@ categories = ["api-bindings", "compilers"]
default-run = "cargo-typify"

[dependencies]
syn = { workspace = true }
typify = { workspace = true }

clap = { workspace = true }
Expand Down
60 changes: 41 additions & 19 deletions cargo-typify/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use std::path::PathBuf;

use clap::{ArgGroup, Args};
use color_eyre::eyre::{eyre, Context, Result};
use typify::{CrateVers, MapType, TypeSpace, TypeSpaceSettings, UnknownPolicy};
use typify::typespace::settings::{ContainerType, TraitSpec};
use typify::{CrateVers, TypeSpace, TypeSpaceSettings, UnknownPolicy};

/// A CLI for the `typify` crate that converts JSON Schema files to Rust code.
#[derive(Args)]
Expand Down Expand Up @@ -144,16 +145,44 @@ pub fn convert(args: &CliArgs) -> Result<String> {
let schema = serde_json::from_str::<schemars::schema::RootSchema>(&content)
.wrap_err("Failed to parse input file as JSON Schema")?;

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

for derive in &args.additional_derives {
settings.with_derive(derive.clone());
}
// For JSON Schema, a map key is always string-like, so any type a path
// names can use the HashMap preset with the given type path. The parse is
// checked here so a bad path is a CLI error rather than a panic inside
// typespace.
let map_type = args
.map_type
.as_deref()
.map(|path| {
syn::parse_str::<syn::Type>(path)
.map(|_| ContainerType::hash_map().with_path(path))
.map_err(|err| eyre!("invalid map type {path:?}: {err}"))
})
.transpose()
.wrap_err("Invalid map type")?;

// A derive names a trait typespace models or a foreign one; typespace
// tells them apart. A bad name is a CLI error.
let derives = args
.additional_derives
.iter()
.map(|derive| TraitSpec::parse(derive).map_err(|err| eyre!("{err}")))
.collect::<Result<Vec<_>>>()
.wrap_err("Invalid derive")?;

for attr in &args.additional_attrs {
settings.with_attr(attr.clone());
}
let mut settings = TypeSpaceSettings::default();
settings.map_typespace_settings(|mut typespace| {
typespace = typespace.with_struct_builder(args.use_builder());
for derive in derives {
typespace = typespace.with_extra_required_trait(derive);
}
for attr in &args.additional_attrs {
typespace = typespace.with_attr(attr.clone());
}
if let Some(map_type) = map_type {
typespace = typespace.with_map_type(map_type);
}
typespace
});

for CrateSpec {
name,
Expand All @@ -164,14 +193,6 @@ pub fn convert(args: &CliArgs) -> Result<String> {
settings.with_crate(name, version.clone(), rename.as_ref());
}

if let Some(map_type) = &args.map_type {
let map_type = map_type
.parse::<MapType>()
.map_err(|msg| eyre!(msg))
.wrap_err("Invalid map type")?;
settings.with_map_type(map_type);
}

if let Some(unknown_crates) = &args.unknown_crates {
let unknown_crates = match unknown_crates.as_str() {
"generate" => UnknownPolicy::Generate,
Expand All @@ -193,7 +214,8 @@ pub fn convert(args: &CliArgs) -> Result<String> {
#![allow(clippy::clone_on_copy)]
";

let contents = format!("{intro}\n{}", type_space.to_stream());
let stream = type_space.to_stream().wrap_err("Type generation failed")?;
let contents = format!("{intro}\n{stream}");

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

Expand Down
6 changes: 3 additions & 3 deletions cargo-typify/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ fn test_derive() {
input,
"--no-builder",
"--additional-derive",
"ExtraDerive",
"::extra::ExtraDerive",
"--output",
output_file.to_str().unwrap(),
])
Expand Down Expand Up @@ -141,9 +141,9 @@ fn test_multi_derive() {
input,
"--no-builder",
"--additional-derive",
"ExtraDerive",
"::extra::ExtraDerive",
"--additional-derive",
"AnotherDerive",
"::extra::AnotherDerive",
"--output",
output_file.to_str().unwrap(),
])
Expand Down
10 changes: 6 additions & 4 deletions cargo-typify/tests/outputs/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#![allow(clippy::clone_on_copy)]

#[doc = "`Fruit`"]
#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, ExtraDerive)]
#[derive(:: extra :: ExtraDerive, :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)]
#[serde(transparent)]
pub struct Fruit(pub ::std::collections::HashMap<::std::string::String, ::std::string::String>);
impl ::std::ops::Deref for Fruit {
Expand All @@ -30,7 +30,7 @@ impl ::std::convert::From<::std::collections::HashMap<::std::string::String, ::s
}
}
#[doc = "`FruitOrVeg`"]
#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, ExtraDerive)]
#[derive(:: extra :: ExtraDerive, :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)]
#[serde(untagged)]
pub enum FruitOrVeg {
Veg(Veggie),
Expand All @@ -47,7 +47,7 @@ impl ::std::convert::From<Fruit> for FruitOrVeg {
}
}
#[doc = "`Veggie`"]
#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, ExtraDerive)]
#[derive(:: extra :: ExtraDerive, :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)]
pub struct Veggie {
#[doc = "Do I like this vegetable?"]
#[serde(rename = "veggieLike")]
Expand All @@ -57,7 +57,9 @@ pub struct Veggie {
pub veggie_name: ::std::string::String,
}
#[doc = "A representation of a person, company, organization, or place"]
#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, Default, ExtraDerive)]
#[derive(
:: extra :: ExtraDerive, :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, Default,
)]
pub struct Veggies {
#[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")]
pub fruits: ::std::vec::Vec<::std::string::String>,
Expand Down
25 changes: 20 additions & 5 deletions cargo-typify/tests/outputs/multi_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

#[doc = "`Fruit`"]
#[derive(
:: serde :: Deserialize, :: serde :: Serialize, AnotherDerive, Clone, Debug, ExtraDerive,
:: extra :: AnotherDerive,
:: extra :: ExtraDerive,
:: serde :: Deserialize,
:: serde :: Serialize,
Clone,
Debug,
)]
#[serde(transparent)]
pub struct Fruit(pub ::std::collections::HashMap<::std::string::String, ::std::string::String>);
Expand Down Expand Up @@ -33,7 +38,12 @@ impl ::std::convert::From<::std::collections::HashMap<::std::string::String, ::s
}
#[doc = "`FruitOrVeg`"]
#[derive(
:: serde :: Deserialize, :: serde :: Serialize, AnotherDerive, Clone, Debug, ExtraDerive,
:: extra :: AnotherDerive,
:: extra :: ExtraDerive,
:: serde :: Deserialize,
:: serde :: Serialize,
Clone,
Debug,
)]
#[serde(untagged)]
pub enum FruitOrVeg {
Expand All @@ -52,7 +62,12 @@ impl ::std::convert::From<Fruit> for FruitOrVeg {
}
#[doc = "`Veggie`"]
#[derive(
:: serde :: Deserialize, :: serde :: Serialize, AnotherDerive, Clone, Debug, ExtraDerive,
:: extra :: AnotherDerive,
:: extra :: ExtraDerive,
:: serde :: Deserialize,
:: serde :: Serialize,
Clone,
Debug,
)]
pub struct Veggie {
#[doc = "Do I like this vegetable?"]
Expand All @@ -64,13 +79,13 @@ pub struct Veggie {
}
#[doc = "A representation of a person, company, organization, or place"]
#[derive(
:: extra :: AnotherDerive,
:: extra :: ExtraDerive,
:: serde :: Deserialize,
:: serde :: Serialize,
AnotherDerive,
Clone,
Debug,
Default,
ExtraDerive,
)]
pub struct Veggies {
#[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")]
Expand Down
6 changes: 4 additions & 2 deletions example-build/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ fn main() {
let content = std::fs::read_to_string("../example.json").unwrap();
let schema = serde_json::from_str::<schemars::schema::RootSchema>(&content).unwrap();

let mut type_space = TypeSpace::new(TypeSpaceSettings::default().with_struct_builder(true));
let mut type_space = TypeSpace::new(
TypeSpaceSettings::default().map_typespace_settings(|s| s.with_struct_builder(true)),
);
type_space.add_root_schema(schema).unwrap();

let contents =
prettyplease::unparse(&syn::parse2::<syn::File>(type_space.to_stream()).unwrap());
prettyplease::unparse(&syn::parse2::<syn::File>(type_space.to_stream().unwrap()).unwrap());

let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf();
out_file.push("codegen.rs");
Expand Down
Loading
Loading