From a159cc6ed3f96db60f2f8ce4e3d88824ea81571e Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 4 Oct 2026 12:01:35 -0700 Subject: [PATCH 1/4] report native crates through the codespace The uses_* booleans are gone. chrono, uuid, and each x-rust-type crate (at its with_crate version and rename) are recorded on the typespace builder, and TypeSpace::to_codespace hands back the codespace whose dependencies list them with typespace's own. --- Cargo.lock | 22 ++++++-- Cargo.toml | 2 +- typify-impl/src/convert.rs | 10 ++-- typify-impl/src/lib.rs | 71 ++++++++++++------------ typify-impl/src/rust_extension.rs | 33 +++++++++--- typify-impl/tests/test_dependencies.rs | 75 ++++++++++++++++++++++++++ 6 files changed, 157 insertions(+), 56 deletions(-) create mode 100644 typify-impl/tests/test_dependencies.rs diff --git a/Cargo.lock b/Cargo.lock index 33d80421..5b90ca46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,12 +252,12 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "codespace" version = "0.0.1-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61018db61b1cdf32bed05d3addbd3a49d03be38e1a7674e067e6ded193d0cf49" dependencies = [ "proc-macro2", "quote", + "semver", "syn 3.0.6", + "version-ranges", ] [[package]] @@ -1028,6 +1028,12 @@ dependencies = [ "bstr", ] +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + [[package]] name = "strsim" version = "0.11.1" @@ -1306,8 +1312,6 @@ dependencies = [ [[package]] name = "typespace" version = "0.0.1-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e351db219780cb453ee29422f86eaba6fa2538c45febb6043c2424595412fd02" dependencies = [ "codespace", "heck", @@ -1441,6 +1445,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e9bd4e9c9ff6a2a9b5969462ba26216af3e010df0377dad8320ab515262ef8" +dependencies = [ + "semver", + "smallvec", +] + [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 498dfc95..925afbd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ 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" +typespace = { path = "../typespace-codespace-deps/typespace" } assert_cmd = "2.2.2" chrono = { version = "0.4.45", features = ["serde"] } diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index 03251aa6..f1ee7c8b 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -799,7 +799,7 @@ impl TypeSpace { ) -> Result<(TypeEntry, &'a Option>)> { match format.as_ref().map(String::as_str) { Some("uuid") => { - self.uses_uuid = true; + self.add_dependency(typespace::codespace::Dependency::new("uuid")); // ::uuid::Uuid Default, but we ignore it because there's no // useful value. We assume the uuid feature in schemars. Ok(( @@ -827,7 +827,7 @@ impl TypeSpace { } Some("date") => { - self.uses_chrono = true; + self.add_dependency(typespace::codespace::Dependency::new("chrono")); // ::chrono::naive::NaiveDate implements Default, but there's // no useful value. We assume the chrono feaeture in schemars. Ok(( @@ -854,7 +854,7 @@ impl TypeSpace { )) } Some("date-time") => { - self.uses_chrono = true; + self.add_dependency(typespace::codespace::Dependency::new("chrono")); // ::chrono::DateTime implements Default, but not usefully. // We assume the chrono feaeture in schemars. Ok(( @@ -976,7 +976,6 @@ impl TypeSpace { type_name: type_name.clone().into_option(), reason: format!("invalid pattern '{}' {}", pattern, e), })?; - self.uses_regress = true; } let string = TypeEntry::from(Type::String); @@ -1948,7 +1947,6 @@ impl TypeSpace { unique_items, contains: None, } => { - self.uses_serde_json = true; let type_id = self.assign_type(TypeEntry::from(Type::JsonValue)); // If items are unique, this is a Set; otherwise it's an Array. @@ -1969,7 +1967,6 @@ impl TypeSpace { &mut self, metadata: &'a Option>, ) -> Result<(TypeEntry, &'a Option>)> { - self.uses_serde_json = true; let type_id = self.assign_type(TypeEntry::from(Type::JsonValue)); Ok((Type::Vec(type_id).into(), metadata)) } @@ -1986,7 +1983,6 @@ impl TypeSpace { &mut self, metadata: &'a Option>, ) -> Result<(TypeEntry, &'a Option>)> { - self.uses_serde_json = true; Ok((Type::JsonValue.into(), metadata)) } diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 5e9b0870..5e6892f9 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -126,10 +126,13 @@ pub struct TypeSpace { name_to_id: BTreeMap, ref_to_id: BTreeMap, - uses_chrono: bool, - uses_uuid: bool, - uses_serde_json: bool, - uses_regress: bool, + /// The crates behind the native types typify chose: `chrono` and + /// `uuid` for the string formats it maps to them, and each crate an + /// `x-rust-type` extension names, at the version + /// [`TypeSpaceSettings::with_crate`] declared for it. Recorded on + /// the `TypespaceBuilder` so the finalized typespace reports them + /// with the crates of what typespace renders itself. + dependencies: Vec, settings: TypeSpaceSettings, @@ -144,10 +147,7 @@ impl Default for TypeSpace { id_to_entry: Default::default(), name_to_id: Default::default(), ref_to_id: Default::default(), - uses_chrono: Default::default(), - uses_uuid: Default::default(), - uses_serde_json: Default::default(), - uses_regress: Default::default(), + dependencies: Default::default(), settings: Default::default(), cache: Default::default(), } @@ -745,26 +745,6 @@ impl TypeSpace { } } - /// Whether the generated code needs `chrono` crate. - pub fn uses_chrono(&self) -> bool { - self.uses_chrono - } - - /// Whether the generated code needs [regress] crate. - pub fn uses_regress(&self) -> bool { - self.uses_regress - } - - /// Whether the generated code needs [serde_json] crate. - pub fn uses_serde_json(&self) -> bool { - self.uses_serde_json - } - - /// Whether the generated code needs `uuid` crate. - pub fn uses_uuid(&self) -> bool { - self.uses_uuid - } - /// The type inserted under `type_id`, as it was inserted. /// /// This is the declaration typify handed typespace, available before @@ -791,6 +771,10 @@ impl TypeSpace { pub fn to_typespace(&self) -> Result> { let mut builder = typespace::TypespaceBuilder::new(self.settings.typespace.clone()); + for dependency in &self.dependencies { + builder.add_dependency(dependency.clone()); + } + for (type_id, type_entry) in &self.id_to_entry { match type_entry { TypeEntry::Type(typ) => { @@ -807,19 +791,34 @@ impl TypeSpace { Ok(builder.finalize(|inner: &TypeId| TypeId(inner.0 | (1 << 63)))?) } - /// All code for processed types. + /// All code for processed types, as a codespace. /// /// Rendering is delegated to typespace: the stored types are /// inserted into a `TypespaceBuilder`, finalized, and rendered - /// through codespace. Finalization errors (dangling references, - /// name collisions, unsatisfiable trait requirements) surface as - /// [`Error::Typespace`]. - pub fn to_stream(&self) -> Result { - let typespace = self.to_typespace()?; + /// through codespace. The codespace tracks the crates its code + /// depends on; see its `dependencies`. Finalization errors (dangling + /// references, name collisions, unsatisfiable trait requirements) + /// surface as [`Error::Typespace`]. + pub fn to_codespace(&self) -> Result { + Ok(self.to_typespace()?.to_codespace()) + } - let codespace = typespace.to_codespace(); + /// All code for processed types. + /// + /// [`TypeSpace::to_codespace`] rendered to a token stream. + pub fn to_stream(&self) -> Result { + Ok(self.to_codespace()?.into_stream()) + } - Ok(codespace.into_stream()) + /// Record a crate behind a native type, once per crate. + fn add_dependency(&mut self, dependency: typespace::codespace::Dependency) { + if !self + .dependencies + .iter() + .any(|known| known.name == dependency.name) + { + self.dependencies.push(dependency); + } } /// Allocated the next TypeId. diff --git a/typify-impl/src/rust_extension.rs b/typify-impl/src/rust_extension.rs index e7a6cf5f..c2ac5708 100644 --- a/typify-impl/src/rust_extension.rs +++ b/typify-impl/src/rust_extension.rs @@ -57,26 +57,42 @@ impl TypeSpace { return None; } - let path = { + let (path, dependency) = { if let Some(crate_spec) = self.settings.crates.get(crate_name.as_str()) { // The version must be non-Never and match the requirements // from the extension. - match &crate_spec.version { - CrateVers::Any => (), - CrateVers::Version(version) if req.matches(version) => (), + let version = match &crate_spec.version { + CrateVers::Any => semver::VersionReq::STAR, + CrateVers::Version(version) if req.matches(version) => { + semver::VersionReq::parse(&version.to_string()) + .expect("a version is a valid requirement") + } _ => return None, - } + }; + let dependency = typespace::codespace::Dependency { + version, + ..typespace::codespace::Dependency::new(&crate_name) + }; // Replace the initial path component with the new crate name. if let Some(new_crate) = &crate_spec.rename { - format!("{}{}", new_crate.replace('-', "_"), &path[path_sep..]) + let new_crate = new_crate.replace('-', "_"); + ( + format!("{}{}", new_crate, &path[path_sep..]), + typespace::codespace::Dependency { + rename: Some(new_crate), + ..dependency + }, + ) } else { - path + (path, dependency) } } else { match self.settings.unknown_crates { crate::UnknownPolicy::Generate => return None, - crate::UnknownPolicy::Allow => path, + crate::UnknownPolicy::Allow => { + (path, typespace::codespace::Dependency::new(&crate_name)) + } // TODO need to bubble up a coherent compiler error via the // generated code. @@ -84,6 +100,7 @@ impl TypeSpace { } } }; + self.add_dependency(dependency); // Convert and collect type parameters. let param_ids = parameters diff --git a/typify-impl/tests/test_dependencies.rs b/typify-impl/tests/test_dependencies.rs new file mode 100644 index 00000000..8b3d465b --- /dev/null +++ b/typify-impl/tests/test_dependencies.rs @@ -0,0 +1,75 @@ +// Copyright 2026 Oxide Computer Company + +//! The crates a generated type space reports. + +use schemars::schema::Schema; +use typify_impl::{CrateVers, TypeSpace, TypeSpaceSettings}; + +fn names(type_space: &TypeSpace) -> Vec { + type_space + .to_codespace() + .unwrap() + .dependencies() + .map(|dep| dep.name.clone()) + .collect() +} + +/// A string format mapped to a native type reports that type's crate, +/// alongside what typespace reports for the rendered code. +#[test] +fn formats_report_their_crates() { + let schema: Schema = serde_json::from_value(serde_json::json!({ + "type": "object", + "properties": { + "when": { "type": "string", "format": "date-time" }, + "id": { "type": "string", "format": "uuid" } + }, + "required": ["when", "id"] + })) + .unwrap(); + let mut type_space = TypeSpace::new(&TypeSpaceSettings::default()); + type_space + .add_type_with_name(&schema, Some("Thing".to_string())) + .unwrap(); + assert_eq!(names(&type_space), ["chrono", "serde", "uuid"]); +} + +/// A type from an `x-rust-type` extension reports its crate at the +/// version the settings declared for it, renamed if the settings +/// renamed it. +#[test] +fn rust_extension_crates_carry_their_version() { + let schema: Schema = serde_json::from_value(serde_json::json!({ + "type": "object", + "properties": { + "value": { + "type": "string", + "x-rust-type": { + "crate": "my-crate", + "version": "1.2.3", + "path": "my_crate::Value" + } + } + }, + "required": ["value"] + })) + .unwrap(); + let mut settings = TypeSpaceSettings::default(); + settings.with_crate( + "my-crate", + CrateVers::Version("1.2.3".parse().unwrap()), + Some(&"their-crate".to_string()), + ); + let mut type_space = TypeSpace::new(&settings); + type_space + .add_type_with_name(&schema, Some("Thing".to_string())) + .unwrap(); + + let codespace = type_space.to_codespace().unwrap(); + let my_crate = codespace + .dependencies() + .find(|dep| dep.name == "my-crate") + .unwrap(); + assert_eq!(my_crate.version.to_string(), "^1.2.3"); + assert_eq!(my_crate.rename.as_deref(), Some("their_crate")); +} From df516aee29c6079d6d23e36118fbeeb0fe48a7a8 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 4 Oct 2026 19:30:02 -0700 Subject: [PATCH 2/4] nits --- Cargo.lock | 4 +++- typify-impl/src/lib.rs | 25 +++++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b90ca46..944e1174 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,7 +251,9 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "codespace" -version = "0.0.1-alpha.2" +version = "0.0.1-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b10f68d82f8de01589cd0ab1a21cddaf271b2a936d0bb2518c56a3498efe2f" dependencies = [ "proc-macro2", "quote", diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 5e6892f9..12a4d5b0 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -126,12 +126,11 @@ pub struct TypeSpace { name_to_id: BTreeMap, ref_to_id: BTreeMap, - /// The crates behind the native types typify chose: `chrono` and - /// `uuid` for the string formats it maps to them, and each crate an - /// `x-rust-type` extension names, at the version - /// [`TypeSpaceSettings::with_crate`] declared for it. Recorded on - /// the `TypespaceBuilder` so the finalized typespace reports them - /// with the crates of what typespace renders itself. + /// The crates needed for native types: `chrono` and `uuid` for the string + /// formats it maps to them, and each crate an `x-rust-type` extension + /// names, at the version [`TypeSpaceSettings::with_crate`] declared for + /// it. Recorded on the `TypespaceBuilder` so the finalized typespace + /// reports them with the crates of what typespace renders itself. dependencies: Vec, settings: TypeSpaceSettings, @@ -791,21 +790,19 @@ impl TypeSpace { Ok(builder.finalize(|inner: &TypeId| TypeId(inner.0 | (1 << 63)))?) } - /// All code for processed types, as a codespace. + /// All code for processed types. /// - /// Rendering is delegated to typespace: the stored types are - /// inserted into a `TypespaceBuilder`, finalized, and rendered - /// through codespace. The codespace tracks the crates its code - /// depends on; see its `dependencies`. Finalization errors (dangling - /// references, name collisions, unsatisfiable trait requirements) - /// surface as [`Error::Typespace`]. + /// Rendering is delegated to typespace: the stored types are inserted into + /// a `TypespaceBuilder`, finalized, and rendered through codespace. The + /// codespace tracks depenencies. Finalization errors result in + /// [`Error::Typespace`]. pub fn to_codespace(&self) -> Result { Ok(self.to_typespace()?.to_codespace()) } /// All code for processed types. /// - /// [`TypeSpace::to_codespace`] rendered to a token stream. + /// [`TypeSpace::to_codespace`] rendered to a `TokenStream`. pub fn to_stream(&self) -> Result { Ok(self.to_codespace()?.into_stream()) } From 049ee696104482a7701741f132e8e7849b807afe Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 4 Oct 2026 21:26:43 -0700 Subject: [PATCH 3/4] depend on typespace 0.0.1-alpha.3 --- Cargo.lock | 4 +++- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 944e1174..4b0cc482 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,7 +1313,9 @@ dependencies = [ [[package]] name = "typespace" -version = "0.0.1-alpha.2" +version = "0.0.1-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac9bd6fcb72ab3ddfb1b9b20e28520f697df23a4b4340562685e7b6a8e8dc44" dependencies = [ "codespace", "heck", diff --git a/Cargo.toml b/Cargo.toml index 925afbd9..7e6eace6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ 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 = { path = "../typespace-codespace-deps/typespace" } +typespace = "0.0.1-alpha.3" assert_cmd = "2.2.2" chrono = { version = "0.4.45", features = ["serde"] } From f1cda8308033e46f9a3eab425cd183115ef11683 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 4 Oct 2026 21:31:52 -0700 Subject: [PATCH 4/4] add CI --- .github/workflows/rust.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index da63d037..4a1a9a37 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -5,9 +5,9 @@ name: Build on: push: - branches: [ main ] + branches: [ main, v0_10 ] pull_request: - branches: [ main ] + branches: [ main, v0_10 ] jobs: check-style: