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: diff --git a/Cargo.lock b/Cargo.lock index 33d80421..4b0cc482 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,13 +251,15 @@ 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 = "61018db61b1cdf32bed05d3addbd3a49d03be38e1a7674e067e6ded193d0cf49" +checksum = "a9b10f68d82f8de01589cd0ab1a21cddaf271b2a936d0bb2518c56a3498efe2f" dependencies = [ "proc-macro2", "quote", + "semver", "syn 3.0.6", + "version-ranges", ] [[package]] @@ -1028,6 +1030,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" @@ -1305,9 +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 = "e351db219780cb453ee29422f86eaba6fa2538c45febb6043c2424595412fd02" +checksum = "5ac9bd6fcb72ab3ddfb1b9b20e28520f697df23a4b4340562685e7b6a8e8dc44" dependencies = [ "codespace", "heck", @@ -1441,6 +1449,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..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 = "0.0.1-alpha.2" +typespace = "0.0.1-alpha.3" 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..12a4d5b0 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -126,10 +126,12 @@ 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 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, @@ -144,10 +146,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 +744,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 +770,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) => { @@ -809,17 +792,30 @@ impl TypeSpace { /// All code for processed types. /// - /// 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 + /// 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_stream(&self) -> Result { - let typespace = self.to_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 `TokenStream`. + 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")); +}