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/CHANGELOG.adoc b/CHANGELOG.adoc index 72e237ed..ec5049e6 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -13,7 +13,20 @@ == Unreleased changes (release date TBD) -https://github.com/oxidecomputer/typify/compare/v0.8.0\...HEAD[Full list of commits] +https://github.com/oxidecomputer/typify/compare/v0.10.0-alpha.2\...HEAD[Full list of commits] + +== 0.10.0-alpha.2 (released 2026-10-05) + +https://github.com/oxidecomputer/typify/compare/v0.8.0\...v0.10.0-alpha.2[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) diff --git a/Cargo.lock b/Cargo.lock index 5400cb98..b0614a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,7 +163,7 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "cargo-typify" -version = "0.8.0" +version = "0.10.0-alpha.2" dependencies = [ "assert_cmd", "clap", @@ -175,6 +175,7 @@ dependencies = [ "schemars", "semver", "serde_json", + "syn 3.0.6", "tempfile", "typify", ] @@ -248,6 +249,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "codespace" +version = "0.0.1-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b10f68d82f8de01589cd0ab1a21cddaf271b2a936d0bb2518c56a3498efe2f" +dependencies = [ + "proc-macro2", + "quote", + "semver", + "syn 3.0.6", + "version-ranges", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -1016,12 +1030,39 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "syn" version = "2.0.118" @@ -1270,9 +1311,28 @@ dependencies = [ "toml 1.0.1+spec-1.1.0", ] +[[package]] +name = "typespace" +version = "0.0.1-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac9bd6fcb72ab3ddfb1b9b20e28520f697df23a4b4340562685e7b6a8e8dc44" +dependencies = [ + "codespace", + "heck", + "log", + "proc-macro2", + "quote", + "schemars", + "serde", + "serde_json", + "strum", + "syn 3.0.6", + "thiserror 2.0.20", +] + [[package]] name = "typify" -version = "0.8.0" +version = "0.10.0-alpha.2" dependencies = [ "chrono", "env_logger", @@ -1292,7 +1352,7 @@ dependencies = [ [[package]] name = "typify-impl" -version = "0.8.0" +version = "0.10.0-alpha.2" dependencies = [ "env_logger", "expectorate", @@ -1310,13 +1370,14 @@ dependencies = [ "serde_json", "syn 3.0.6", "thiserror 2.0.20", + "typespace", "unicode-ident", "uuid", ] [[package]] name = "typify-macro" -version = "0.8.0" +version = "0.10.0-alpha.2" dependencies = [ "proc-macro2", "quote", @@ -1388,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 f98da9ea..87de2760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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.2", path = "typify" } +typify-impl = { version = "0.10.0-alpha.2", path = "typify-impl" } +typify-macro = { version = "0.10.0-alpha.2", path = "typify-macro" } + +typespace = "0.0.1-alpha.3" assert_cmd = "2.2.2" chrono = { version = "0.4.45", features = ["serde"] } diff --git a/cargo-typify/Cargo.toml b/cargo-typify/Cargo.toml index 95717a21..b33dee9c 100644 --- a/cargo-typify/Cargo.toml +++ b/cargo-typify/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-typify" -version = "0.8.0" +version = "0.10.0-alpha.2" edition = "2021" license = "Apache-2.0" description = "cargo command to generate Rust code from a JSON Schema" @@ -12,6 +12,7 @@ categories = ["api-bindings", "compilers"] default-run = "cargo-typify" [dependencies] +syn = { workspace = true } typify = { workspace = true } clap = { workspace = true } diff --git a/cargo-typify/src/lib.rs b/cargo-typify/src/lib.rs index f90edd05..0a1f3798 100644 --- a/cargo-typify/src/lib.rs +++ b/cargo-typify/src/lib.rs @@ -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)] @@ -144,16 +145,44 @@ pub fn convert(args: &CliArgs) -> Result { let schema = serde_json::from_str::(&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::(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::>>() + .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, @@ -164,14 +193,6 @@ pub fn convert(args: &CliArgs) -> Result { settings.with_crate(name, version.clone(), rename.as_ref()); } - if let Some(map_type) = &args.map_type { - let map_type = map_type - .parse::() - .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, @@ -193,7 +214,8 @@ pub fn convert(args: &CliArgs) -> Result { #![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")?; diff --git a/cargo-typify/tests/integration.rs b/cargo-typify/tests/integration.rs index a3d54b6a..5423ecec 100644 --- a/cargo-typify/tests/integration.rs +++ b/cargo-typify/tests/integration.rs @@ -91,7 +91,7 @@ fn test_derive() { input, "--no-builder", "--additional-derive", - "ExtraDerive", + "::extra::ExtraDerive", "--output", output_file.to_str().unwrap(), ]) @@ -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(), ]) diff --git a/cargo-typify/tests/outputs/derive.rs b/cargo-typify/tests/outputs/derive.rs index e77b9b9b..6dd0c4da 100644 --- a/cargo-typify/tests/outputs/derive.rs +++ b/cargo-typify/tests/outputs/derive.rs @@ -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 { @@ -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), @@ -47,7 +47,7 @@ impl ::std::convert::From 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")] @@ -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>, diff --git a/cargo-typify/tests/outputs/multi_derive.rs b/cargo-typify/tests/outputs/multi_derive.rs index d818e398..75b31e9e 100644 --- a/cargo-typify/tests/outputs/multi_derive.rs +++ b/cargo-typify/tests/outputs/multi_derive.rs @@ -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>); @@ -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 { @@ -52,7 +62,12 @@ impl ::std::convert::From 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?"] @@ -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")] diff --git a/example-build/build.rs b/example-build/build.rs index 9cecec6b..6fd8e080 100644 --- a/example-build/build.rs +++ b/example-build/build.rs @@ -8,11 +8,13 @@ fn main() { let content = std::fs::read_to_string("../example.json").unwrap(); let schema = serde_json::from_str::(&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::(type_space.to_stream()).unwrap()); + prettyplease::unparse(&syn::parse2::(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"); diff --git a/release.toml b/release.toml index e683d79b..745a3cd3 100644 --- a/release.toml +++ b/release.toml @@ -3,11 +3,11 @@ # Update the change log to reflect the new release and set us up for the next release. pre-release-replacements = [ # First, replace the current "Unreleased changes" header with one reflecting the new release version and date. - {file="../CHANGELOG.adoc", search="Unreleased changes \\(release date TBD\\)", replace="{{version}} (released {{date}})", exactly=1}, + {file="../CHANGELOG.adoc", search="Unreleased changes \\(release date TBD\\)", replace="{{version}} (released {{date}})", exactly=1, prerelease=true}, # Update the link to the list of raw commits in the formerly "Unreleased changes" section. It should end at the tag for the newly-released version. - {file="../CHANGELOG.adoc", search="\\\\.\\.\\.HEAD", replace="\\...{{tag_name}}", exactly=1}, + {file="../CHANGELOG.adoc", search="\\\\.\\.\\.HEAD", replace="\\...{{tag_name}}", exactly=1, prerelease=true}, # Next, append a new "Unreleased changes" header beneath the sentinel line. - {file="../CHANGELOG.adoc", search="// cargo-release: next header goes here \\(do not change this line\\)", replace="// cargo-release: next header goes here (do not change this line)\n\n== Unreleased changes (release date TBD)\n\nhttps://github.com/oxidecomputer/typify/compare/{{tag_name}}\\...HEAD[Full list of commits]", exactly=1}, + {file="../CHANGELOG.adoc", search="// cargo-release: next header goes here \\(do not change this line\\)", replace="// cargo-release: next header goes here (do not change this line)\n\n== Unreleased changes (release date TBD)\n\nhttps://github.com/oxidecomputer/typify/compare/{{tag_name}}\\...HEAD[Full list of commits]", exactly=1, prerelease=true}, ] pre-release-commit-message = "release typify {{version}}" diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml index 36b72134..edd86342 100644 --- a/typify-impl/Cargo.toml +++ b/typify-impl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typify-impl" -version = "0.8.0" +version = "0.10.0-alpha.2" edition = "2021" license = "Apache-2.0" description = "typify backend implementation" @@ -19,6 +19,7 @@ serde = { workspace = true } serde_json = { workspace = true } syn = { workspace = true } thiserror = { workspace = true } +typespace = { workspace = true } unicode-ident = { workspace = true } [dev-dependencies] diff --git a/typify-impl/src/conversions.rs b/typify-impl/src/conversions.rs index 355ddb18..dd37ace2 100644 --- a/typify-impl/src/conversions.rs +++ b/typify-impl/src/conversions.rs @@ -13,7 +13,7 @@ pub(crate) struct SchemaCache { impl SchemaCache { pub fn insert(&mut self, schema: &SchemaObject, type_name: &String, impls: &[TypeSpaceImpl]) { - let type_entry = TypeEntry::new_native(type_name, impls); + let type_entry = TypeEntry::new_native(type_name, TypeSpaceImpl::native_traits(impls)); self.schemas.push(( SchemaObject { metadata: None, diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index 7c1716e7..f1ee7c8b 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -3,22 +3,19 @@ use std::collections::BTreeSet; use crate::merge::{merge_all, try_merge_with_subschemas}; -use crate::type_entry::{ - EnumTagType, TypeEntry, TypeEntryDetails, TypeEntryEnum, TypeEntryNewtype, TypeEntryStruct, - Variant, VariantDetails, -}; +use crate::type_entry::{TypeEntry, TypeEntryEnum, TypeEntryNewtype, TypeEntryStruct, Variant}; use crate::util::{all_mutually_exclusive, ref_key, ReorderedInstanceType, StringValidator}; use log::{debug, info}; use schemars::schema::{ ArrayValidation, InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec, StringValidation, SubschemaValidation, }; +use typespace::build::{EnumTagType, Type, VariantDetails}; +use typespace::{TypespaceTrait, TypespaceTraitSet}; use crate::util::get_type_name; -use crate::{Error, Name, Result, TypeSpace, TypeSpaceImpl}; - -pub const STD_NUM_NONZERO_PREFIX: &str = "::std::num::NonZero"; +use crate::{Error, Name, Result, TypeSpace}; impl TypeSpace { pub(crate) fn convert_schema<'a>( @@ -802,55 +799,154 @@ 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(( TypeEntry::new_native( "::uuid::Uuid", - &[TypeSpaceImpl::Display, TypeSpaceImpl::FromStr], + [ + TypespaceTrait::Clone, + TypespaceTrait::Debug, + TypespaceTrait::Serialize, + TypespaceTrait::Deserialize, + TypespaceTrait::Display, + TypespaceTrait::FromStr, + TypespaceTrait::Eq, + TypespaceTrait::PartialEq, + TypespaceTrait::Ord, + TypespaceTrait::PartialOrd, + TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, + ] + .into_iter() + .collect::(), ), metadata, )) } 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(( TypeEntry::new_native( "::chrono::naive::NaiveDate", - &[TypeSpaceImpl::Display, TypeSpaceImpl::FromStr], + [ + TypespaceTrait::Clone, + TypespaceTrait::Debug, + TypespaceTrait::Serialize, + TypespaceTrait::Deserialize, + TypespaceTrait::Display, + TypespaceTrait::FromStr, + TypespaceTrait::Eq, + TypespaceTrait::PartialEq, + TypespaceTrait::Ord, + TypespaceTrait::PartialOrd, + TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, + ] + .into_iter() + .collect::(), ), metadata, )) } 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(( TypeEntry::new_native( "::chrono::DateTime<::chrono::offset::Utc>", - &[TypeSpaceImpl::Display, TypeSpaceImpl::FromStr], + [ + TypespaceTrait::Clone, + TypespaceTrait::Debug, + TypespaceTrait::Serialize, + TypespaceTrait::Deserialize, + TypespaceTrait::Display, + TypespaceTrait::FromStr, + TypespaceTrait::Eq, + TypespaceTrait::PartialEq, + TypespaceTrait::Ord, + TypespaceTrait::PartialOrd, + TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, + ] + .into_iter() + .collect::(), ), metadata, )) } + // ::std::net::IpAddr has no Default impl. Some("ip") => Ok(( TypeEntry::new_native( "::std::net::IpAddr", - &[TypeSpaceImpl::Display, TypeSpaceImpl::FromStr], + [ + TypespaceTrait::Clone, + TypespaceTrait::Debug, + TypespaceTrait::Serialize, + TypespaceTrait::Deserialize, + TypespaceTrait::Display, + TypespaceTrait::FromStr, + TypespaceTrait::Eq, + TypespaceTrait::PartialEq, + TypespaceTrait::Ord, + TypespaceTrait::PartialOrd, + TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, + ] + .into_iter() + .collect::(), ), metadata, )), + // ::std::net::Ipv4Addr has no Default impl. Some("ipv4") => Ok(( TypeEntry::new_native( "::std::net::Ipv4Addr", - &[TypeSpaceImpl::Display, TypeSpaceImpl::FromStr], + [ + TypespaceTrait::Clone, + TypespaceTrait::Debug, + TypespaceTrait::Serialize, + TypespaceTrait::Deserialize, + TypespaceTrait::Display, + TypespaceTrait::FromStr, + TypespaceTrait::Eq, + TypespaceTrait::PartialEq, + TypespaceTrait::Ord, + TypespaceTrait::PartialOrd, + TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, + ] + .into_iter() + .collect::(), ), metadata, )), + // ::std::net::Ipv6Addr has not Default impl. Some("ipv6") => Ok(( TypeEntry::new_native( "::std::net::Ipv6Addr", - &[TypeSpaceImpl::Display, TypeSpaceImpl::FromStr], + [ + TypespaceTrait::Clone, + TypespaceTrait::Debug, + TypespaceTrait::Serialize, + TypespaceTrait::Deserialize, + TypespaceTrait::Display, + TypespaceTrait::FromStr, + TypespaceTrait::Eq, + TypespaceTrait::PartialEq, + TypespaceTrait::Ord, + TypespaceTrait::PartialOrd, + TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, + ] + .into_iter() + .collect::(), ), metadata, )), @@ -871,7 +967,7 @@ impl TypeSpace { max_length: None, min_length: None, pattern: None, - }) => Ok((TypeEntryDetails::String.into(), metadata)), + }) => Ok((Type::String.into(), metadata)), Some(validation) => { if let Some(pattern) = &validation.pattern { @@ -880,10 +976,9 @@ impl TypeSpace { type_name: type_name.clone().into_option(), reason: format!("invalid pattern '{}' {}", pattern, e), })?; - self.uses_regress = true; } - let string = TypeEntryDetails::String.into(); + let string = TypeEntry::from(Type::String); let type_id = self.assign_type(string); Ok(( TypeEntryNewtype::from_metadata_with_string_validation( @@ -937,7 +1032,7 @@ impl TypeSpace { Some(Ok(Variant::new( variant_name.clone(), None, - VariantDetails::Simple, + VariantDetails::Unit, ))) } @@ -1207,7 +1302,7 @@ impl TypeSpace { &self, metadata: &'a Option>, ) -> Result<(TypeEntry, &'a Option>)> { - Ok((TypeEntryDetails::Unit.into(), metadata)) + Ok((Type::Unit.into(), metadata)) } /// Determine whether a schema's property name validation constraints can be handled @@ -1361,10 +1456,7 @@ impl TypeSpace { .ref_to_id .get(&key) .unwrap_or_else(|| panic!("$ref {} is missing", ref_name)); - Ok(( - TypeEntryDetails::Reference(type_id.clone()).into(), - metadata, - )) + Ok((TypeEntry::Reference(type_id.clone()), metadata)) } fn convert_all_of<'a>( @@ -1677,13 +1769,6 @@ impl TypeSpace { let (type_entry, _) = self.convert_schema_object(Name::Unknown, original_schema, &type_schema)?; - // Make sure all the values are valid. - // TODO this isn't strictly legal since we may not yet have - // resolved references. - enum_values - .iter() - .try_for_each(|value| type_entry.validate_value(self, value).map(|_| ()))?; - let type_id = self.assign_type(type_entry); let newtype_entry = TypeEntryNewtype::from_metadata_with_deny_values( @@ -1743,13 +1828,6 @@ impl TypeSpace { original_schema, &typed_schema, )?; - // Make sure all the values are valid. - // TODO this isn't strictly legal since we may not yet - // have resolved references. - enum_values.iter().try_for_each(|value| { - type_entry.validate_value(self, value).map(|_| ()) - })?; - let type_id = self.assign_type(type_entry); let newtype_entry = TypeEntryNewtype::from_metadata_with_deny_values( @@ -1808,7 +1886,7 @@ impl TypeSpace { }); let rest = (items.len()..*max_items as usize).map(|_| Ok(rest_id.clone())); let types = start.chain(rest).collect::>>()?; - Ok((TypeEntryDetails::Tuple(types).into(), metadata)) + Ok((Type::Tuple(types).into(), metadata)) } // Tuple with at least as many items as required. Some(SingleOrVec::Vec(items)) => { @@ -1821,26 +1899,20 @@ impl TypeSpace { Ok(self.id_for_schema(item_name, item_schema)?.0) }) .collect::>()?; - Ok((TypeEntryDetails::Tuple(types).into(), metadata)) + Ok((Type::Tuple(types).into(), metadata)) } // Array with a schema for the item. Some(SingleOrVec::Single(item_schema)) => { let item_id = self.id_for_schema(type_name.append("item"), item_schema)?.0; - Ok(( - TypeEntryDetails::Array(item_id, *max_items as usize).into(), - metadata, - )) + Ok((Type::Array(item_id, *max_items as usize).into(), metadata)) } // Array with no schema for the item. None => { let any_id = self .id_for_schema(type_name.append("item"), &Schema::Bool(true))? .0; - Ok(( - TypeEntryDetails::Array(any_id, *max_items as usize).into(), - metadata, - )) + Ok((Type::Array(any_id, *max_items as usize).into(), metadata)) } }, @@ -1861,8 +1933,8 @@ impl TypeSpace { // If items are unique, this is a Set; otherwise it's an Array. match unique_items { - Some(true) => Ok((TypeEntryDetails::Set(type_id).into(), metadata)), - _ => Ok((TypeEntryDetails::Vec(type_id).into(), metadata)), + Some(true) => Ok((Type::Set(type_id).into(), metadata)), + _ => Ok((Type::Vec(type_id).into(), metadata)), } } @@ -1875,13 +1947,12 @@ impl TypeSpace { unique_items, contains: None, } => { - self.uses_serde_json = true; - let type_id = self.assign_type(TypeEntryDetails::JsonValue.into()); + let type_id = self.assign_type(TypeEntry::from(Type::JsonValue)); // If items are unique, this is a Set; otherwise it's an Array. match unique_items { - Some(true) => Ok((TypeEntryDetails::Set(type_id).into(), metadata)), - _ => Ok((TypeEntryDetails::Vec(type_id).into(), metadata)), + Some(true) => Ok((Type::Set(type_id).into(), metadata)), + _ => Ok((Type::Vec(type_id).into(), metadata)), } } @@ -1896,9 +1967,8 @@ impl TypeSpace { &mut self, metadata: &'a Option>, ) -> Result<(TypeEntry, &'a Option>)> { - self.uses_serde_json = true; - let type_id = self.assign_type(TypeEntryDetails::JsonValue.into()); - Ok((TypeEntryDetails::Vec(type_id).into(), metadata)) + let type_id = self.assign_type(TypeEntry::from(Type::JsonValue)); + Ok((Type::Vec(type_id).into(), metadata)) } // TODO not sure if I want to deal with enum_values here, but we'll see... @@ -1913,8 +1983,7 @@ impl TypeSpace { &mut self, metadata: &'a Option>, ) -> Result<(TypeEntry, &'a Option>)> { - self.uses_serde_json = true; - Ok((TypeEntryDetails::JsonValue.into(), metadata)) + Ok((Type::JsonValue.into(), metadata)) } fn convert_never<'a>( @@ -1954,11 +2023,6 @@ impl TypeSpace { let (type_entry, metadata) = self.convert_schema_object(inner_type_name, original_schema, &type_schema)?; - // Make sure all the values are valid. - enum_values - .iter() - .try_for_each(|value| type_entry.validate_value(self, value).map(|_| ()))?; - let type_id = self.assign_type(type_entry); let newtype_entry = TypeEntryNewtype::from_metadata_with_enum_values( @@ -2131,7 +2195,11 @@ mod tests { &schema.schema, ) .unwrap(); - let output = ty.type_name(&type_space); + let type_id = type_space.assign_type(ty); + // name() borrows from the typespace now that it answers a Cow, + // so the typespace has to outlive the name. + let typespace = type_space.to_typespace().unwrap(); + let output = typespace.get_type(&type_id).name(); let actual = output.split("::").last().unwrap().trim(); let expected = type_name.split("::").last().unwrap(); assert_eq!(actual, expected); @@ -2161,37 +2229,6 @@ mod tests { int_test!(NonZeroU32); int_test!(NonZeroU64); - #[test] - fn test_redundant_types() { - #[derive(JsonSchema)] - #[allow(dead_code)] - struct Alphabet { - a: u32, - b: u32, - c: u32, - d: Option, - e: Option, - f: (u32, u32, u32, Option), - } - - let schema = schema_for!(Alphabet); - - let mut type_space = TypeSpace::default(); - type_space - .add_ref_types(schema.definitions.clone()) - .unwrap(); - let _ = type_space - .add_type_with_name(&schema.schema.into(), Some("Alphabet".to_string())) - .unwrap(); - - // We expect a total of 4 types: - // 1. u32 - // 2. option -> 1 - // 3. tuple -> 1, 1, 1, 2 - // 4. struct -> 1, 1, 1, 2, 2, 3 - assert_eq!(type_space.iter_types().count(), 4); - } - #[test] fn test_basic_option_flat() { #[derive(JsonSchema, Schema)] @@ -2296,7 +2333,7 @@ mod tests { let mut type_space = TypeSpace::default(); let _ = type_space.add_type(&schema.schema.into()).unwrap(); - let actual = type_space.to_stream(); + let actual = type_space.to_stream().unwrap(); let file = syn::parse2::(actual).expect("type space should emit a valid file"); match file.items.as_slice() { [] => {} @@ -2319,9 +2356,11 @@ mod tests { let mut type_space = TypeSpace::default(); let type_id = type_space.add_type(&schema.schema.into()).unwrap(); - match &type_space.id_to_entry[&type_id].details { - super::TypeEntryDetails::Enum(details) => assert!(details.variants.is_empty()), - details => panic!("empty anyOf should be unsatisfiable, got {details:?}"), + match &type_space.id_to_entry[&type_id] { + crate::type_entry::TypeEntry::Type(typespace::build::Type::Enum(details)) => { + assert!(details.get_variants().is_empty()) + } + entry => panic!("empty anyOf should be unsatisfiable, got {entry:?}"), } } @@ -2347,7 +2386,8 @@ mod tests { [TypeSpaceImpl::Display].into_iter(), )); let type_id = type_space.add_type(&schema.schema.into()).unwrap(); - let typ = type_space.get_type(&type_id).unwrap(); + let typespace = type_space.to_typespace().unwrap(); + let typ = typespace.get_type(&type_id); let actual = typ.ident(); let expected = quote! { not::a::real::library::Uuid }; diff --git a/typify-impl/src/cycles.rs b/typify-impl/src/cycles.rs deleted file mode 100644 index 0ebc6f76..00000000 --- a/typify-impl/src/cycles.rs +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2023 Oxide Computer Company - -use std::{ - collections::{BTreeMap, BTreeSet}, - ops::Range, -}; - -use crate::{ - type_entry::{ - TypeEntry, TypeEntryDetails, TypeEntryEnum, TypeEntryNewtype, TypeEntryStruct, - VariantDetails, - }, - TypeId, TypeSpace, -}; - -impl TypeSpace { - /// We need to root out any containment cycles, breaking them by inserting - /// a `Box` type. Our choice of *where* to break cycles is more arbitrary - /// than optimal, but is well beyond sufficient. - pub(crate) fn break_cycles(&mut self, range: Range) { - enum Node { - Start { - type_id: TypeId, - }, - Processing { - type_id: TypeId, - children_ids: Vec, - }, - } - - let mut visited = BTreeSet::::new(); - - for id in range { - let type_id = TypeId(id); - - // This isn't strictly necessary, but we'll short-circuit some work - // by checking this right away. - if visited.contains(&type_id) { - continue; - } - - let mut active = BTreeSet::::new(); - let mut stack = Vec::::new(); - - active.insert(type_id.clone()); - stack.push(Node::Start { type_id }); - - while let Some(top) = stack.last_mut() { - match top { - // Skip right to the end since we've already seen this type. - Node::Start { type_id } if visited.contains(type_id) => { - assert!(active.contains(type_id)); - - let type_id = type_id.clone(); - *top = Node::Processing { - type_id, - children_ids: Vec::new(), - }; - } - - // Break any immediate cycles and queue up this type for - // descent into its child types. - Node::Start { type_id } => { - assert!(active.contains(type_id)); - - visited.insert(type_id.clone()); - - // Determine which child types form cycles--and - // therefore need to be snipped--and the rest--into - // which we should descend. We make this its own block - // to clarify the lifetime of the exclusive reference - // to the type. We don't really *need* to have an - // exclusive reference here, but there's no point in - // writing `get_child_ids` again for shared references. - let (snip, descend) = { - let type_entry = self.id_to_entry.get_mut(type_id).unwrap(); - - let child_ids = get_child_ids(type_entry) - .into_iter() - .map(|child_id| child_id.clone()); - - // If the child type is in active then we've found - // a cycle (otherwise we'll descend). - child_ids.partition::, _>(|child_id| active.contains(child_id)) - }; - - // Note that while `snip` might contain duplicates, - // `id_to_box` is idempotent insofar as the same input - // TypeId will result in the same output TypeId. Ergo - // the resulting pairs from which we construct the - // mapping would contain exact duplicates; it would not - // contain two values associated with the same key. - let replace = snip - .into_iter() - .map(|type_id| { - let box_id = self.id_to_box(&type_id); - - (type_id, box_id) - }) - .collect::>(); - - // Break any cycles by reassigning the child type to a box. - let type_entry = self.id_to_entry.get_mut(type_id).unwrap(); - let child_ids = get_child_ids(type_entry); - for child_id in child_ids { - if let Some(replace_id) = replace.get(child_id) { - *child_id = replace_id.clone(); - } - } - - // Descend into child types. - let node = Node::Processing { - type_id: type_id.clone(), - children_ids: descend, - }; - *top = node; - } - - // If there are children left, push the next child onto the - // stack. If there are none left, pop this type. - Node::Processing { - type_id, - children_ids, - } => { - if let Some(type_id) = children_ids.pop() { - // Descend into the next child node. - active.insert(type_id.clone()); - stack.push(Node::Start { type_id }); - } else { - // All done; remove the item from the active list - // and stack. - active.remove(type_id); - let _ = stack.pop(); - } - } - } - } - } - } -} - -/// For types that could potentially participate in a cycle, return a list of -/// mutable references to the child types. -fn get_child_ids(type_entry: &mut TypeEntry) -> Vec<&mut TypeId> { - match &mut type_entry.details { - TypeEntryDetails::Enum(TypeEntryEnum { variants, .. }) => variants - .iter_mut() - .flat_map(|variant| match &mut variant.details { - VariantDetails::Simple => Vec::new(), - VariantDetails::Item(type_id) => vec![type_id], - VariantDetails::Tuple(type_ids) => type_ids.iter_mut().collect(), - VariantDetails::Struct(properties) => properties - .iter_mut() - .map(|prop| &mut prop.type_id) - .collect(), - }) - .collect::>(), - - TypeEntryDetails::Struct(TypeEntryStruct { properties, .. }) => properties - .iter_mut() - .map(|prop| &mut prop.type_id) - .collect(), - - TypeEntryDetails::Newtype(TypeEntryNewtype { type_id, .. }) => { - vec![type_id] - } - - // Unnamed types that can participate in containment cycles. - TypeEntryDetails::Option(type_id) => vec![type_id], - TypeEntryDetails::Array(type_id, _) => vec![type_id], - TypeEntryDetails::Tuple(type_ids) => type_ids.iter_mut().collect(), - - _ => Vec::new(), - } -} - -#[cfg(test)] -mod tests { - use schema::Schema; - use schemars::JsonSchema; - - use crate::test_util::validate_output; - - #[test] - fn test_trivial_cycle() { - #[derive(JsonSchema, Schema)] - #[allow(dead_code)] - struct A { - a: Box, - } - - validate_output::(); - } - - #[test] - fn test_optional_trivial_cycle() { - #[derive(JsonSchema, Schema)] - #[allow(dead_code)] - struct A { - a: Option>, - } - - validate_output::(); - } - - #[test] - fn test_enum_trivial_cycles() { - #[derive(JsonSchema, Schema)] - #[allow(dead_code)] - enum A { - Variant0(u64), - Variant1 { - a: u64, - b: Vec, - rop: Option>, - }, - Variant2 { - a: Box, - }, - Variant3(u64, Box), - Variant4(Option>, String), - } - - validate_output::(); - } - - #[test] - fn test_newtype_trivial_cycle() { - #[derive(JsonSchema, Schema)] - #[allow(dead_code)] - struct A(Box); - - validate_output::(); - } - - #[test] - fn test_abab_cycle() { - #[derive(JsonSchema, Schema)] - #[allow(dead_code)] - struct A(B); - - #[derive(JsonSchema, Schema)] - #[allow(dead_code)] - struct B(Box); - - validate_output::(); - } -} diff --git a/typify-impl/src/defaults.rs b/typify-impl/src/defaults.rs deleted file mode 100644 index 6a343575..00000000 --- a/typify-impl/src/defaults.rs +++ /dev/null @@ -1,1025 +0,0 @@ -// Copyright 2025 Oxide Computer Company - -use std::collections::BTreeMap; - -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; - -use crate::{ - convert::STD_NUM_NONZERO_PREFIX, - type_entry::{ - DefaultKind, EnumTagType, StructProperty, StructPropertyRename, StructPropertyState, - TypeEntry, TypeEntryDetails, TypeEntryEnum, TypeEntryNewtype, TypeEntryStruct, Variant, - VariantDetails, WrappedValue, - }, - util::{sanitize, Case}, - DefaultImpl, Error, Result, TypeId, TypeSpace, -}; - -// Implementations for "stock" default functions so we don't litter the -// namespace with many that are effectively identical. -impl From<&DefaultImpl> for TokenStream { - fn from(default: &DefaultImpl) -> Self { - match default { - DefaultImpl::Boolean => quote! { - pub(super) fn default_bool() -> bool { - V - } - }, - DefaultImpl::I64 => quote! { - pub(super) fn default_i64() -> T - where - T: ::std::convert::TryFrom, - >::Error: ::std::fmt::Debug, - { - T::try_from(V).unwrap() - } - }, - DefaultImpl::U64 => quote! { - pub(super) fn default_u64() -> T - where - T: ::std::convert::TryFrom, - >::Error: ::std::fmt::Debug, - { - T::try_from(V).unwrap() - } - }, - DefaultImpl::NZU64 => quote! { - pub(super) fn default_nzu64() -> T - where - T: ::std::convert::TryFrom<::std::num::NonZeroU64>, - >::Error: - ::std::fmt::Debug, - { - T::try_from(::std::num::NonZeroU64::try_from(V).unwrap()) - .unwrap() - } - }, - } - } -} - -impl TypeEntry { - pub(crate) fn check_defaults(&self, type_space: &mut TypeSpace) -> Result<()> { - // Check the "whole-type" default. - match &self.details { - TypeEntryDetails::Enum(TypeEntryEnum { - default: Some(WrappedValue(default)), - .. - }) - | TypeEntryDetails::Struct(TypeEntryStruct { - default: Some(WrappedValue(default)), - .. - }) - | TypeEntryDetails::Newtype(TypeEntryNewtype { - default: Some(WrappedValue(default)), - .. - }) => { - if let DefaultKind::Generic(default_fn) = - self.validate_value(type_space, default)? - { - type_space.defaults.insert(default_fn); - } - } - - _ => (), - } - - // Check default values for struct properties or those of struct-type - // enum variants. - match &self.details { - TypeEntryDetails::Struct(TypeEntryStruct { properties, .. }) => { - properties - .iter() - .try_for_each(|prop| Self::check_property_defaults(prop, type_space))?; - } - - TypeEntryDetails::Enum(TypeEntryEnum { variants, .. }) => { - variants.iter().try_for_each(|variant| { - if let VariantDetails::Struct(properties) = &variant.details { - properties - .iter() - .try_for_each(|prop| Self::check_property_defaults(prop, type_space)) - } else { - Ok(()) - } - })?; - } - - _ => (), - }; - - Ok(()) - } - - fn check_property_defaults( - property: &StructProperty, - type_space: &mut TypeSpace, - ) -> Result<()> { - if let StructProperty { - state: StructPropertyState::Default(WrappedValue(prop_default)), - type_id, - .. - } = property - { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - if let DefaultKind::Generic(default_fn) = - type_entry.validate_value(type_space, prop_default)? - { - type_space.defaults.insert(default_fn); - } - } - Ok(()) - } - - /// Check that the given [`Value`] is a valid instance of this type - /// - /// The return value indicates whether the default is the "intrinsic", - /// typical default for the given type, can be handled by generic function, - /// or requires a bespoke function to generate the value. This contains - /// additional validation logic compared with [`value()`] but is able to - /// skip the parts where we actually emit code. - /// - /// [`Value`]: serde_json::Value - pub(crate) fn validate_value( - &self, - type_space: &TypeSpace, - default: &serde_json::Value, - ) -> Result { - match &self.details { - TypeEntryDetails::Enum(TypeEntryEnum { - tag_type, variants, .. - }) => match tag_type { - EnumTagType::External => { - validate_default_for_external_enum(type_space, variants, default) - .ok_or_else(Error::invalid_value) - } - EnumTagType::Internal { tag } => { - validate_default_for_internal_enum(type_space, variants, default, tag) - .ok_or_else(Error::invalid_value) - } - EnumTagType::Adjacent { tag, content } => { - validate_default_for_adjacent_enum(type_space, variants, default, tag, content) - .ok_or_else(Error::invalid_value) - } - EnumTagType::Untagged => { - validate_default_for_untagged_enum(type_space, variants, default) - .ok_or_else(Error::invalid_value) - } - }, - TypeEntryDetails::Struct(TypeEntryStruct { properties, .. }) => { - validate_default_struct_props(properties, type_space, default) - .ok_or_else(Error::invalid_value) - } - - TypeEntryDetails::Newtype(TypeEntryNewtype { type_id, .. }) => { - // Validate the inner type, but irrespective of the result, - // we'll need a custom function to make a default of the outer - // newtype. - let _ = validate_type_id(type_id, type_space, default)?; - Ok(DefaultKind::Specific) - } - TypeEntryDetails::Option(type_id) => { - if let serde_json::Value::Null = default { - Ok(DefaultKind::Intrinsic) - } else { - // Make sure the default is valid for the sub-type. - let _ = validate_type_id(type_id, type_space, default)?; - Ok(DefaultKind::Specific) - } - } - TypeEntryDetails::Box(type_id) => validate_type_id(type_id, type_space, default), - - TypeEntryDetails::Vec(type_id) => { - if let serde_json::Value::Array(v) = default { - if v.is_empty() { - Ok(DefaultKind::Intrinsic) - } else { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - for value in v { - let _ = type_entry.validate_value(type_space, value)?; - } - Ok(DefaultKind::Specific) - } - } else { - Err(Error::invalid_value()) - } - } - TypeEntryDetails::Map(key_id, value_id) => { - if let serde_json::Value::Object(m) = default { - if m.is_empty() { - Ok(DefaultKind::Intrinsic) - } else { - let key_ty = type_space.id_to_entry.get(key_id).unwrap(); - let value_ty = type_space.id_to_entry.get(value_id).unwrap(); - for (key, value) in m { - let _ = key_ty.validate_value( - type_space, - &serde_json::Value::String(key.clone()), - )?; - let _ = value_ty.validate_value(type_space, value)?; - } - Ok(DefaultKind::Specific) - } - } else { - Err(Error::invalid_value()) - } - } - TypeEntryDetails::Set(type_id) => { - if let serde_json::Value::Array(v) = default { - if v.is_empty() { - Ok(DefaultKind::Intrinsic) - } else { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - for (i, value) in v.iter().enumerate() { - // Sets can't contain duplicates; also Value isn't - // Ord so O(n^2) it is! - for other in &v[(i + 1)..] { - if value == other { - return Err(Error::invalid_value()); - } - } - let _ = type_entry.validate_value(type_space, value)?; - } - Ok(DefaultKind::Specific) - } - } else { - Err(Error::invalid_value()) - } - } - TypeEntryDetails::Tuple(ids) => { - validate_default_tuple(ids, type_space, default).ok_or_else(Error::invalid_value) - } - - TypeEntryDetails::Array(type_id, length) => { - let Some(arr) = default.as_array() else { - return Err(Error::invalid_value()); - }; - if arr.len() != *length { - return Err(Error::invalid_value()); - } - - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - for value in arr { - let _ = type_entry.validate_value(type_space, value)?; - } - Ok(DefaultKind::Specific) - } - TypeEntryDetails::Unit => { - if let serde_json::Value::Null = default { - Ok(DefaultKind::Intrinsic) - } else { - Err(Error::invalid_value()) - } - } - TypeEntryDetails::Native(_) => { - // This is tricky. There's not a lot we can do--particularly if - // and when we start to consider arbitrary types as "built-in" - // (e.g. if schemars tags types with an extension to denote - // their rust type or if the user can supply a list of type - // names to treat as built-in). So we just do no checking and - // will fail an `unwrap()` in the code emitted by `value()` if - // this Value is not valid for this built-in type. - Ok(DefaultKind::Specific) - } - TypeEntryDetails::JsonValue => Ok(DefaultKind::Specific), - TypeEntryDetails::Boolean => match default { - serde_json::Value::Bool(false) => Ok(DefaultKind::Intrinsic), - serde_json::Value::Bool(true) => Ok(DefaultKind::Generic(DefaultImpl::Boolean)), - _ => Err(Error::invalid_value()), - }, - // Note that min and max values are handled already by the - // conversion routines since we have those close at hand. - TypeEntryDetails::Integer(itype) => match (default.as_u64(), default.as_i64()) { - (None, None) => Err(Error::invalid_value()), - (Some(0), _) => Ok(DefaultKind::Intrinsic), - (_, Some(0)) => unreachable!(), - (Some(_), _) => { - if itype.starts_with(STD_NUM_NONZERO_PREFIX) { - Ok(DefaultKind::Generic(DefaultImpl::NZU64)) - } else { - Ok(DefaultKind::Generic(DefaultImpl::U64)) - } - } - (_, Some(_)) => Ok(DefaultKind::Generic(DefaultImpl::I64)), - }, - TypeEntryDetails::Float(_) => { - if let Some(value) = default.as_f64() { - if value == 0.0 { - Ok(DefaultKind::Intrinsic) - } else { - Ok(DefaultKind::Generic(DefaultImpl::I64)) - } - } else { - Err(Error::invalid_value()) - } - } - TypeEntryDetails::String => { - if let Some("") = default.as_str() { - Ok(DefaultKind::Intrinsic) - } else { - Ok(DefaultKind::Specific) - } - } - - TypeEntryDetails::Reference(_) => unreachable!(), - } - } - - /// Return a string representing the function that can be called to produce - /// the value for the given default. If there is no such built-in function, - /// the .1 will be Some with a TokenStream for a function that can produce - /// that value. - pub(crate) fn default_fn( - &self, - default: &serde_json::Value, - type_space: &TypeSpace, - type_name: &str, - prop_name: &str, - ) -> (String, Option) { - let maybe_builtin = match &self.details { - // This can only be covered by the intrinsic default - TypeEntryDetails::Unit => unreachable!(), - TypeEntryDetails::Boolean => Some("defaults::default_bool::".to_string()), - TypeEntryDetails::Integer(name) => { - if let Some(value) = default.as_u64() { - if name.starts_with(STD_NUM_NONZERO_PREFIX) { - Some(format!("defaults::default_nzu64::<{}, {}>", name, value)) - } else { - Some(format!("defaults::default_u64::<{}, {}>", name, value)) - } - } else if let Some(value) = default.as_i64() { - Some(format!("defaults::default_i64::<{}, {}>", name, value)) - } else { - panic!() - } - } - _ => None, - }; - - if let Some(fn_name) = maybe_builtin { - (fn_name, None) - } else { - let n = self.type_ident(type_space, &Some("super".to_string())); - let value = self - .output_value(type_space, default, "e! { super:: }) - .unwrap_or_else(|| { - panic!( - "{}\nvalue: {}\ntype: {:#?}", - "The default value could not be rendered for this type", - serde_json::to_string_pretty(default).unwrap(), - self, - ) - }); - let fn_name = sanitize(&format!("{}_{}", type_name, prop_name), Case::Snake); - let fn_ident = format_ident!("{}", fn_name); - let def = quote! { - pub(super) fn #fn_ident() -> #n { - #value - } - }; - (format!("defaults::{}", fn_name), Some(def)) - } - } -} - -pub(crate) fn validate_default_for_external_enum( - type_space: &TypeSpace, - variants: &[Variant], - default: &serde_json::Value, -) -> Option { - if let Some(simple_name) = default.as_str() { - let variant = variants - .iter() - .find(|variant| simple_name == variant.raw_name)?; - matches!(&variant.details, VariantDetails::Simple).then(|| ())?; - - Some(DefaultKind::Specific) - } else { - let map = default.as_object()?; - if map.len() != 1 { - return None; - } - - let (name, value) = map.iter().next()?; - - let variant = variants.iter().find(|variant| name == &variant.raw_name)?; - - match &variant.details { - VariantDetails::Simple => None, - VariantDetails::Item(type_id) => validate_type_id(type_id, type_space, value).ok(), - VariantDetails::Tuple(tup) => validate_default_tuple(tup, type_space, value), - VariantDetails::Struct(props) => { - validate_default_struct_props(props, type_space, value) - } - } - } -} - -pub(crate) fn validate_default_for_internal_enum( - type_space: &TypeSpace, - variants: &[Variant], - default: &serde_json::Value, - tag: &str, -) -> Option { - let map = default.as_object()?; - let name = map.get(tag).and_then(serde_json::Value::as_str)?; - let variant = variants.iter().find(|variant| name == variant.raw_name)?; - - match &variant.details { - VariantDetails::Simple => Some(DefaultKind::Specific), - VariantDetails::Struct(props) => { - // Make an object without the tag. - let inner_default = serde_json::Value::Object( - map.clone() - .into_iter() - .filter(|(name, _)| name != tag) - .collect(), - ); - - validate_default_struct_props(props, type_space, &inner_default) - } - - VariantDetails::Item(_) | VariantDetails::Tuple(_) => unreachable!(), - } -} - -pub(crate) fn validate_default_for_adjacent_enum( - type_space: &TypeSpace, - variants: &[Variant], - default: &serde_json::Value, - tag: &str, - content: &str, -) -> Option { - let map = default.as_object()?; - - let (tag_value, content_value) = match ( - map.len(), - map.get(tag).and_then(serde_json::Value::as_str), - map.get(content), - ) { - (1, Some(tag_value), None) => (tag_value, None), - (2, Some(tag_value), content_value @ Some(_)) => (tag_value, content_value), - _ => return None, - }; - - let variant = variants - .iter() - .find(|variant| tag_value == variant.raw_name)?; - - match (&variant.details, content_value) { - (VariantDetails::Simple, None) => Some(DefaultKind::Specific), - (VariantDetails::Tuple(tup), Some(content_value)) => { - validate_default_tuple(tup, type_space, content_value) - } - (VariantDetails::Struct(props), Some(content_value)) => { - validate_default_struct_props(props, type_space, content_value) - } - _ => None, - } -} - -pub(crate) fn validate_default_for_untagged_enum( - type_space: &TypeSpace, - variants: &[Variant], - default: &serde_json::Value, -) -> Option { - variants.iter().find_map(|variant| { - // The name of the variant is not meaningful; we just need to see - // if any of the variants are valid with the given default. - match &variant.details { - VariantDetails::Simple => { - default.as_null()?; - Some(DefaultKind::Specific) - } - VariantDetails::Item(type_id) => validate_type_id(type_id, type_space, default).ok(), - VariantDetails::Tuple(tup) => validate_default_tuple(tup, type_space, default), - VariantDetails::Struct(props) => { - validate_default_struct_props(props, type_space, default) - } - } - }) -} - -fn validate_type_id( - type_id: &TypeId, - type_space: &TypeSpace, - default: &serde_json::Value, -) -> Result { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - type_entry.validate_value(type_space, default) -} - -fn validate_default_tuple( - types: &[TypeId], - type_space: &TypeSpace, - default: &serde_json::Value, -) -> Option { - let arr = default.as_array()?; - if arr.len() != types.len() { - return None; - } - - types - .iter() - .zip(arr.iter()) - .all(|(type_id, value)| validate_type_id(type_id, type_space, value).is_ok()) - .then_some(DefaultKind::Specific) -} - -fn validate_default_struct_props( - properties: &[StructProperty], - type_space: &TypeSpace, - default: &serde_json::Value, -) -> Option { - let map = default.as_object()?; - - // Gather up all properties including those of flattened struct properties: - // a tuple of (name: Option, type_id: TypeId, required: bool). We - // partition these into the named_properties which we then put into a map - // with the property name as the key, and unnamed_properties which consists - // of properties from flattened maps which have types but not names. - let (named_properties, unnamed_properties): (Vec<_>, Vec<_>) = properties - .iter() - .flat_map(|property| all_props(property, type_space)) - .partition(|(name, _, _)| name.is_some()); - - // These are the direct properties of this struct as well as the properties - // of any nested, flatted struct. - let named_properties = named_properties - .into_iter() - .map(|(name, type_id, required)| (name.unwrap(), (type_id, required))) - .collect::>(); - // These are just the types for any flattened map (either within this - // struct or nested within another flattened struct). - let unnamed_properties = unnamed_properties - .into_iter() - .map(|(_, type_id, _)| type_id) - .collect::>(); - - // Make sure that every value in the map validates properly. - map.iter().try_for_each(|(name, default_value)| { - // If there's a matching, named property, the value needs to validate. - // Otherwise it needs to validate against the schema of one of the - // unnamed properties i.e. it must be a valid value type for a nested, - // flatted map. - if let Some((type_id, _)) = named_properties.get(name) { - validate_type_id(type_id, type_space, default_value) - .ok() - .map(|_| ()) - } else { - unnamed_properties - .iter() - .any(|type_id| validate_type_id(type_id, type_space, default_value).is_ok()) - .then_some(()) - } - })?; - - // Make sure that every required field is present in the map. - named_properties - .iter() - .filter(|(_, (_, required))| *required) - .try_for_each(|(name, _)| map.get(*name).map(|_| ()))?; - - Some(DefaultKind::Specific) -} - -fn all_props<'a>( - property: &'a StructProperty, - type_space: &'a TypeSpace, -) -> Vec<(Option<&'a String>, &'a TypeId, bool)> { - let maybe_name = match &property.rename { - StructPropertyRename::None => Some(&property.name), - StructPropertyRename::Rename(rename) => Some(rename), - StructPropertyRename::Flatten => None, - }; - - if let Some(name) = maybe_name { - let required = match &property.state { - StructPropertyState::Required => true, - StructPropertyState::Optional | StructPropertyState::Default(_) => false, - }; - - vec![(Some(name), &property.type_id, required)] - } else { - // The type must be a struct, an option for a struct, or a map. - let type_entry = type_space.id_to_entry.get(&property.type_id).unwrap(); - - let (properties, all_required) = match &type_entry.details { - TypeEntryDetails::Struct(TypeEntryStruct { properties, .. }) => { - let optional = matches!(&property.state, StructPropertyState::Optional); - (properties, !optional) - } - TypeEntryDetails::Option(type_id) => { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - if let TypeEntryDetails::Struct(TypeEntryStruct { properties, .. }) = - &type_entry.details - { - (properties, false) - } else { - unreachable!() - } - } - - // TODO Rather than an option, this should probably be something - // that lets us say "explicit name" or "type to validate against" - TypeEntryDetails::Map(_, value_id) => return vec![(None, value_id, false)], - _ => unreachable!(), - }; - - properties - .iter() - .flat_map(|property| all_props(property, type_space)) - .map(|(name, type_id, required)| (name, type_id, required && all_required)) - .collect() - } -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use schemars::JsonSchema; - use serde_json::json; - use uuid::Uuid; - - use crate::{ - test_util::get_type, - type_entry::{DefaultKind, TypeEntry}, - DefaultImpl, - }; - - #[test] - fn test_default_option() { - let (type_space, type_id) = get_type::>(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(type_entry - .validate_value(&type_space, &json!("forty-two")) - .is_err()); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(null)), - Ok(DefaultKind::Intrinsic) - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(42)), - Ok(DefaultKind::Specific) - )); - } - - #[test] - fn test_default_box() { - let (type_space, type_id) = get_type::>(); - - let type_entry = TypeEntry { - details: crate::type_entry::TypeEntryDetails::Box(type_id), - extra_derives: Default::default(), - extra_attrs: Default::default(), - }; - - assert!(type_entry - .validate_value(&type_space, &json!("forty-two")) - .is_err()); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(null)), - Ok(DefaultKind::Intrinsic) - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(42)), - Ok(DefaultKind::Specific) - )); - } - - #[test] - fn test_default_array() { - let (type_space, type_id) = get_type::>(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(type_entry - .validate_value(&type_space, &json!([null])) - .is_err()); - assert!(matches!( - type_entry.validate_value(&type_space, &json!([])), - Ok(DefaultKind::Intrinsic), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!([1, 2, 5])), - Ok(DefaultKind::Specific), - )); - } - - #[test] - fn test_default_map() { - let (type_space, type_id) = get_type::>(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(type_entry.validate_value(&type_space, &json!([])).is_err()); - assert!(matches!( - type_entry.validate_value(&type_space, &json!({})), - Ok(DefaultKind::Intrinsic), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!({"a": 1, "b": 2})), - Ok(DefaultKind::Specific), - )); - } - - #[test] - fn test_default_tuple() { - let (type_space, type_id) = get_type::<(u32, u32, String)>(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(type_entry - .validate_value(&type_space, &json!([1, 2, "three", 4])) - .is_err()); - assert!(matches!( - type_entry.validate_value(&type_space, &json!([1, 2, "three"])), - Ok(DefaultKind::Specific), - )); - } - - #[test] - fn test_default_builtin() { - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(matches!( - type_entry.validate_value(&type_space, &json!("not-a-uuid")), - Ok(DefaultKind::Specific) - )); - } - - #[test] - fn test_default_bool() { - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(matches!( - type_entry.validate_value(&type_space, &json!(false)), - Ok(DefaultKind::Intrinsic), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(true)), - Ok(DefaultKind::Generic(DefaultImpl::Boolean)), - )); - } - - #[test] - fn test_default_numbers_and_string() { - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(type_entry - .validate_value(&type_space, &json!(true)) - .is_err()); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(0)), - Ok(DefaultKind::Intrinsic), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(42)), - Ok(DefaultKind::Generic(DefaultImpl::U64)), - )); - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(matches!( - type_entry.validate_value(&type_space, &json!("")), - Ok(DefaultKind::Intrinsic), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!("howdy")), - Ok(DefaultKind::Specific), - )); - } - - #[test] - fn test_struct_simple() { - #[derive(JsonSchema)] - #[allow(dead_code)] - struct Test { - a: String, - b: u32, - c: Option, - d: Option, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!( - { - "a": "aaaa", - "b": 7, - "c": "cccc" - } - ) - ), - Ok(DefaultKind::Specific), - )); - assert!(type_entry - .validate_value( - &type_space, - &json!( - { - "a": "aaaa", - "c": "cccc", - "d": 7 - } - ) - ) - .is_err()); - assert!(type_entry - .validate_value( - &type_space, - &json!( - { - "a": "aaaa", - "b": 7, - "d": {} - } - ) - ) - .is_err()); - } - - #[test] - fn test_enum_external() { - #[derive(JsonSchema)] - #[allow(dead_code)] - enum Test { - A, - B(String, String), - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(matches!( - type_entry.validate_value(&type_space, &json!("A")), - Ok(DefaultKind::Specific), - )); - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "B": ["xx", "yy"] - }) - ), - Ok(DefaultKind::Specific), - )); - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "C": { "cc": "xx", "dd": "yy" } - }) - ), - Ok(DefaultKind::Specific), - )); - assert!(type_entry - .validate_value(&type_space, &json!({ "A": null })) - .is_err()); - assert!(type_entry.validate_value(&type_space, &json!("B")).is_err()); - } - - #[test] - fn test_enum_internal() { - #[derive(JsonSchema)] - #[allow(dead_code)] - #[serde(tag = "tag")] - enum Test { - A, - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "tag": "A" - }) - ), - Ok(DefaultKind::Specific), - )); - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "tag": "C", - "cc": "xx", - "dd": "yy" - }) - ), - Ok(DefaultKind::Specific), - )); - assert!(type_entry - .validate_value( - &type_space, - &json!({ - "not-tag": "A" - }) - ) - .is_err()); - assert!(type_entry - .validate_value( - &type_space, - &json!({ - "tag": "B", - "cc": "where's D?" - }) - ) - .is_err()); - } - - #[test] - fn test_enum_adjacent() { - #[derive(JsonSchema)] - #[allow(dead_code)] - #[serde(tag = "tag", content = "content")] - enum Test { - A, - B(String, String), - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "tag": "A" - }) - ), - Ok(DefaultKind::Specific), - )); - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "tag": "B", - "content": ["xx", "yy"] - }) - ), - Ok(DefaultKind::Specific), - )); - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "tag": "C", - "content": { "cc": "xx", "dd": "yy" } - }) - ), - Ok(DefaultKind::Specific), - )); - assert!(type_entry.validate_value(&type_space, &json!("A")).is_err()); - assert!(type_entry - .validate_value( - &type_space, - &json!({ - "tag": "A", - "content": null, - }) - ) - .is_err()); - } - #[test] - fn test_enum_untagged() { - #[derive(JsonSchema)] - #[allow(dead_code)] - #[serde(untagged)] - enum Test { - A, - B(String, String), - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert!(matches!( - type_entry.validate_value(&type_space, &json!(null)), - Ok(DefaultKind::Specific), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(["xx", "yy"])), - Ok(DefaultKind::Specific), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!( { "cc": "xx", "dd": "yy" })), - Ok(DefaultKind::Specific), - )); - assert!(type_entry.validate_value(&type_space, &json!({})).is_err()); - } -} diff --git a/typify-impl/src/enums.rs b/typify-impl/src/enums.rs index 43bbbc0f..745d8a9f 100644 --- a/typify-impl/src/enums.rs +++ b/typify-impl/src/enums.rs @@ -1,21 +1,14 @@ -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use std::collections::{BTreeMap, BTreeSet, HashSet}; use heck::{ToKebabCase, ToPascalCase}; -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; use schemars::schema::{ InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec, }; use crate::{ - output::OutputSpace, - structs::generate_serde_attr, - type_entry::{ - EnumTagType, TypeEntry, TypeEntryDetails, TypeEntryEnum, TypeEntryStruct, Variant, - VariantDetails, - }, + type_entry::{TypeEntry, TypeEntryEnum, Variant}, util::{ constant_string_value, get_object, get_type_name, metadata_description, metadata_title_and_description, schema_is_named, @@ -23,6 +16,8 @@ use crate::{ Name, Result, TypeSpace, }; +use typespace::build::{EnumTagType, Type, VariantDetails}; + impl TypeSpace { pub(crate) fn maybe_option( &mut self, @@ -209,7 +204,7 @@ impl TypeSpace { } => Some(Variant::new( variant_name.to_string(), description, - VariantDetails::Simple, + VariantDetails::Unit, )), ProtoVariant::Typed { @@ -246,39 +241,23 @@ impl TypeSpace { &mut self, prop_type_name: Name, variant_schema: &Schema, - ) -> Result<(VariantDetails, bool)> { + ) -> Result<(VariantDetails, bool)> { let (ty, _) = self.convert_schema(prop_type_name, variant_schema)?; match ty { - TypeEntry { - details: TypeEntryDetails::Tuple(types), - .. - } => { + TypeEntry::Type(Type::Tuple(types)) => { let details = VariantDetails::Tuple(types); Ok((details, false)) } - TypeEntry { - details: TypeEntryDetails::Unit, - .. - } => { - let details = VariantDetails::Simple; + TypeEntry::Type(Type::Unit) => { + let details = VariantDetails::Unit; Ok((details, false)) } - TypeEntry { - details: - TypeEntryDetails::Struct(TypeEntryStruct { - name: _, - rename: _, - description: _, - default: _, // TODO arguably we should look at this - properties, - deny_unknown_fields, - schema: _, - }), - .. - } => { - let details = VariantDetails::Struct(properties); - Ok((details, deny_unknown_fields)) + TypeEntry::Type(Type::Struct(type_struct)) => { + // The struct's name and description are discarded; the + // variant supplies its own. + let details = VariantDetails::Struct(type_struct.get_properties().to_vec()); + Ok((details, type_struct.get_deny_unknown_fields())) } ty => { @@ -407,7 +386,7 @@ impl TypeSpace { Ok(Variant::new( variant_name.to_string(), None, - VariantDetails::Simple, + VariantDetails::Unit, )) } else { let tag_schema = validation.properties.get(tag).unwrap(); @@ -531,7 +510,7 @@ impl TypeSpace { assert_eq!(tag_name, tag); assert_eq!(validation.required.len(), 1); - let variant = Variant::new(variant_name.to_string(), None, VariantDetails::Simple); + let variant = Variant::new(variant_name.to_string(), None, VariantDetails::Unit); Ok((variant, false)) } else { let tag_schema = validation.properties.get(tag).unwrap(); @@ -747,109 +726,10 @@ fn get_common_prefix(name: &str, prefix: &str) -> String { .to_pascal_case() } -pub(crate) fn output_variant( - variant: &Variant, - type_space: &TypeSpace, - output: &mut OutputSpace, - type_name: &str, -) -> TokenStream { - let ident_name = variant.ident_name.as_ref().unwrap(); - let variant_name = format_ident!("{}", ident_name); - let doc = variant.description.as_ref().map(|s| { - quote! { #[doc = #s] } - }); - let serde = (&variant.raw_name != ident_name).then(|| { - let s = &variant.raw_name; - quote! { #[serde(rename = #s)] } - }); - match &variant.details { - VariantDetails::Simple => quote! { - #doc - #serde - #variant_name, - }, - VariantDetails::Item(type_id) => { - let item_type_ident = type_space - .id_to_entry - .get(type_id) - .unwrap() - .type_ident(type_space, &None); - - quote! { - #doc - #serde - #variant_name(#item_type_ident), - } - } - - VariantDetails::Tuple(tuple) => { - let types = tuple.iter().map(|type_id| { - type_space - .id_to_entry - .get(type_id) - .unwrap() - .type_ident(type_space, &None) - }); - - if tuple.len() != 1 { - quote! { - #doc - #serde - #variant_name(#(#types),*), - } - } else { - // A tuple variant with a single element requires special - // handling lest its "tuple-ness" be lost. This is important to - // ensure correct serialization and deserialization behavior. - // Note in particular the extra parentheses and trailing comma. - quote! { - #doc - #serde - #variant_name((#(#types,)*)), - } - } - } - - VariantDetails::Struct(props) => { - let prop_streams = props.iter().map(|prop| { - let prop_doc = prop.description.as_ref().map(|s| quote! { #[doc = #s] }); - - let prop_type_entry = type_space.id_to_entry.get(&prop.type_id).unwrap(); - let (prop_serde, _) = generate_serde_attr( - &format!("{}{}", type_name, variant.ident_name.as_ref().unwrap()), - &prop.name, - &prop.rename, - &prop.state, - prop_type_entry, - type_space, - output, - ); - - let prop_name = format_ident!("{}", prop.name); - let prop_type = prop_type_entry.type_ident(type_space, &None); - - quote! { - #prop_doc - #prop_serde - #prop_name: #prop_type, - } - }); - quote! { - #doc - #serde - #variant_name { - #(#prop_streams)* - }, - } - } - } -} - #[cfg(test)] mod tests { use std::collections::HashSet; - use quote::quote; use schema::Schema; use schemars::{ schema::{InstanceType, RootSchema, SchemaObject, SingleOrVec}, @@ -858,12 +738,15 @@ mod tests { use serde::Serialize; use crate::{ - output::OutputSpace, - test_util::{validate_output, validate_output_for_untagged_enm}, - type_entry::{EnumTagType, TypeEntryEnum, Variant, VariantDetails}, - Name, RefKey, TypeEntryDetails, TypeId, TypeSpace, TypeSpaceSettings, + test_util::{render_items_for, validate_output, validate_output_for_untagged_enm}, + type_entry::TypeEntry, + Name, RefKey, TypeId, TypeSpace, TypeSpaceSettings, }; + use quote::quote; + use typespace::build::{EnumTagType, Type, VariantDetails}; + use typespace::settings::ForeignTrait; + #[allow(dead_code)] #[derive(Serialize, JsonSchema, Schema)] #[serde(deny_unknown_fields)] @@ -1045,55 +928,33 @@ mod tests { ) .unwrap(); - match &ty.details { - TypeEntryDetails::Enum(TypeEntryEnum { - name, - rename: None, - description: None, - default: None, - tag_type: EnumTagType::Untagged, - variants, - deny_unknown_fields: _, - bespoke_impls: _, - schema: _, - }) => { - assert_eq!(name, "UntaggedEnum"); + match &ty { + TypeEntry::Type(Type::Enum(type_enum)) + if type_enum.get_tag_type() == Some(&EnumTagType::Untagged) => + { + let variants = type_enum.get_variants(); + assert_eq!(type_enum.get_name(), Some("UntaggedEnum")); assert_eq!(variants.len(), 5); assert!(matches!( - variants.first().unwrap(), - Variant { - details: VariantDetails::Simple, - .. - } + variants.first().unwrap().details(), + VariantDetails::Unit )); assert!(matches!( - variants.get(1).unwrap(), - Variant { - details: VariantDetails::Struct(_), - .. - } + variants.get(1).unwrap().details(), + VariantDetails::Struct(_) )); assert!(matches!( - variants.get(2).unwrap(), - Variant { - details: VariantDetails::Item(_), - .. - } + variants.get(2).unwrap().details(), + VariantDetails::Item(_) )); assert!(matches!( - variants.get(3).unwrap(), - Variant { - details: VariantDetails::Tuple(tup), - .. - } if tup.len() == 2 + variants.get(3).unwrap().details(), + VariantDetails::Tuple(tup) if tup.len() == 2 )); assert!(matches!( - variants.get(4).unwrap(), - Variant { - details: VariantDetails::Tuple(tup), - .. - } if tup.len() == 3 + variants.get(4).unwrap().details(), + VariantDetails::Tuple(tup) if tup.len() == 3 )); } @@ -1124,13 +985,9 @@ mod tests { // This confirms in particular that the tag type is untagged and // therefore that the other enum tagging regimes did not match. assert!(matches!( - &ty.details, - TypeEntryDetails::Enum(TypeEntryEnum { - rename: None, - description: None, - tag_type: EnumTagType::Untagged, - .. - }) + &ty, + TypeEntry::Type(Type::Enum(type_enum)) + if type_enum.get_tag_type() == Some(&EnumTagType::Untagged) )); } @@ -1218,48 +1075,22 @@ mod tests { ) .unwrap(); - if let TypeEntryDetails::Enum(TypeEntryEnum { - variants, - tag_type, - deny_unknown_fields, - .. - }) = &type_entry.details - { + if let TypeEntry::Type(Type::Enum(type_enum)) = &type_entry { + let variants = type_enum.get_variants(); + let tag_type = type_enum.get_tag_type().unwrap(); + let deny_unknown_fields = type_enum.get_deny_unknown_fields(); let variant_names = variants .iter() - .map(|variant| variant.ident_name.as_ref().unwrap().clone()) + .map(|variant| variant.rust_name().to_string()) .collect::>(); assert_eq!(variant_names.len(), variants.len()); assert_eq!(tag_type, &EnumTagType::Untagged); - assert_eq!(deny_unknown_fields, &true); + assert!(deny_unknown_fields); } else { panic!(); } } - #[test] - fn test_maybe_option() { - let subschemas = vec![ - SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), - ..Default::default() - } - .into(), - SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))), - ..Default::default() - } - .into(), - ]; - - let mut type_space = TypeSpace::default(); - let type_entry = type_space - .maybe_option(Name::Unknown, &None, &subschemas) - .unwrap(); - - assert_eq!(type_entry.details, TypeEntryDetails::Option(TypeId(1))) - } - #[test] fn test_simple_untagged_enum() { let schema_json = r##" @@ -1332,23 +1163,15 @@ mod tests { .unwrap(); let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - match &type_entry.details { - TypeEntryDetails::Enum(TypeEntryEnum { - tag_type, - variants, - deny_unknown_fields: _, - .. - }) => { - assert_eq!(tag_type, &EnumTagType::Untagged); - //assert_eq!(deny_unknown_fields, &true); - for variant in variants { - match &variant.details { + match type_entry.as_type() { + Type::Enum(type_enum) => { + assert_eq!(type_enum.get_tag_type(), Some(&EnumTagType::Untagged)); + //assert_eq!(type_enum.get_deny_unknown_fields(), true); + for variant in type_enum.get_variants() { + match variant.details() { VariantDetails::Item(item) => { let variant_type = type_space.id_to_entry.get(item).unwrap(); - assert!(variant_type - .name() - .unwrap() - .ends_with(variant.ident_name.as_ref().unwrap())); + assert!(variant_type.name().unwrap().ends_with(variant.rust_name())); } _ => panic!("{:#?}", type_entry), } @@ -1434,11 +1257,8 @@ mod tests { .unwrap(); let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - match &type_entry.details { - TypeEntryDetails::Enum(TypeEntryEnum { - tag_type: EnumTagType::Untagged, - .. - }) => {} + match type_entry.as_type() { + Type::Enum(type_enum) if type_enum.get_tag_type() == Some(&EnumTagType::Untagged) => {} _ => panic!("{:#?}", type_entry), } } @@ -1471,9 +1291,15 @@ mod tests { &subschemas, ) .unwrap(); - let mut output = OutputSpace::default(); - type_entry.output(&type_space, &mut output); - let actual = output.into_stream(); + let _ = type_space.assign_type(type_entry); + let actual = render_items_for(&type_space, "ResultX"); + + // This is typify1's output for this type. Differences from + // typespace's rendering are gaps: the derive list, the doc + // rendering (one #[doc] vs. line-by-line), and the convenience + // From impl. + let schema_json = serde_json::to_string_pretty(&original_schema).unwrap(); + let schema_lines = schema_json.lines(); let expected = quote! { #[doc = "`ResultX`"] #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] @@ -1488,18 +1314,17 @@ mod tests { } } }; - assert_eq!(actual.to_string(), expected.to_string()); + assert_eq!(actual, expected.to_string()); } #[test] fn test_result_derives() { - let mut type_space = TypeSpace::new( - TypeSpaceSettings::default() - .with_derive("A".to_string()) - .with_derive("B".to_string()) - .with_derive("C".to_string()) - .with_derive("D".to_string()), - ); + let mut type_space = + TypeSpace::new(TypeSpaceSettings::default().map_typespace_settings(|s| { + ["::a::A", "::b::B", "::c::C", "::d::D"] + .into_iter() + .fold(s, |s, path| s.with_derive(ForeignTrait::new(path).unwrap())) + })); let schema = schema_for!(Result); let subschemas = schema.schema.subschemas.unwrap().one_of.unwrap(); let type_entry = type_space @@ -1510,12 +1335,12 @@ mod tests { &subschemas, ) .unwrap(); - let mut output = OutputSpace::default(); - type_entry.output(&type_space, &mut output); - let actual = output.into_stream(); + let _ = type_space.assign_type(type_entry); + let actual = render_items_for(&type_space, "ResultX"); + let expected = quote! { #[doc = "`ResultX`"] - #[derive(::serde::Deserialize, ::serde::Serialize, A, B, C, Clone, D, Debug)] + #[derive(::a::A, ::b::B, ::c::C, ::d::D, ::serde::Deserialize, ::serde::Serialize, Clone, Debug)] pub enum ResultX { Ok(u32), Err(::std::string::String), @@ -1527,7 +1352,7 @@ mod tests { } } }; - assert_eq!(actual.to_string(), expected.to_string()); + assert_eq!(actual, expected.to_string()); } #[test] @@ -1541,4 +1366,27 @@ mod tests { validate_output::(); } + + #[test] + fn test_maybe_option() { + let subschemas = vec![ + SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), + ..Default::default() + } + .into(), + SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))), + ..Default::default() + } + .into(), + ]; + + let mut type_space = TypeSpace::default(); + let type_entry = type_space + .maybe_option(Name::Unknown, &None, &subschemas) + .unwrap(); + + assert!(matches!(type_entry.as_type(), Type::Option(TypeId(1)))); + } } diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index ee1a2615..12a4d5b0 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -1,53 +1,51 @@ -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! typify backend implementation. #![deny(missing_docs)] -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; use conversions::SchemaCache; -use log::{debug, info}; -use output::OutputSpace; +use log::info; use proc_macro2::TokenStream; -use quote::{format_ident, quote, ToTokens}; use schemars::schema::{Metadata, RootSchema, Schema}; use thiserror::Error; -use type_entry::{ - StructPropertyState, TypeEntry, TypeEntryDetails, TypeEntryNative, TypeEntryNewtype, - WrappedValue, -}; +use type_entry::{TypeEntry, TypeEntryNewtype}; use crate::util::{sanitize, Case}; pub use crate::util::accept_as_ident; +/// The typespace crate, re-exported for consumers of this one. +/// +/// [`TypeSpace::to_typespace`] yields a [`typespace::Typespace`]; +/// its view API answers type queries (identifiers, structure, trait +/// impls) that used to live on this crate's own wrapper types. +pub use ::typespace; + #[cfg(test)] mod test_util; mod conversions; mod convert; -mod cycles; -mod defaults; mod enums; mod merge; -mod output; mod rust_extension; mod structs; mod type_entry; mod util; mod validate; -mod value; #[allow(missing_docs)] #[derive(Error, Debug)] pub enum Error { #[error("unexpected value type")] BadValue(String, serde_json::Value), - #[error("invalid TypeId")] - InvalidTypeId, #[error("value does not conform to the given schema")] InvalidValue, + #[error(transparent)] + Typespace(#[from] typespace::error::Error), #[error("invalid schema for {}: {reason}", show_type_name(.type_name.as_deref()))] InvalidSchema { type_name: Option, @@ -55,12 +53,6 @@ pub enum Error { }, } -impl Error { - fn invalid_value() -> Self { - Self::InvalidValue - } -} - #[allow(missing_docs)] pub type Result = std::result::Result; @@ -68,84 +60,19 @@ fn show_type_name(type_name: Option<&str>) -> &str { type_name.unwrap_or("") } -/// Representation of a type which may have a definition or may be built-in. -#[derive(Debug)] -pub struct Type<'a> { - type_space: &'a TypeSpace, - type_entry: &'a TypeEntry, -} - -#[allow(missing_docs)] -/// Type details returned by Type::details() to inspect a type. -pub enum TypeDetails<'a> { - Enum(TypeEnum<'a>), - Struct(TypeStruct<'a>), - Newtype(TypeNewtype<'a>), - - Option(TypeId), - Vec(TypeId), - Map(TypeId, TypeId), - Set(TypeId), - Box(TypeId), - Tuple(Box + 'a>), - Array(TypeId, usize), - Builtin(&'a str), - - Unit, - String, -} - -/// Enum type details. -pub struct TypeEnum<'a> { - details: &'a type_entry::TypeEntryEnum, -} - -/// Enum variant details. -pub enum TypeEnumVariant<'a> { - /// Variant with no associated data. - Simple, - /// Tuple-type variant with at least one associated type. - Tuple(Vec), - /// Struct-type variant with named properties and types. - Struct(Vec<(&'a str, TypeId)>), -} - -/// Full information pertaining to an enum variant. -pub struct TypeEnumVariantInfo<'a> { - /// Name. - pub name: &'a str, - /// Description. - pub description: Option<&'a str>, - /// Details for the enum variant. - pub details: TypeEnumVariant<'a>, -} - -/// Struct type details. -pub struct TypeStruct<'a> { - details: &'a type_entry::TypeEntryStruct, -} - -/// Full information pertaining to a struct property. -pub struct TypeStructPropInfo<'a> { - /// Name. - pub name: &'a str, - /// Description. - pub description: Option<&'a str>, - /// Whether the propertty is required. - pub required: bool, - /// Identifies the schema for the property. - pub type_id: TypeId, -} - -/// Newtype details. -pub struct TypeNewtype<'a> { - details: &'a type_entry::TypeEntryNewtype, -} - /// Type identifier returned from type creation and used to lookup types. #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone, Hash)] pub struct TypeId(u64); +// typespace requires its Id type to implement Display (for error +// reporting); typify's TypeId is an opaque integer, so this shows the +// number. +impl std::fmt::Display for TypeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + #[derive(Debug, Clone, PartialEq)] pub(crate) enum Name { Required(String), @@ -177,7 +104,13 @@ pub(crate) enum RefKey { Def(String), } -/// A collection of types. +/// A collection of types under conversion from JSON Schema. +/// +/// Add schemas with the `add_*` methods. Querying and rendering are +/// typespace's job: [`TypeSpace::to_typespace`] finalizes the +/// collected types into a [`typespace::Typespace`] whose view API +/// answers identifier, structure, and trait-impl queries; +/// [`TypeSpace::to_stream`] renders everything to code. #[derive(Debug)] pub struct TypeSpace { next_id: u64, @@ -189,22 +122,20 @@ pub struct TypeSpace { definitions: BTreeMap, id_to_entry: BTreeMap, - type_to_id: BTreeMap, 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, cache: SchemaCache, - - // Shared functions for generating default values - defaults: BTreeSet, } impl Default for TypeSpace { @@ -213,139 +144,100 @@ impl Default for TypeSpace { next_id: 1, definitions: Default::default(), id_to_entry: Default::default(), - type_to_id: 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(), - defaults: Default::default(), - } - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum DefaultImpl { - Boolean, - I64, - U64, - NZU64, -} - -impl DefaultImpl { - /// The name of the shared function this renders as. - fn fn_name(&self) -> &'static str { - match self { - DefaultImpl::Boolean => "default_bool", - DefaultImpl::I64 => "default_i64", - DefaultImpl::U64 => "default_u64", - DefaultImpl::NZU64 => "default_nzu64", } } } -/// Type name to use in generated code. -#[derive(Clone)] -pub struct MapType(pub syn::Type); - -impl MapType { - /// Create a new MapType from a [`str`]. - /// - /// # Panics - /// - /// Panics if `s` cannot be parsed as a Rust type. Prefer - /// [`str::parse`] (via the [`FromStr`](std::str::FromStr) - /// implementation) to handle invalid input without panicking. - pub fn new(s: &str) -> Self { - let map_type = syn::parse_str::(s).expect("valid ident"); - Self(map_type) - } -} +/// Settings that alter type generation. +#[derive(Debug, Clone)] +pub struct TypeSpaceSettings { + typespace: typespace::settings::Settings, -impl std::str::FromStr for MapType { - type Err = String; + unknown_crates: UnknownPolicy, + crates: BTreeMap, - fn from_str(s: &str) -> std::result::Result { - let map_type = syn::parse_str::(s) - .map_err(|err| format!("invalid map type {s:?}: {err}"))?; - Ok(Self(map_type)) - } + patch: BTreeMap, + replace: BTreeMap, + convert: Vec, } -impl Default for MapType { +impl Default for TypeSpaceSettings { fn default() -> Self { - Self::new("::std::collections::HashMap") - } -} - -impl std::fmt::Debug for MapType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "MapType({})", self.0.to_token_stream()) + Self::new() } } -impl std::fmt::Display for MapType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.to_token_stream().fmt(f) +impl TypeSpaceSettings { + /// typify's defaults: types render with the typespace settings typify + /// starts from (every type must serialize, deserialize, clone, and + /// debug-print; comparison, hashing, string conversion, `Copy`, and + /// `Default` are taken wherever the type can support them; maps + /// render as `::std::collections::HashMap`; typespace's typify + /// compatibility mode is on), with no replacements, patches, or + /// conversions and the default policy for external crates. + pub fn new() -> Self { + Self { + typespace: baseline_typespace_settings(), + unknown_crates: Default::default(), + crates: Default::default(), + patch: Default::default(), + replace: Default::default(), + convert: Default::default(), + } } -} -impl<'de> serde::Deserialize<'de> for MapType { - fn deserialize(deserializer: D) -> std::result::Result + /// Adjust the typespace settings generated code is rendered with. + /// + /// `f` receives the settings as they stand and returns the settings + /// to use: extra derives and attributes, a struct builder, a + /// different map type, and anything else typespace offers. + pub fn map_typespace_settings(&mut self, f: F) -> &mut Self where - D: serde::Deserializer<'de>, + F: FnOnce(typespace::settings::Settings) -> typespace::settings::Settings, { - let s = String::deserialize(deserializer)?; - s.parse().map_err(serde::de::Error::custom) - } -} - -impl From for MapType { - /// # Panics - /// - /// Panics if `s` cannot be parsed as a Rust type. Prefer - /// [`str::parse`] (via the [`FromStr`](std::str::FromStr) - /// implementation) to handle invalid input without panicking. - fn from(s: String) -> Self { - Self::new(&s) - } -} - -impl From<&str> for MapType { - /// # Panics - /// - /// Panics if `s` cannot be parsed as a Rust type. Prefer - /// [`str::parse`] (via the [`FromStr`](std::str::FromStr) - /// implementation) to handle invalid input without panicking. - fn from(s: &str) -> Self { - Self::new(s) + let typespace = std::mem::replace( + &mut self.typespace, + typespace::settings::Settings::minimal(), + ); + self.typespace = f(typespace); + self } } -impl From for MapType { - fn from(t: syn::Type) -> Self { - Self(t) - } -} - -/// Settings that alter type generation. -#[derive(Default, Debug, Clone)] -pub struct TypeSpaceSettings { - type_mod: Option, - extra_derives: Vec, - extra_attrs: Vec, - struct_builder: bool, - - unknown_crates: UnknownPolicy, - crates: BTreeMap, - map_type: MapType, - - patch: BTreeMap, - replace: BTreeMap, - convert: Vec, +/// The typespace settings behind [`TypeSpaceSettings::new`]; see there. +/// Compatibility mode withholds `Default` from a tuple struct, a unit +/// struct, and a newtype, which typify never derives it for. +/// +/// A JSON object's keys are strings, so a map key here is always +/// `String`, a string newtype, or a string enum, every one of which +/// carries both the hashed and the ordered lookup traits. Whatever a +/// configured map container demands of its key is therefore satisfied, +/// and a map named by path alone can take the `hash_map` preset's +/// obligations and provisions without a second thought; see the +/// consumers that set one (cargo-typify's `--map-type`, the macro's +/// `map_type`). +fn baseline_typespace_settings() -> typespace::settings::Settings { + typespace::settings::Settings::minimal() + .with_required_trait(typespace::TypespaceTrait::Serialize) + .with_required_trait(typespace::TypespaceTrait::Deserialize) + .with_required_trait(typespace::TypespaceTrait::Clone) + .with_required_trait(typespace::TypespaceTrait::Debug) + .with_desired_trait(typespace::TypespaceTrait::Default) + .with_desired_trait(typespace::TypespaceTrait::Eq) + .with_desired_trait(typespace::TypespaceTrait::PartialEq) + .with_desired_trait(typespace::TypespaceTrait::Ord) + .with_desired_trait(typespace::TypespaceTrait::PartialOrd) + .with_desired_trait(typespace::TypespaceTrait::Hash) + .with_desired_trait(typespace::TypespaceTrait::Display) + .with_desired_trait(typespace::TypespaceTrait::FromStr) + .with_desired_trait(typespace::TypespaceTrait::Copy) + .with_map_type(typespace::settings::ContainerType::hash_map()) + .with_typify_compat(true) } #[derive(Debug, Clone)] @@ -449,35 +341,44 @@ impl std::str::FromStr for TypeSpaceImpl { } } -impl TypeSpaceSettings { - /// Set the name of the path prefix for types defined in this [TypeSpace]. - pub fn with_type_mod>(&mut self, type_mod: S) -> &mut Self { - self.type_mod = Some(type_mod.as_ref().to_string()); - self - } - - /// Add an additional derive macro to apply to all defined types. - pub fn with_derive(&mut self, derive: String) -> &mut Self { - if !self.extra_derives.contains(&derive) { - self.extra_derives.push(derive); - } - self - } - - /// Add an additional attribute to apply to all defined types. - pub fn with_attr(&mut self, attr: String) -> &mut Self { - if !self.extra_attrs.contains(&attr) { - self.extra_attrs.push(attr); +impl TypeSpaceImpl { + /// Translate consumer-supplied capability markers into the trait + /// set typespace expects a native to declare. + /// + /// This backs `with_conversion` and `with_replacement`, the two + /// public settings that let a consumer name an opaque native type; + /// typify cannot verify the consumer's claim the way it verifies + /// its own built-in natives, so it takes the markers at face + /// value. typify has always assumed the basic complement (`Clone`, + /// `Debug`, `Serialize`, `Deserialize`) regardless of what markers + /// are given, so those are added unconditionally. `JsonSchema` + /// joins them: a requested derive was always emitted without + /// consulting the conversion target, so a named native must + /// satisfy it. `FromStringIrrefutable` has no typespace equivalent + /// and is dropped. + pub(crate) fn native_traits(impls: &[Self]) -> typespace::TypespaceTraitSet { + let mut traits = [ + typespace::TypespaceTrait::Clone, + typespace::TypespaceTrait::Debug, + typespace::TypespaceTrait::Serialize, + typespace::TypespaceTrait::Deserialize, + typespace::TypespaceTrait::JsonSchema, + ] + .into_iter() + .collect::(); + for impl_ in impls { + match impl_ { + Self::FromStr => traits.add(typespace::TypespaceTrait::FromStr), + Self::Display => traits.add(typespace::TypespaceTrait::Display), + Self::Default => traits.add(typespace::TypespaceTrait::Default), + Self::FromStringIrrefutable => {} + } } - self - } - - /// For structs, include a "builder" type that can be used to construct it. - pub fn with_struct_builder(&mut self, struct_builder: bool) -> &mut Self { - self.struct_builder = struct_builder; - self + traits } +} +impl TypeSpaceSettings { /// Replace a referenced type with a named type. This causes the referenced /// type *not* to be generated. If the same `type_name` is specified multiple times, /// the last one is honored. @@ -524,7 +425,7 @@ impl TypeSpaceSettings { /// use typify_impl::{TypeSpace, TypeSpaceImpl, TypeSpaceSettings}; /// let mut type_space = TypeSpace::new( /// TypeSpaceSettings::default() - /// .with_struct_builder(true) + /// .map_typespace_settings(|s| s.with_struct_builder(true)) /// .with_conversion( /// SchemaObject { /// instance_type: Some(InstanceType::Number.into()), @@ -580,25 +481,6 @@ impl TypeSpaceSettings { ); self } - - /// Specify the map-like type to be used in generated code. - /// - /// ## Requirements - /// - /// - An `is_empty` method that returns a boolean - /// - Two generic parameters, `K` and `V` - /// - [`Default`] + [`Clone`] + [`Debug`] + - /// [`Serialize`][serde::Serialize] + [`Deserialize`][serde::Deserialize] - /// - /// ## Examples - /// - /// - [`::std::collections::HashMap`] - /// - [`::std::collections::BTreeMap`] - /// - [`::indexmap::IndexMap`](https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html) - pub fn with_map_type>(&mut self, map_type: T) -> &mut Self { - self.map_type = map_type.into(); - self - } } impl TypeSpacePatch { @@ -719,23 +601,17 @@ impl TypeSpace { Some(replace_type) => { let type_entry = TypeEntry::new_native( replace_type.replace_type.clone(), - &replace_type.impls.clone(), + TypeSpaceImpl::native_traits(&replace_type.impls), ); self.id_to_entry.insert(type_id, type_entry); } } } - // Eliminate cycles. It's sufficient to only start from referenced - // types as a reference is required to make a cycle. - self.break_cycles(base_id..base_id + def_len); - // Finalize all created types. for index in base_id..self.next_id { let type_id = TypeId(index); - let mut type_entry = self.id_to_entry.get(&type_id).unwrap().clone(); - debug!("finalizing type entry: {} {:#?}", index, &type_entry); - type_entry.finalize(self)?; + let type_entry = self.id_to_entry.get(&type_id).unwrap().clone(); self.id_to_entry.insert(type_id, type_entry); } @@ -748,19 +624,11 @@ impl TypeSpace { .as_ref() .and_then(|m| m.default.as_ref()) .cloned() - .map(WrappedValue::new); - let type_entry = match &mut type_entry.details { + .map(typespace::build::JsonValue::new); + let type_entry = match &mut type_entry { // The types that are already named are good to go. - TypeEntryDetails::Enum(details) => { - details.default = default; - type_entry - } - TypeEntryDetails::Struct(details) => { - details.default = default; - type_entry - } - TypeEntryDetails::Newtype(details) => { - details.default = default; + TypeEntry::Type(typ) if typ.is_named() => { + typ.set_default(default); type_entry } @@ -768,7 +636,7 @@ impl TypeSpace { // simple alias to another type in this list of definitions // (which may nor may not have already been converted). We // simply create a newtype with that type ID. - TypeEntryDetails::Reference(type_id) => TypeEntryNewtype::from_metadata( + TypeEntry::Reference(type_id) => TypeEntryNewtype::from_metadata( self, type_name, metadata, @@ -776,7 +644,11 @@ impl TypeSpace { schema.clone(), ), - TypeEntryDetails::Native(native) if native.name_match(&type_name) => type_entry, + TypeEntry::Type(typespace::build::Type::Native(native)) + if native_name_match(native, &type_name) => + { + type_entry + } // For types that don't have names, this is effectively a type // alias which we treat as a newtype. @@ -799,7 +671,8 @@ impl TypeSpace { }; // TODO need a type alias? if let Some(entry_name) = type_entry.name() { - self.name_to_id.insert(entry_name.clone(), type_id.clone()); + self.name_to_id + .insert(entry_name.to_string(), type_id.clone()); } self.id_to_entry.insert(type_id, type_entry); Ok(()) @@ -829,8 +702,7 @@ impl TypeSpace { // Finalize all created types. for index in base_id..self.next_id { let type_id = TypeId(index); - let mut type_entry = self.id_to_entry.get(&type_id).unwrap().clone(); - type_entry.finalize(self)?; + let type_entry = self.id_to_entry.get(&type_id).unwrap().clone(); self.id_to_entry.insert(type_id, type_entry); } @@ -872,113 +744,78 @@ impl TypeSpace { } } - /// Get a type given its ID. - pub fn get_type(&self, type_id: &TypeId) -> Result> { - let type_entry = self.id_to_entry.get(type_id).ok_or(Error::InvalidTypeId)?; - Ok(Type { - type_space: self, - type_entry, - }) + /// The type inserted under `type_id`, as it was inserted. + /// + /// This is the declaration typify handed typespace, available before + /// finalization: what kind of type it is, and the ids its children + /// carry. Anything the finalized graph decides, such as trait impls or + /// identifiers, is not known here; ask the [`typespace::Typespace`] + /// from [`TypeSpace::to_typespace`] for those. + /// + /// Answers `None` for an id this type space never returned. + pub fn inserted_type(&self, type_id: &TypeId) -> Option<&typespace::build::Type> { + match self.id_to_entry.get(type_id)? { + TypeEntry::Type(typ) => Some(typ), + TypeEntry::Reference(_) => None, + } } - /// Whether the generated code needs `chrono` crate. - pub fn uses_chrono(&self) -> bool { - self.uses_chrono - } + /// Finalize the collected types into a [`typespace::Typespace`]. + /// + /// The typespace is the query surface for the collected types: use + /// its `get_type` and `iter_types` to inspect a type's structure, + /// render its identifier (optionally scoped by a module path), and + /// ask about trait impls. Conversion may continue after this call; + /// a later call reflects the additional types. + 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()); + } - /// Whether the generated code needs [regress] crate. - pub fn uses_regress(&self) -> bool { - self.uses_regress - } + for (type_id, type_entry) in &self.id_to_entry { + match type_entry { + TypeEntry::Type(typ) => { + builder + .insert(type_id.clone(), typ.clone()) + .expect("type IDs are unique by construction"); + } + // References never land in id_to_entry (assign_type + // unwraps them); this is defensive. + TypeEntry::Reference(_) => {} + } + } - /// Whether the generated code needs [serde_json] crate. - pub fn uses_serde_json(&self) -> bool { - self.uses_serde_json + Ok(builder.finalize(|inner: &TypeId| TypeId(inner.0 | (1 << 63)))?) } - /// Whether the generated code needs `uuid` crate. - pub fn uses_uuid(&self) -> bool { - self.uses_uuid + /// 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 depenencies. Finalization errors result in + /// [`Error::Typespace`]. + pub fn to_codespace(&self) -> Result { + Ok(self.to_typespace()?.to_codespace()) } - /// Iterate over all types including those defined in this [TypeSpace] and - /// those referred to by those types. - pub fn iter_types(&self) -> impl Iterator> { - self.id_to_entry.values().map(move |type_entry| Type { - type_space: self, - type_entry, - }) + /// All code for processed types. + /// + /// [`TypeSpace::to_codespace`] rendered to a `TokenStream`. + pub fn to_stream(&self) -> Result { + Ok(self.to_codespace()?.into_stream()) } - /// All code for processed types. - pub fn to_stream(&self) -> TokenStream { - let mut output = OutputSpace::default(); - - // Add all types. - self.id_to_entry - .values() - .for_each(|type_entry| type_entry.output(self, &mut output)); - - // Add the shared default functions that some emitted item actually - // calls. This is gross, and may have false-positives, but those should - // be basically benign. - let called = self - .defaults + /// Record a crate behind a native type, once per crate. + fn add_dependency(&mut self, dependency: typespace::codespace::Dependency) { + if !self + .dependencies .iter() - .filter(|x| output.contains(x.fn_name())) - .collect::>(); - called - .into_iter() - .for_each(|x| output.add_item(output::OutputSpaceMod::Defaults, "", x.into())); - - // Add the error type conversions use, but only when some emitted item - // references it. This is also kind of gross that we're groveling - // around through output to decide, but it will--I hope--be - // short-lived. - if output.contains("ConversionError") { - self.add_error_item(&mut output); + .any(|known| known.name == dependency.name) + { + self.dependencies.push(dependency); } - - output.into_stream() - } - - /// The error type generated `TryFrom` and `FromStr` impls report. - fn add_error_item(&self, output: &mut OutputSpace) { - output.add_item( - output::OutputSpaceMod::Error, - "", - quote! { - /// Error from a `TryFrom` or `FromStr` implementation. - pub struct ConversionError(::std::borrow::Cow<'static, str>); - - impl ::std::error::Error for ConversionError {} - impl ::std::fmt::Display for ConversionError { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) - -> Result<(), ::std::fmt::Error> - { - ::std::fmt::Display::fmt(&self.0, f) - } - } - - impl ::std::fmt::Debug for ConversionError { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) - -> Result<(), ::std::fmt::Error> - { - ::std::fmt::Debug::fmt(&self.0, f) - } - } - impl From<&'static str> for ConversionError { - fn from(value: &'static str) -> Self { - Self(value.into()) - } - } - impl From for ConversionError { - fn from(value: String) -> Self { - Self(value.into()) - } - } - }, - ); } /// Allocated the next TypeId. @@ -988,14 +825,13 @@ impl TypeSpace { id } - /// Assign a TypeId for a TypeEntry. This handles resolving references, - /// checking for duplicate type definitions (e.g. to make sure there aren't - /// two conflicting types of the same name), and deduplicates various - /// flavors of built-in types. + /// Assign a TypeId for a TypeEntry. This handles resolving references + /// and checking for duplicate type definitions (e.g. to make sure there + /// aren't two conflicting types of the same name). fn assign_type(&mut self, ty: TypeEntry) -> TypeId { - if let TypeEntryDetails::Reference(type_id) = ty.details { + if let TypeEntry::Reference(type_id) = ty { type_id - } else if let Some(name) = ty.name() { + } else if let Some(name) = ty.name().map(str::to_string) { // If there's already a type of this name, we make sure it's // identical. Note that this covers all user-defined types. @@ -1005,7 +841,7 @@ impl TypeSpace { // bunch of places and if that were the case we might expect // them to be different and resolve that by renaming or scoping // them in some way. - if let Some(type_id) = self.name_to_id.get(name) { + if let Some(type_id) = self.name_to_id.get(&name) { // TODO we'd like to verify that the type is structurally the // same, but the types may not be functionally equal. This is a // consequence of types being "finalized" after each type @@ -1014,15 +850,12 @@ impl TypeSpace { type_id.clone() } else { let type_id = self.assign(); - self.name_to_id.insert(name.clone(), type_id.clone()); + self.name_to_id.insert(name, type_id.clone()); self.id_to_entry.insert(type_id.clone(), ty); type_id } - } else if let Some(type_id) = self.type_to_id.get(&ty.details) { - type_id.clone() } else { let type_id = self.assign(); - self.type_to_id.insert(ty.details.clone(), type_id.clone()); self.id_to_entry.insert(type_id.clone(), ty); type_id } @@ -1039,18 +872,14 @@ impl TypeSpace { ) -> Result<(TypeId, &'a Option>)> { let (mut type_entry, metadata) = self.convert_schema(type_name, schema)?; if let Some(metadata) = metadata { - let default = metadata.default.clone().map(WrappedValue::new); - match &mut type_entry.details { - TypeEntryDetails::Enum(details) => { - details.default = default; - } - TypeEntryDetails::Struct(details) => { - details.default = default; - } - TypeEntryDetails::Newtype(details) => { - details.default = default; - } - _ => (), + let default = metadata + .default + .clone() + .map(typespace::build::JsonValue::new); + // Only named types carry a default; set_default is a no-op + // otherwise. + if let Some(typ) = type_entry.as_type_mut() { + typ.set_default(default); } } let type_id = self.assign_type(type_entry); @@ -1058,212 +887,48 @@ impl TypeSpace { } /// Create an Option from a pre-assigned TypeId and assign it an ID. + /// + /// typify1 tolerated nested Option types internally and flattened + /// them when rendering identifiers; typespace renders exactly the + /// types it is given, so we avoid constructing Option> in + /// the first place. (A forward reference won't have an entry yet, + /// but references always name named types, never raw Options, so + /// wrapping is correct in that case.) fn id_to_option(&mut self, id: &TypeId) -> TypeId { - self.assign_type(TypeEntryDetails::Option(id.clone()).into()) - } - - // Create an Option from a TypeEntry by assigning it type. - fn type_to_option(&mut self, ty: TypeEntry) -> TypeEntry { - TypeEntryDetails::Option(self.assign_type(ty)).into() - } - - /// Create a Box from a pre-assigned TypeId and assign it an ID. - fn id_to_box(&mut self, id: &TypeId) -> TypeId { - self.assign_type(TypeEntryDetails::Box(id.clone()).into()) - } -} - -impl ToTokens for TypeSpace { - fn to_tokens(&self, tokens: &mut TokenStream) { - tokens.extend(self.to_stream()) - } -} - -impl Type<'_> { - /// The name of the type as a String. - pub fn name(&self) -> String { - let Type { - type_space, - type_entry, - } = self; - type_entry.type_name(type_space) - } - - /// The identifier for the type as might be used for a function return or - /// defining the type of a member of a struct.. - pub fn ident(&self) -> TokenStream { - let Type { - type_space, - type_entry, - } = self; - type_entry.type_ident(type_space, &type_space.settings.type_mod) - } - - /// The identifier for the type as might be used for a parameter in a - /// function signature. In general: simple types are the same as - /// [Type::ident] and complex types prepend a `&`. - pub fn parameter_ident(&self) -> TokenStream { - let Type { - type_space, - type_entry, - } = self; - type_entry.type_parameter_ident(type_space, None) - } - - /// The identifier for the type as might be used for a parameter in a - /// function signature along with a lifetime parameter. In general: simple - /// types are the same as [Type::ident] and complex types prepend a - /// `&'`. - pub fn parameter_ident_with_lifetime(&self, lifetime: &str) -> TokenStream { - let Type { - type_space, - type_entry, - } = self; - type_entry.type_parameter_ident(type_space, Some(lifetime)) - } - - /// A textual description of the type appropriate for debug output. - pub fn describe(&self) -> String { - self.type_entry.describe() - } - - /// Get details about the type. - pub fn details(&self) -> TypeDetails<'_> { - match &self.type_entry.details { - // Named user-defined types - TypeEntryDetails::Enum(details) => TypeDetails::Enum(TypeEnum { details }), - TypeEntryDetails::Struct(details) => TypeDetails::Struct(TypeStruct { details }), - TypeEntryDetails::Newtype(details) => TypeDetails::Newtype(TypeNewtype { details }), - - // Compound types - TypeEntryDetails::Option(type_id) => TypeDetails::Option(type_id.clone()), - TypeEntryDetails::Vec(type_id) => TypeDetails::Vec(type_id.clone()), - TypeEntryDetails::Map(key_id, value_id) => { - TypeDetails::Map(key_id.clone(), value_id.clone()) + if let Some(entry) = self.id_to_entry.get(id) { + if matches!(entry, TypeEntry::Type(typespace::build::Type::Option(_))) { + return id.clone(); } - TypeEntryDetails::Set(type_id) => TypeDetails::Set(type_id.clone()), - TypeEntryDetails::Box(type_id) => TypeDetails::Box(type_id.clone()), - TypeEntryDetails::Tuple(types) => TypeDetails::Tuple(Box::new(types.iter().cloned())), - TypeEntryDetails::Array(type_id, length) => { - TypeDetails::Array(type_id.clone(), *length) - } - - // Builtin types - TypeEntryDetails::Unit => TypeDetails::Unit, - TypeEntryDetails::Native(TypeEntryNative { - type_name: name, .. - }) - | TypeEntryDetails::Integer(name) - | TypeEntryDetails::Float(name) => TypeDetails::Builtin(name.as_str()), - TypeEntryDetails::Boolean => TypeDetails::Builtin("bool"), - TypeEntryDetails::String => TypeDetails::String, - TypeEntryDetails::JsonValue => TypeDetails::Builtin("::serde_json::Value"), - - // Only used during processing; shouldn't be visible at this point - TypeEntryDetails::Reference(_) => unreachable!(), } + self.assign_type(typespace::build::Type::Option(id.clone()).into()) } - /// Checks if the type has the associated impl. - pub fn has_impl(&self, impl_name: TypeSpaceImpl) -> bool { - let Type { - type_space, - type_entry, - } = self; - type_entry.has_impl(type_space, impl_name) - } - - /// Provides the the type identifier for the builder if one exists. - pub fn builder(&self) -> Option { - let Type { - type_space, - type_entry, - } = self; - - if !type_space.settings.struct_builder { - return None; - } - - match &type_entry.details { - TypeEntryDetails::Struct(type_entry::TypeEntryStruct { name, .. }) => { - match &type_space.settings.type_mod { - Some(type_mod) => { - let type_mod = format_ident!("{}", type_mod); - let type_name = format_ident!("{}", name); - Some(quote! { #type_mod :: builder :: #type_name }) - } - None => { - let type_name = format_ident!("{}", name); - Some(quote! { builder :: #type_name }) - } - } - } - _ => None, + // Create an Option from a TypeEntry by assigning it type. + fn type_to_option(&mut self, ty: TypeEntry) -> TypeEntry { + // As with id_to_option, don't nest Options. + if matches!(&ty, TypeEntry::Type(typespace::build::Type::Option(_))) { + return ty; } + typespace::build::Type::Option(self.assign_type(ty)).into() } } -impl<'a> TypeEnum<'a> { - /// Get name and information of each enum variant. - pub fn variants(&'a self) -> impl Iterator)> { - self.variants_info().map(|info| (info.name, info.details)) - } - - /// Get all information for each enum variant. - pub fn variants_info(&'a self) -> impl Iterator> { - self.details.variants.iter().map(move |variant| { - let details = match &variant.details { - type_entry::VariantDetails::Simple => TypeEnumVariant::Simple, - // The distinction between a lone item variant and a tuple - // variant with a single item is only relevant internally. - type_entry::VariantDetails::Item(type_id) => { - TypeEnumVariant::Tuple(vec![type_id.clone()]) - } - type_entry::VariantDetails::Tuple(types) => TypeEnumVariant::Tuple(types.clone()), - type_entry::VariantDetails::Struct(properties) => TypeEnumVariant::Struct( - properties - .iter() - .map(|prop| (prop.name.as_str(), prop.type_id.clone())) - .collect(), - ), - }; - TypeEnumVariantInfo { - name: variant.ident_name.as_ref().unwrap(), - description: variant.description.as_deref(), - details, - } - }) - } -} - -impl<'a> TypeStruct<'a> { - /// Get name and type of each property. - pub fn properties(&'a self) -> impl Iterator { - self.details - .properties - .iter() - .map(move |prop| (prop.name.as_str(), prop.type_id.clone())) - } - - /// Get all information about each struct property. - pub fn properties_info(&'a self) -> impl Iterator> { - self.details - .properties - .iter() - .map(move |prop| TypeStructPropInfo { - name: prop.name.as_str(), - description: prop.description.as_deref(), - required: matches!(&prop.state, StructPropertyState::Required), - type_id: prop.type_id.clone(), - }) - } -} - -impl TypeNewtype<'_> { - /// Get the inner type of the newtype struct. - pub fn inner(&self) -> TypeId { - self.details.type_id.clone() - } +/// Whether a native type's name matches the required name for a +/// reference type (or the native type has parameters and so couldn't +/// simply be aliased). +fn native_name_match(native: &typespace::build::Native, type_name: &Name) -> bool { + // typespace answers a native's path as a syn::Type, so read the + // last segment rather than splitting the rendered tokens, which + // carry spaces around their separators. + let native_name = match native.path() { + syn::Type::Path(path) => path.path.segments.last().map(|seg| seg.ident.to_string()), + _ => None, + }; + !native.parameters().is_empty() + || matches!( + (type_name, native_name.as_deref()), + (Name::Required(req), Some(name)) if req == name + ) } #[cfg(test)] @@ -1275,36 +940,9 @@ mod tests { use std::collections::HashSet; use crate::{ - output::OutputSpace, - test_util::validate_output, - type_entry::{TypeEntryEnum, VariantDetails}, - MapType, Name, TypeEntryDetails, TypeSpace, TypeSpaceSettings, + test_util::validate_output, type_entry::TypeEntry, Name, TypeSpace, TypeSpaceSettings, }; - - #[test] - fn test_map_type_from_str() { - let map_type = "::std::collections::BTreeMap".parse::().unwrap(); - assert_eq!(map_type.to_string(), ":: std :: collections :: BTreeMap"); - - "not a valid!!type".parse::().unwrap_err(); - "".parse::().unwrap_err(); - } - - #[test] - fn test_map_type_deserialize() { - let map_type: MapType = - serde_json::from_value(json!("::std::collections::BTreeMap")).unwrap(); - assert_eq!(map_type.to_string(), ":: std :: collections :: BTreeMap"); - - // Strings with escape sequences require owned deserialization; make - // sure that works. - let map_type: MapType = - serde_json::from_str("\"::std::collections::\\u0042TreeMap\"").unwrap(); - assert_eq!(map_type.to_string(), ":: std :: collections :: BTreeMap"); - - // ... and invalid types must produce an error rather than a panic. - serde_json::from_value::(json!("not a valid!!type")).unwrap_err(); - } + use typespace::build::{Type, VariantDetails}; #[allow(dead_code)] #[derive(Serialize, JsonSchema)] @@ -1367,16 +1005,10 @@ mod tests { println!("{:#?}", ty); - let mut output = OutputSpace::default(); - ty.output(&type_space, &mut output); - println!("{}", output.into_stream()); - - for ty in type_space.id_to_entry.values() { - println!("{:#?}", ty); - let mut output = OutputSpace::default(); - ty.output(&type_space, &mut output); - println!("{}", output.into_stream()); - } + // Render everything (including the newly converted type) via + // typespace. + let _ = type_space.assign_type(ty); + println!("{}", type_space.to_stream().unwrap()); } #[test] @@ -1403,7 +1035,7 @@ mod tests { let settings = TypeSpaceSettings::default(); let mut type_space = TypeSpace::new(&settings); type_space.add_root_schema(schema).unwrap(); - let tokens = type_space.to_stream().to_string(); + let tokens = type_space.to_stream().unwrap().to_string(); println!("{}", tokens); assert!(tokens .contains(" pub struct Somename { pub someproperty : :: std :: string :: String , }")) @@ -1433,14 +1065,15 @@ mod tests { ) .unwrap(); - match ty.details { - TypeEntryDetails::Enum(TypeEntryEnum { variants, .. }) => { - for variant in &variants { - assert_eq!(variant.details, VariantDetails::Simple); + match &ty { + TypeEntry::Type(Type::Enum(type_enum)) => { + let variants = type_enum.get_variants(); + for variant in variants { + assert_eq!(variant.details(), &VariantDetails::Unit); } let var_names = variants .iter() - .map(|variant| variant.ident_name.as_ref().unwrap().clone()) + .map(|variant| variant.rust_name().to_string()) .collect::>(); assert_eq!( var_names, @@ -1451,10 +1084,7 @@ mod tests { ); } _ => { - let mut output = OutputSpace::default(); - ty.output(&type_space, &mut output); - println!("{}", output.into_stream()); - panic!(); + panic!("unexpected type entry {:#?}", ty); } } } @@ -1480,13 +1110,14 @@ mod tests { ) .unwrap(); - if let TypeEntryDetails::Option(id) = &te.details { + if let TypeEntry::Type(Type::Option(id)) = &te { let ote = type_space.id_to_entry.get(id).unwrap(); - if let TypeEntryDetails::Enum(TypeEntryEnum { variants, .. }) = &ote.details { - let variants = variants + if let Type::Enum(type_enum) = ote.as_type() { + let variants = type_enum + .get_variants() .iter() - .map(|v| match v.details { - VariantDetails::Simple => v.ident_name.as_ref().unwrap().clone(), + .map(|v| match v.details() { + VariantDetails::Unit => v.rust_name().to_string(), _ => panic!("unexpected variant type"), }) .collect::>(); @@ -1521,60 +1152,4 @@ mod tests { validate_output::(); } - - #[test] - fn test_builder_name() { - #[allow(dead_code)] - #[derive(JsonSchema)] - struct TestStruct { - x: u32, - } - - let mut type_space = TypeSpace::default(); - let schema = schema_for!(TestStruct); - let type_id = type_space.add_root_schema(schema).unwrap().unwrap(); - let ty = type_space.get_type(&type_id).unwrap(); - - assert!(ty.builder().is_none()); - - let mut type_space = TypeSpace::new(TypeSpaceSettings::default().with_struct_builder(true)); - let schema = schema_for!(TestStruct); - let type_id = type_space.add_root_schema(schema).unwrap().unwrap(); - let ty = type_space.get_type(&type_id).unwrap(); - - assert_eq!( - ty.builder().map(|ts| ts.to_string()), - Some("builder :: TestStruct".to_string()) - ); - - let mut type_space = TypeSpace::new( - TypeSpaceSettings::default() - .with_type_mod("types") - .with_struct_builder(true), - ); - let schema = schema_for!(TestStruct); - let type_id = type_space.add_root_schema(schema).unwrap().unwrap(); - let ty = type_space.get_type(&type_id).unwrap(); - - assert_eq!( - ty.builder().map(|ts| ts.to_string()), - Some("types :: builder :: TestStruct".to_string()) - ); - - #[allow(dead_code)] - #[derive(JsonSchema)] - enum TestEnum { - X, - Y, - } - let mut type_space = TypeSpace::new( - TypeSpaceSettings::default() - .with_type_mod("types") - .with_struct_builder(true), - ); - let schema = schema_for!(TestEnum); - let type_id = type_space.add_root_schema(schema).unwrap().unwrap(); - let ty = type_space.get_type(&type_id).unwrap(); - assert!(ty.builder().is_none()); - } } 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/src/structs.rs b/typify-impl/src/structs.rs index 379c34c9..b9b0b75a 100644 --- a/typify-impl/src/structs.rs +++ b/typify-impl/src/structs.rs @@ -1,26 +1,34 @@ -// Copyright 2024 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use heck::ToSnakeCase; -use proc_macro2::TokenStream; -use quote::quote; use schemars::schema::{InstanceType, Metadata, ObjectValidation, Schema, SchemaObject}; use crate::{ - output::{OutputSpace, OutputSpaceMod}, - type_entry::{ - StructProperty, StructPropertyRename, StructPropertyState, TypeEntry, TypeEntryStruct, - WrappedValue, - }, + type_entry::{TypeEntry, TypeEntryStruct}, util::{get_type_name, metadata_description, recase, Case}, - Name, Result, TypeEntryDetails, TypeId, TypeSpace, + Name, Result, TypeId, TypeSpace, }; +use typespace::build::{JsonValue, StructProperty, StructPropertySerde, StructPropertyState, Type}; + +/// The pre-mapping volitionality of a property, mirroring typify1's +/// internal `StructPropertyState`. This is in-flight data: it captures +/// what `has_default` learns from the schema before `struct_property` +/// maps it onto typespace's `StructPropertyState` (and decides whether +/// to Option-wrap the property type). See the comment in +/// `struct_property` for the mapping. +enum PropertyState { + Required, + Optional, + Default(serde_json::Value), +} + impl TypeSpace { pub(crate) fn struct_members( &mut self, type_name: Option, validation: &ObjectValidation, - ) -> Result<(Vec, bool)> { + ) -> Result<(Vec>, bool)> { // These are the fields we don't currently handle //assert!(validation.max_properties.is_none()); //assert!(validation.min_properties.is_none()); @@ -71,7 +79,7 @@ impl TypeSpace { .collect::>>()?; // Sort parameters by name to ensure a deterministic result. - properties.sort_by(|a, b| a.name.cmp(&b.name)); + properties.sort_by(|a, b| a.rust_name().cmp(b.rust_name())); // If there are additional properties tack them on, flattened, at the // end. Note that a `None` value for additional_properties is @@ -103,13 +111,8 @@ impl TypeSpace { additional_properties, )?; let map_type_id = self.assign_type(map_type); - let extra_prop = StructProperty { - name: "extra".to_string(), - rename: StructPropertyRename::Flatten, - state: StructPropertyState::Required, - description: None, - type_id: map_type_id, - }; + let extra_prop = StructProperty::new("extra", map_type_id) + .with_json_name(StructPropertySerde::Flatten); properties.push(extra_prop); false @@ -125,13 +128,43 @@ impl TypeSpace { required: &schemars::Set, prop_name: &str, schema: &schemars::schema::Schema, - ) -> Result { + ) -> Result> { let sub_type_name = match type_name { Some(name) => Name::Suggested(name), None => Name::Unknown, }; let (mut type_id, metadata) = self.id_for_schema(sub_type_name, schema)?; + // Map the property's volitionality onto typespace's + // StructPropertyState. This is the most delicate judgment call in + // the typespace migration because the two models split the work + // differently: + // + // typify1 Option-wraps at conversion time: a non-required + // property with no intrinsic or explicit default gets its type + // wrapped in Option here, and its state (typify1's "Optional") + // then means "may be absent; when absent, take the type's + // intrinsic default"--a state that applied equally to Option, + // Vec, Map, and Unit typed properties. + // + // typespace Option-wraps at render time: its Optional state on a + // non-Option type produces Option in the output along with + // json_serde::deserialize_some (preserving absent-vs-null), a + // semantic typify1 never expresses. Its Default state means "may + // be absent; absent means the intrinsic default" and emits + // #[serde(default)] plus is_empty-style skips. + // + // We keep typify1's behavior: wrap here, and then + // - a wrapped (or intrinsically Option) property maps to + // typespace Optional. Under the default ConflateAsAbsent + // setting typespace leaves an Option type unwrapped, so there + // is no double-wrapping. + // - a non-required, non-Option property with an intrinsic + // default (Vec, Map, Unit, or a default value that equals the + // intrinsic one) maps to typespace Default. Mapping these to + // typespace Optional would wrap them in Option, which typify1 + // deliberately does not do. + // - an explicit default value maps to typespace DefaultValue. let state = if required.contains(prop_name) { StructPropertyState::Required } else { @@ -159,27 +192,43 @@ impl TypeSpace { &type_id, metadata.as_ref().and_then(|m| m.default.as_ref()), ) { - StructPropertyState::Required => { + PropertyState::Required => { type_id = self.id_to_option(&type_id); StructPropertyState::Optional } - other => other, + PropertyState::Optional => { + // The type may itself be an Option (may be null and + // may be absent); otherwise it has an intrinsic + // default (Vec, Map, Unit, or a matching explicit + // default). See the mapping note above. + if matches!( + self.id_to_entry.get(&type_id).map(TypeEntry::as_type), + Some(Type::Option(_)) + ) { + StructPropertyState::Optional + } else { + StructPropertyState::Default + } + } + PropertyState::Default(value) => { + StructPropertyState::DefaultValue(JsonValue::new(value)) + } } }; let (name, rename) = recase(prop_name, Case::Snake); let rename = match rename { - Some(old_name) => StructPropertyRename::Rename(old_name), - None => StructPropertyRename::None, + Some(old_name) => StructPropertySerde::Rename(old_name), + None => StructPropertySerde::None, }; - Ok(StructProperty { - name, - rename, - state, - description: metadata_description(metadata), - type_id, - }) + let mut property = StructProperty::new(name, type_id) + .with_json_name(rename) + .with_state(state); + if let Some(description) = metadata_description(metadata) { + property = property.with_description(description); + } + Ok(property) } pub(crate) fn make_map( @@ -189,7 +238,7 @@ impl TypeSpace { additional_properties: &Option>, ) -> Result { let key_id = match property_names.as_deref() { - Some(Schema::Bool(true)) | None => self.assign_type(TypeEntryDetails::String.into()), + Some(Schema::Bool(true)) | None => self.assign_type(Type::String.into()), // TODO this would correspond to an empty object: an object with // no legal property values. @@ -216,7 +265,7 @@ impl TypeSpace { None => self.id_for_schema(Name::Unknown, &Schema::Bool(true))?, }; - Ok(TypeEntryDetails::Map(key_id, value_id).into()) + Ok(Type::Map(key_id, value_id).into()) } /// Perform a schema conversion for a type that must be string-like. @@ -291,17 +340,18 @@ impl TypeSpace { // from the name of the type let name = format!("subtype_{}", idx); - Ok(StructProperty { - name, - rename: StructPropertyRename::Flatten, - state: if optional { - StructPropertyState::Optional - } else { - StructPropertyState::Required - }, - description: None, - type_id, - }) + // The optional case wraps the type in an Option just + // above, so typespace's Optional state (which leaves + // Option types unwrapped under ConflateAsAbsent) is + // the right mapping. + let state = if optional { + StructPropertyState::Optional + } else { + StructPropertyState::Required + }; + Ok(StructProperty::new(name, type_id) + .with_json_name(StructPropertySerde::Flatten) + .with_state(state)) }) .collect::>>()?; @@ -319,108 +369,6 @@ impl TypeSpace { } } -pub(crate) enum DefaultFunction { - None, - Default, - Custom(String), -} - -/// Generate the serde attribute parameters for the given property. -/// -/// This may include a default value that requires a generated function to -/// produce it. In such a case, that function will be added to the OutputSpace. -/// -/// Note that if we have several serde attribute parameters, they could each -/// appear in their own attribute. We choose to condense them for the sake of -/// legibility. -pub(crate) fn generate_serde_attr( - type_name: &str, - prop_name: &str, - naming: &StructPropertyRename, - state: &StructPropertyState, - prop_type: &TypeEntry, - type_space: &TypeSpace, - output: &mut OutputSpace, -) -> (TokenStream, DefaultFunction) { - let mut serde_options = Vec::new(); - match naming { - StructPropertyRename::Rename(s) => serde_options.push(quote! { rename = #s }), - StructPropertyRename::Flatten => serde_options.push(quote! { flatten }), - StructPropertyRename::None => (), - } - - let default_fn = match (state, &prop_type.details) { - (StructPropertyState::Optional, TypeEntryDetails::Option(_)) => { - serde_options.push(quote! { skip_serializing_if = "::std::option::Option::is_none" }); - DefaultFunction::Default - } - (StructPropertyState::Optional, TypeEntryDetails::Vec(_)) => { - serde_options.push(quote! { default }); - serde_options.push(quote! { skip_serializing_if = "::std::vec::Vec::is_empty" }); - DefaultFunction::Default - } - (StructPropertyState::Optional, TypeEntryDetails::Map(key_id, value_id)) => { - serde_options.push(quote! { default }); - - let map_to_use = &type_space.settings.map_type; - let key_ty = type_space - .id_to_entry - .get(key_id) - .expect("unresolved key type id for map"); - let value_ty = type_space - .id_to_entry - .get(value_id) - .expect("unresolved value type id for map"); - - if key_ty.details == TypeEntryDetails::String - && value_ty.details == TypeEntryDetails::JsonValue - { - serde_options.push(quote! { - skip_serializing_if = "::serde_json::Map::is_empty" - }); - } else { - let is_empty = format!("{}::is_empty", map_to_use); - serde_options.push(quote! { - skip_serializing_if = #is_empty - }); - } - DefaultFunction::Default - } - (StructPropertyState::Optional, _) => { - serde_options.push(quote! { default }); - DefaultFunction::Default - } - - (StructPropertyState::Default(WrappedValue(value)), _) => { - let (fn_name, default_fn) = - prop_type.default_fn(value, type_space, type_name, prop_name); - serde_options.push(quote! { default = #fn_name }); - - if let Some(default_fn) = default_fn { - output.add_item(OutputSpaceMod::Defaults, type_name, default_fn); - } - DefaultFunction::Custom(fn_name) - } - - // Required Option types need a serde annotation. - (StructPropertyState::Required, TypeEntryDetails::Option(_)) => { - serde_options.push(quote! { deserialize_with = "::std::option::Option::deserialize" }); - DefaultFunction::None - } - (StructPropertyState::Required, _) => DefaultFunction::None, - }; - - let serde = if serde_options.is_empty() { - quote! {} - } else { - quote! { - #[serde( #(#serde_options),*)] - } - }; - - (serde, default_fn) -} - /// See if this type is a type that we can omit with a serde directive; note /// that the type id lookup will fail only for references (and only during /// initial reference processing). @@ -428,7 +376,7 @@ fn has_default( type_space: &mut TypeSpace, type_id: &TypeId, default: Option<&serde_json::Value>, -) -> StructPropertyState { +) -> PropertyState { // This lookup can fail in the scenario where a struct (or struct // variant) member is optional and the type of that optional member is a // reference to a type that has not yet been converted. This is fine: those @@ -437,54 +385,46 @@ fn has_default( type_space .id_to_entry .get(type_id) - .map(|type_entry| &type_entry.details), + .map(|type_entry| type_entry.as_type()), default, ) { // No default specified. - (Some(TypeEntryDetails::Option(_)), None) => StructPropertyState::Optional, - (Some(TypeEntryDetails::Vec(_)), None) => StructPropertyState::Optional, - (Some(TypeEntryDetails::Map(..)), None) => StructPropertyState::Optional, - (Some(TypeEntryDetails::Unit), None) => StructPropertyState::Optional, - (_, None) => StructPropertyState::Required, + (Some(Type::Option(_)), None) => PropertyState::Optional, + (Some(Type::Vec(_)), None) => PropertyState::Optional, + (Some(Type::Map(..)), None) => PropertyState::Optional, + (Some(Type::Unit), None) => PropertyState::Optional, + (_, None) => PropertyState::Required, // Default specified is the same as the implicit default: null - (Some(TypeEntryDetails::Option(_)), Some(serde_json::Value::Null)) => { - StructPropertyState::Optional - } + (Some(Type::Option(_)), Some(serde_json::Value::Null)) => PropertyState::Optional, // Default specified is the same as the implicit default: [] - (Some(TypeEntryDetails::Vec(_)), Some(serde_json::Value::Array(a))) if a.is_empty() => { - StructPropertyState::Optional + (Some(Type::Vec(_)), Some(serde_json::Value::Array(a))) if a.is_empty() => { + PropertyState::Optional } // Default specified is the same as the implicit default: {} - (Some(TypeEntryDetails::Map(..)), Some(serde_json::Value::Object(m))) if m.is_empty() => { - StructPropertyState::Optional + (Some(Type::Map(..)), Some(serde_json::Value::Object(m))) if m.is_empty() => { + PropertyState::Optional } // Default specified is the same as the implicit default: false - (Some(TypeEntryDetails::Boolean), Some(serde_json::Value::Bool(false))) => { - StructPropertyState::Optional - } + (Some(Type::Boolean), Some(serde_json::Value::Bool(false))) => PropertyState::Optional, // Default specified is the same as the implicit default: 0 - (Some(TypeEntryDetails::Integer(_)), Some(serde_json::Value::Number(n))) - if n.as_u64() == Some(0) => - { - StructPropertyState::Optional + (Some(Type::Integer(_)), Some(serde_json::Value::Number(n))) if n.as_u64() == Some(0) => { + PropertyState::Optional } // Default specified is the same as the implicit default: 0.0 - (Some(TypeEntryDetails::Integer(_)), Some(serde_json::Value::Number(n))) - if n.as_f64() == Some(0.0) => - { - StructPropertyState::Optional + (Some(Type::Integer(_)), Some(serde_json::Value::Number(n))) if n.as_f64() == Some(0.0) => { + PropertyState::Optional } // Default specified is the same as the implicit default: "" - (Some(TypeEntryDetails::String), Some(serde_json::Value::String(s))) if s.is_empty() => { - StructPropertyState::Optional + (Some(Type::String), Some(serde_json::Value::String(s))) if s.is_empty() => { + PropertyState::Optional } // This is a reference that will resolve to this type id later. - (None, Some(default)) => StructPropertyState::Default(WrappedValue(default.clone())), + (None, Some(default)) => PropertyState::Default(default.clone()), // All other types as well as types with intrinsic defaults that have // been explicitly overridden. - (Some(_), Some(default)) => StructPropertyState::Default(WrappedValue(default.clone())), + (Some(_), Some(default)) => PropertyState::Default(default.clone()), } } @@ -572,7 +512,13 @@ mod tests { let mut type_space = TypeSpace::default(); let (ty, _) = type_space.convert_schema(Name::Unknown, &schema).unwrap(); - let output = ty.type_name(&type_space).replace(" ", ""); + let type_id = type_space.assign_type(ty); + let output = type_space + .to_typespace() + .unwrap() + .get_type(&type_id) + .name() + .replace(" ", ""); assert_eq!( output, "::serde_json::Map<::std::string::String,::serde_json::Value>" diff --git a/typify-impl/src/test_util.rs b/typify-impl/src/test_util.rs index bbc2fd46..1546982a 100644 --- a/typify-impl/src/test_util.rs +++ b/typify-impl/src/test_util.rs @@ -12,7 +12,7 @@ use syn::{ FieldsNamed, FieldsUnnamed, File, Type, TypePath, TypeTuple, Variant, }; -use crate::{output::OutputSpace, RefKey, TypeId, TypeSpace}; +use crate::{RefKey, TypeId, TypeSpace}; pub(crate) fn get_type() -> (TypeSpace, TypeId) { let schema = schema_for!(T); @@ -62,14 +62,54 @@ pub(crate) fn validate_output_for_untagged_enm() { validate_output_impl::(true) } +/// Render the full type space via typespace and pluck out the items +/// (type definitions and impl blocks) that pertain to the type of the +/// given name. typify1 could render a single type in isolation; with +/// rendering delegated to typespace, the whole (finalized) type space is +/// rendered and filtered. +pub(crate) fn items_for(type_space: &TypeSpace, name: &str) -> Vec { + let stream = type_space.to_stream().unwrap(); + let file = + parse2::(stream.clone()).unwrap_or_else(|_| panic!("invalid output: {}", stream)); + file.items + .into_iter() + .filter(|item| match item { + syn::Item::Struct(s) => s.ident == name, + syn::Item::Enum(e) => e.ident == name, + syn::Item::Type(t) => t.ident == name, + syn::Item::Impl(i) => match i.self_ty.as_ref() { + Type::Path(type_path) => type_path + .path + .segments + .last() + .is_some_and(|segment| segment.ident == name), + _ => false, + }, + _ => false, + }) + .collect() +} + +/// The rendered tokens (as a string) for all items pertaining to the +/// named type; see [`items_for`]. +pub(crate) fn render_items_for(type_space: &TypeSpace, name: &str) -> String { + items_for(type_space, name) + .into_iter() + .map(|item| item.to_token_stream()) + .collect::() + .to_string() +} + #[track_caller] fn validate_output_impl(ignore_variant_names: bool) { let (type_space, type_id) = get_type::(); let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - let mut output = OutputSpace::default(); - type_entry.output(&type_space, &mut output); - let output = output.into_stream(); + let type_name = type_entry.name().expect("type must be named").to_string(); + let output = items_for(&type_space, &type_name) + .into_iter() + .map(|item| item.to_token_stream()) + .collect::(); let expected = T::schema(); diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index 2881f555..598bda76 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -1,241 +1,90 @@ // Copyright 2026 Oxide Computer Company -use std::collections::{BTreeMap, BTreeSet, HashMap}; +//! Construction of [`typespace::Type`] values from schema data. +//! +//! Historically this module defined typify's internal type +//! representation (`TypeEntry` and friends) along with its rendering. +//! The representation is now `typespace::build::Type`, +//! constructed directly at the conversion sites in this module and in +//! convert.rs / structs.rs / enums.rs, and rendering is typespace's job +//! (see `TypeSpace::to_stream`). What remains here is: +//! +//! - `TypeEntry`, which wraps a typespace type or an in-flight +//! reference. +//! - the `from_metadata` constructors that turn schema metadata into +//! named typespace types. +//! +//! The queries typify's public API used to answer from here (`has_impl`, +//! `type_ident`, ...) are typespace's job; see +//! `TypeSpace::to_typespace` and typespace's view module. + +use std::collections::HashMap; -use proc_macro2::{Punct, Spacing, TokenStream, TokenTree}; -use quote::{format_ident, quote, ToTokens}; use schemars::schema::{Metadata, Schema}; -use syn::Path; use unicode_ident::is_xid_continue; use crate::{ - enums::output_variant, - output::{OutputSpace, OutputSpaceMod}, sanitize, - structs::{generate_serde_attr, DefaultFunction}, util::{get_type_name, metadata_description, unique, TypePatch}, - Case, DefaultImpl, Name, Result, TypeId, TypeSpace, TypeSpaceImpl, + Case, Name, TypeId, TypeSpace, }; -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct SchemaWrapper(Schema); - -impl Eq for SchemaWrapper {} - -impl Ord for SchemaWrapper { - fn cmp(&self, _other: &Self) -> std::cmp::Ordering { - std::cmp::Ordering::Equal - } -} -impl PartialOrd for SchemaWrapper { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) struct TypeEntryEnum { - pub name: String, - pub rename: Option, - pub description: Option, - pub default: Option, - pub tag_type: EnumTagType, - pub variants: Vec, - pub deny_unknown_fields: bool, - pub bespoke_impls: BTreeSet, - pub schema: SchemaWrapper, -} - -/// Cached attributes that (mostly) result in customized impl generation. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum TypeEntryEnumImpl { - AllSimpleVariants, - UntaggedFromStr, - UntaggedDisplay, - /// This is a cached marker to let us know that at least one of the - /// variants is irrefutably a string. There is currently no associated - /// implementation that we generate. - UntaggedFromStringIrrefutable, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) struct TypeEntryStruct { - pub name: String, - pub rename: Option, - pub description: Option, - pub default: Option, - pub properties: Vec, - pub deny_unknown_fields: bool, - pub schema: SchemaWrapper, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) struct TypeEntryNewtype { - pub name: String, - pub rename: Option, - pub description: Option, - pub default: Option, - pub type_id: TypeId, - pub constraints: TypeEntryNewtypeConstraints, - pub schema: SchemaWrapper, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum TypeEntryNewtypeConstraints { - None, - EnumValue(Vec), - DenyValue(Vec), - String { - max_length: Option, - min_length: Option, - pattern: Option, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) struct TypeEntryNative { - pub type_name: String, - impls: Vec, - // TODO to support const generics, this can be some sort of TypeOrValue, - // but note that we may some day need to disambiguate char and &'static str - // since schemars represents a char as a string of length 1. - pub parameters: Vec, -} -impl TypeEntryNative { - pub(crate) fn name_match(&self, type_name: &Name) -> bool { - let native_name = self.type_name.rsplit("::").next().unwrap(); - !self.parameters.is_empty() - || matches!(type_name, Name::Required(req) if req == native_name) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct WrappedValue(pub serde_json::Value); -impl WrappedValue { - pub(crate) fn new(value: serde_json::Value) -> Self { - Self(value) - } -} - -impl Ord for WrappedValue { - fn cmp(&self, _: &Self) -> std::cmp::Ordering { - std::cmp::Ordering::Equal - } -} -impl PartialOrd for WrappedValue { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -// TODO This struct needs to go away (again). The derives should go into the -// generated struct/enum/newtype structs. Same for the impls. Native types will -// also have impls. Builtin generic types such as Box or Vec will delegate to -// their subtypes (while recursive, it is necessarily terminating... though I -// suppose we could memoize it). Builtin simple types such as u64 or String -// have a static list. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct TypeEntry { - pub details: TypeEntryDetails, - pub extra_derives: BTreeSet, - pub extra_attrs: BTreeSet, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum TypeEntryDetails { - Enum(TypeEntryEnum), - Struct(TypeEntryStruct), - Newtype(TypeEntryNewtype), - - /// Native types exported from a well-known crate. - Native(TypeEntryNative), - - // Types from core and std. - Option(TypeId), - Box(TypeId), - Vec(TypeId), - Map(TypeId, TypeId), - Set(TypeId), - Array(TypeId, usize), - Tuple(Vec), - Unit, - Boolean, - /// Integers - Integer(String), - /// Floating point numbers; not Eq, Ord, or Hash - Float(String), - /// Strings... which we handle a little specially. - String, - /// serde_json::Value which we also handle specially. - JsonValue, +use typespace::build::{ + Enum, EnumTagType, EnumVariant, JsonValue, Native, NewtypeConstraints, NewtypeStruct, Struct, + StructProperty, Type, VariantDetails, +}; - /// While these types won't very make their way out to the user, we need - /// reference types in particular to represent simple type aliases between - /// types named as reference targets. +/// A typespace type under construction, or an in-flight reference to +/// another type ID. +/// +/// typespace has no reference variant--every ID inserted into its +/// builder must name a concrete type--and it doesn't need one: by the +/// time output is requested, every reference has been resolved. +/// References exist only during conversion (see `convert_reference`) and +/// are unwrapped by `TypeSpace::assign_type` before storage. +#[derive(Debug, Clone)] +pub(crate) enum TypeEntry { + Type(Type), Reference(TypeId), } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum EnumTagType { - External, - Internal { tag: String }, - Adjacent { tag: String, content: String }, - Untagged, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// In-flight enum variant used while assembling a `TypeEnum`. The +/// identifier name is chosen (and de-duplicated) in +/// `TypeEntryEnum::from_metadata`; until then only the raw (JSON) name is +/// known. +#[derive(Debug, Clone)] pub(crate) struct Variant { pub raw_name: String, - pub ident_name: Option, - pub description: Option, - pub details: VariantDetails, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum VariantDetails { - Simple, - Item(TypeId), - Tuple(Vec), - Struct(Vec), -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) struct StructProperty { - pub name: String, - pub rename: StructPropertyRename, - pub state: StructPropertyState, pub description: Option, - pub type_id: TypeId, + pub details: VariantDetails, } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum StructPropertyRename { - None, - Rename(String), - Flatten, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum StructPropertyState { - Required, - Optional, - Default(WrappedValue), +impl Variant { + pub(crate) fn new( + raw_name: String, + description: Option, + details: VariantDetails, + ) -> Self { + Self { + raw_name, + description, + details, + } + } } -#[derive(Debug)] -pub(crate) enum DefaultKind { - Intrinsic, - Specific, - Generic(DefaultImpl), +fn variant_names_unique(names: &[String]) -> bool { + unique(names.iter()) } -fn variants_unique(variants: &[Variant]) -> bool { - unique( - variants - .iter() - .map(|variant| variant.ident_name.as_ref().unwrap()), - ) -} +/// Constructors for named typespace types. These retain the shape (and +/// names) of typify's original `TypeEntryEnum` / `TypeEntryStruct` / +/// `TypeEntryNewtype` constructors so that the conversion code reads the +/// same; the difference is that they now construct `typespace::Type` +/// values directly. +pub(crate) struct TypeEntryEnum {} +pub(crate) struct TypeEntryStruct {} +pub(crate) struct TypeEntryNewtype {} impl TypeEntryEnum { pub(crate) fn from_metadata( @@ -243,125 +92,99 @@ impl TypeEntryEnum { type_name: Name, metadata: &Option>, tag_type: EnumTagType, - mut variants: Vec, + variants: Vec, deny_unknown_fields: bool, schema: Schema, ) -> TypeEntry { // Let's find some decent names for variants. We first try the simple // sanitization. - variants.iter_mut().for_each(|variant| { - let ident_name = sanitize(&variant.raw_name, Case::Pascal); - variant.ident_name = Some(ident_name); - }); + let mut ident_names = variants + .iter() + .map(|variant| sanitize(&variant.raw_name, Case::Pascal)) + .collect::>(); // If variants aren't unique, we're turn the elided characters into // 'x's. - if !variants_unique(&variants) { - variants.iter_mut().for_each(|variant| { - let ident_name = sanitize( - &variant - .raw_name - .replace(|c| c == '_' || !is_xid_continue(c), "X"), - Case::Pascal, - ); - variant.ident_name = Some(ident_name); - }); + if !variant_names_unique(&ident_names) { + ident_names = variants + .iter() + .map(|variant| { + sanitize( + &variant + .raw_name + .replace(|c| c == '_' || !is_xid_continue(c), "X"), + Case::Pascal, + ) + }) + .collect(); } // If variants still aren't unique, we fail: we'd rather not emit code // that can't compile - if !variants_unique(&variants) { + if !variant_names_unique(&ident_names) { let mut counts = HashMap::new(); - variants.iter().for_each(|variant| { + ident_names.iter().for_each(|ident_name| { counts - .entry(variant.ident_name.as_ref().unwrap()) + .entry(ident_name) .and_modify(|xxx| *xxx += 1) .or_insert(0); }); let dups = variants .iter() - .filter(|variant| *counts.get(variant.ident_name.as_ref().unwrap()).unwrap() > 0) - .map(|variant| variant.raw_name.as_str()) + .zip(ident_names.iter()) + .filter(|(_, ident_name)| *counts.get(ident_name).unwrap() > 0) + .map(|(variant, _)| variant.raw_name.as_str()) .collect::>() .join(","); panic!("Failed to make unique variant names for [{}]", dups); } + let variants: Vec> = variants + .into_iter() + .zip(ident_names) + .map( + |( + Variant { + raw_name, + description, + details, + }, + ident_name, + )| { + // typify computed the serde rename at output time by + // comparing the sanitized identifier with the raw + // name; typespace wants it precomputed. + let rename = (ident_name != raw_name).then_some(raw_name); + let mut variant = EnumVariant::new(ident_name, details); + if let Some(rename) = rename { + variant = variant.with_rename(rename); + } + if let Some(description) = description { + variant = variant.with_description(description); + } + variant + }, + ) + .collect(); + let name = get_type_name(&type_name, metadata).unwrap(); - let rename = None; let description = metadata_description(metadata); let type_patch = TypePatch::new(type_space, name); - let details = TypeEntryDetails::Enum(Self { - name: type_patch.name, - rename, - description, - default: None, - tag_type, - variants, - deny_unknown_fields, - bespoke_impls: Default::default(), - schema: SchemaWrapper(schema), - }); - - TypeEntry { - details, - extra_derives: type_patch.derives, - extra_attrs: type_patch.attrs, + let mut type_enum = Enum::new() + .name(&type_patch.name) + .description(make_doc(&type_patch.name, description.as_ref(), &schema)) + .extra_derives(type_patch.derives) + .extra_attrs(type_patch.attrs) + .tag_type(tag_type) + .variants(variants); + if deny_unknown_fields { + type_enum = type_enum.deny_unknown_fields(); } - } - - pub(crate) fn finalize(&mut self, type_space: &TypeSpace) { - self.bespoke_impls = [ - // Not untagged with all simple variants. - (self.tag_type != EnumTagType::Untagged - && !self.variants.is_empty() - && self - .variants - .iter() - .all(|variant| matches!(variant.details, VariantDetails::Simple))) - .then_some(TypeEntryEnumImpl::AllSimpleVariants), - // Untagged and all variants impl FromStr, but none **is** a - // String (i.e. irrefutably). - untagged_newtype_variants( - type_space, - &self.tag_type, - &self.variants, - TypeSpaceImpl::FromStr, - Some(TypeSpaceImpl::FromStringIrrefutable), - ) - .then_some(TypeEntryEnumImpl::UntaggedFromStr), - // Untagged and all variants impl Display. - untagged_newtype_variants( - type_space, - &self.tag_type, - &self.variants, - TypeSpaceImpl::Display, - None, - ) - .then_some(TypeEntryEnumImpl::UntaggedDisplay), - untagged_newtype_string(type_space, &self.tag_type, &self.variants) - .then_some(TypeEntryEnumImpl::UntaggedFromStringIrrefutable), - ] - .into_iter() - .flatten() - .collect(); - } -} + let typ = type_enum.build().unwrap(); -impl Variant { - pub(crate) fn new( - raw_name: String, - description: Option, - details: VariantDetails, - ) -> Self { - Self { - raw_name, - ident_name: None, - description, - details, - } + TypeEntry::Type(typ) } } @@ -370,68 +193,77 @@ impl TypeEntryStruct { type_space: &TypeSpace, type_name: Name, metadata: &Option>, - properties: Vec, + properties: Vec>, deny_unknown_fields: bool, schema: Schema, ) -> TypeEntry { let name = get_type_name(&type_name, metadata).unwrap(); - let rename = None; let description = metadata_description(metadata); let default = metadata .as_ref() .and_then(|m| m.default.as_ref()) .cloned() - .map(WrappedValue::new); + .map(JsonValue::new); let type_patch = TypePatch::new(type_space, name); - let details = TypeEntryDetails::Struct(Self { - name: type_patch.name, - rename, - description, - default, - properties, - deny_unknown_fields, - schema: SchemaWrapper(schema), - }); - - TypeEntry { - details, - extra_derives: type_patch.derives, - extra_attrs: type_patch.attrs, + let mut type_struct = Struct::new() + .name(&type_patch.name) + .description(make_doc(&type_patch.name, description.as_ref(), &schema)) + .extra_derives(type_patch.derives) + .extra_attrs(type_patch.attrs) + .properties(properties); + if deny_unknown_fields { + type_struct = type_struct.deny_unknown_fields(); } + let mut typ = type_struct.build().unwrap(); + typ.set_default(default); + + TypeEntry::Type(typ) } } impl TypeEntryNewtype { - pub(crate) fn from_metadata( + fn make( type_space: &TypeSpace, type_name: Name, metadata: &Option>, type_id: TypeId, + constraints: NewtypeConstraints, schema: Schema, ) -> TypeEntry { let name = get_type_name(&type_name, metadata).unwrap(); - let rename = None; let description = metadata_description(metadata); let type_patch = TypePatch::new(type_space, name); - let details = TypeEntryDetails::Newtype(Self { - name: type_patch.name, - rename, - description, - default: None, - type_id, - constraints: TypeEntryNewtypeConstraints::None, - schema: SchemaWrapper(schema), - }); + let typ = NewtypeStruct::new(type_id) + .name(&type_patch.name) + .description(make_doc(&type_patch.name, description.as_ref(), &schema)) + .extra_derives(type_patch.derives) + .extra_attrs(type_patch.attrs) + .constraints(constraints) + .build() + .unwrap(); - TypeEntry { - details, - extra_derives: type_patch.derives, - extra_attrs: type_patch.attrs, - } + TypeEntry::Type(typ) + } + + pub(crate) fn from_metadata( + type_space: &TypeSpace, + type_name: Name, + metadata: &Option>, + type_id: TypeId, + schema: Schema, + ) -> TypeEntry { + Self::make( + type_space, + type_name, + metadata, + type_id, + NewtypeConstraints::None, + schema, + ) } pub(crate) fn from_metadata_with_enum_values( @@ -442,29 +274,17 @@ impl TypeEntryNewtype { enum_values: &[serde_json::Value], schema: Schema, ) -> TypeEntry { - let name = get_type_name(&type_name, metadata).unwrap(); - let rename = None; - let description = metadata_description(metadata); - - let type_patch = TypePatch::new(type_space, name); - - let details = TypeEntryDetails::Newtype(Self { - name: type_patch.name, - rename, - description, - default: None, + let constraints = NewtypeConstraints::AllowList( + enum_values.iter().cloned().map(JsonValue::new).collect(), + ); + Self::make( + type_space, + type_name, + metadata, type_id, - constraints: TypeEntryNewtypeConstraints::EnumValue( - enum_values.iter().cloned().map(WrappedValue::new).collect(), - ), - schema: SchemaWrapper(schema), - }); - - TypeEntry { - details, - extra_derives: type_patch.derives, - extra_attrs: type_patch.attrs, - } + constraints, + schema, + ) } pub(crate) fn from_metadata_with_deny_values( @@ -475,29 +295,16 @@ impl TypeEntryNewtype { enum_values: &[serde_json::Value], schema: Schema, ) -> TypeEntry { - let name = get_type_name(&type_name, metadata).unwrap(); - let rename = None; - let description = metadata_description(metadata); - - let type_patch = TypePatch::new(type_space, name); - - let details = TypeEntryDetails::Newtype(Self { - name: type_patch.name, - rename, - description, - default: None, + let constraints = + NewtypeConstraints::DenyList(enum_values.iter().cloned().map(JsonValue::new).collect()); + Self::make( + type_space, + type_name, + metadata, type_id, - constraints: TypeEntryNewtypeConstraints::DenyValue( - enum_values.iter().cloned().map(WrappedValue::new).collect(), - ), - schema: SchemaWrapper(schema), - }); - - TypeEntry { - details, - extra_derives: type_patch.derives, - extra_attrs: type_patch.attrs, - } + constraints, + schema, + ) } pub(crate) fn from_metadata_with_string_validation( @@ -508,1651 +315,102 @@ impl TypeEntryNewtype { validation: &schemars::schema::StringValidation, schema: Schema, ) -> TypeEntry { - let name = get_type_name(&type_name, metadata).unwrap(); - let rename = None; - let description = metadata_description(metadata); - let schemars::schema::StringValidation { max_length, min_length, pattern, } = validation.clone(); - let type_patch = TypePatch::new(type_space, name); - - let details = TypeEntryDetails::Newtype(Self { - name: type_patch.name, - rename, - description, - default: None, + Self::make( + type_space, + type_name, + metadata, type_id, - constraints: TypeEntryNewtypeConstraints::String { - max_length, - min_length, - pattern, + NewtypeConstraints::String { + max: max_length.map(|len| len as usize), + min: min_length.map(|len| len as usize), + patterns: pattern.into_iter().collect(), }, - schema: SchemaWrapper(schema), - }); - - TypeEntry { - details, - extra_derives: type_patch.derives, - extra_attrs: type_patch.attrs, - } + schema, + ) } } -impl From for TypeEntry { - fn from(details: TypeEntryDetails) -> Self { - Self { - details, - extra_derives: Default::default(), - extra_attrs: Default::default(), - } +impl From> for TypeEntry { + fn from(typ: Type) -> Self { + Self::Type(typ) } } impl TypeEntry { - pub(crate) fn new_native(type_name: S, impls: &[TypeSpaceImpl]) -> Self { - TypeEntry { - details: TypeEntryDetails::Native(TypeEntryNative { - type_name: type_name.to_string(), - impls: impls.to_vec(), - parameters: Default::default(), - }), - extra_derives: Default::default(), - extra_attrs: Default::default(), - } - } - pub(crate) fn new_native_params(type_name: S, params: &[TypeId]) -> Self { - TypeEntry { - details: TypeEntryDetails::Native(TypeEntryNative { - type_name: type_name.to_string(), - impls: Default::default(), - parameters: params.to_vec(), - }), - extra_derives: Default::default(), - extra_attrs: Default::default(), - } - } - pub(crate) fn new_boolean() -> Self { - TypeEntry { - details: TypeEntryDetails::Boolean, - extra_derives: Default::default(), - extra_attrs: Default::default(), - } - } - pub(crate) fn new_integer(type_name: S) -> Self { - TypeEntryDetails::Integer(type_name.to_string()).into() - } - pub(crate) fn new_float(type_name: S) -> Self { - TypeEntry { - details: TypeEntryDetails::Float(type_name.to_string()), - extra_derives: Default::default(), - extra_attrs: Default::default(), - } - } - - pub(crate) fn finalize(&mut self, type_space: &mut TypeSpace) -> Result<()> { - if let TypeEntryDetails::Enum(enum_details) = &mut self.details { - enum_details.finalize(type_space); - } - - self.check_defaults(type_space) - } - - pub(crate) fn name(&self) -> Option<&String> { - match &self.details { - TypeEntryDetails::Enum(TypeEntryEnum { name, .. }) - | TypeEntryDetails::Struct(TypeEntryStruct { name, .. }) - | TypeEntryDetails::Newtype(TypeEntryNewtype { name, .. }) => Some(name), - - _ => None, - } - } - - pub(crate) fn has_impl<'a>( - &'a self, - type_space: &'a TypeSpace, - impl_name: TypeSpaceImpl, - ) -> bool { - match &self.details { - TypeEntryDetails::Enum(details) => match impl_name { - TypeSpaceImpl::Default => details.default.is_some(), - TypeSpaceImpl::FromStr => { - details - .bespoke_impls - .contains(&TypeEntryEnumImpl::AllSimpleVariants) - || details - .bespoke_impls - .contains(&TypeEntryEnumImpl::UntaggedFromStr) - } - TypeSpaceImpl::Display => { - details - .bespoke_impls - .contains(&TypeEntryEnumImpl::AllSimpleVariants) - || details - .bespoke_impls - .contains(&TypeEntryEnumImpl::UntaggedDisplay) - } - TypeSpaceImpl::FromStringIrrefutable => details - .bespoke_impls - .contains(&TypeEntryEnumImpl::UntaggedFromStringIrrefutable), - }, - - TypeEntryDetails::Struct(details) => match impl_name { - TypeSpaceImpl::Default => details.default.is_some(), - _ => false, - }, - TypeEntryDetails::Newtype(details) => match (&details.constraints, impl_name) { - (_, TypeSpaceImpl::Default) => details.default.is_some(), - (TypeEntryNewtypeConstraints::String { .. }, TypeSpaceImpl::FromStr) => true, - (TypeEntryNewtypeConstraints::String { .. }, TypeSpaceImpl::Display) => true, - (TypeEntryNewtypeConstraints::None, _) => { - // TODO this is a lucky kludge that will need to be removed - // once we have proper handling of reference cycles (i.e. - // as opposed to containment cycles... which we **do** - // handle correctly). In particular output_newtype calls - // this to determine if it should produce a FromStr impl. - // This implementation could be infinitely recursive for a - // type such as this: - // struct A(Box); - // While this type is useless and unusable, we do-- - // basically--support and test this. On such a type, if one - // were to ask `ty.has_impl(TypeSpaceImpl::Default)` it - // would be infinitely recursive. Fortunately the type - // doesn't occur in the wild (we hope) and generation - // doesn't rely on that particular query. - - let type_entry = type_space.id_to_entry.get(&details.type_id).unwrap(); - type_entry.has_impl(type_space, impl_name) - } - _ => false, - }, - TypeEntryDetails::Native(details) => details.impls.contains(&impl_name), - TypeEntryDetails::Box(type_id) => { - if impl_name == TypeSpaceImpl::Default { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - type_entry.has_impl(type_space, impl_name) - } else { - false - } - } - - TypeEntryDetails::JsonValue => false, - - TypeEntryDetails::Unit - | TypeEntryDetails::Option(_) - | TypeEntryDetails::Vec(_) - | TypeEntryDetails::Map(_, _) - | TypeEntryDetails::Set(_) => { - matches!(impl_name, TypeSpaceImpl::Default) - } - - TypeEntryDetails::Tuple(type_ids) => { - // Default is implemented for tuples of up to 12 items long. - matches!(impl_name, TypeSpaceImpl::Default) - && type_ids.len() <= 12 - && type_ids.iter().all(|type_id| { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - type_entry.has_impl(type_space, TypeSpaceImpl::Default) - }) - } - - TypeEntryDetails::Array(item_id, length) => { - // Default is implemented for arrays of up to length 32. - if *length <= 32 && impl_name == TypeSpaceImpl::Default { - let type_entry = type_space.id_to_entry.get(item_id).unwrap(); - type_entry.has_impl(type_space, impl_name) - } else { - false - } - } - - TypeEntryDetails::Boolean => match impl_name { - TypeSpaceImpl::Default | TypeSpaceImpl::FromStr | TypeSpaceImpl::Display => true, - TypeSpaceImpl::FromStringIrrefutable => false, - }, - TypeEntryDetails::Integer(_) => match impl_name { - TypeSpaceImpl::Default | TypeSpaceImpl::FromStr | TypeSpaceImpl::Display => true, - TypeSpaceImpl::FromStringIrrefutable => false, - }, - - TypeEntryDetails::Float(_) => match impl_name { - TypeSpaceImpl::Default | TypeSpaceImpl::FromStr | TypeSpaceImpl::Display => true, - TypeSpaceImpl::FromStringIrrefutable => false, - }, - TypeEntryDetails::String => match impl_name { - TypeSpaceImpl::Default - | TypeSpaceImpl::FromStr - | TypeSpaceImpl::Display - | TypeSpaceImpl::FromStringIrrefutable => true, - }, - - TypeEntryDetails::Reference(_) => unreachable!(), - } + pub(crate) fn new_native( + type_name: S, + traits: typespace::TypespaceTraitSet, + ) -> Self { + Type::Native(Native::new(&type_name.to_string(), traits, Vec::new())).into() } - pub(crate) fn output(&self, type_space: &TypeSpace, output: &mut OutputSpace) { - let derive_set = [ - "::serde::Serialize", - "::serde::Deserialize", - "Debug", - "Clone", + pub(crate) fn new_native_params(type_name: S, params: &[TypeId]) -> Self { + let traits = [ + typespace::TypespaceTrait::Clone, + typespace::TypespaceTrait::Debug, + typespace::TypespaceTrait::Serialize, + typespace::TypespaceTrait::Deserialize, ] .into_iter() - .collect::>(); - - match &self.details { - TypeEntryDetails::Enum(enum_details) => { - self.output_enum(type_space, output, enum_details, derive_set) - } - TypeEntryDetails::Struct(struct_details) => { - self.output_struct(type_space, output, struct_details, derive_set) - } - TypeEntryDetails::Newtype(newtype_details) => { - self.output_newtype(type_space, output, newtype_details, derive_set) - } + .collect::(); - // We should never get here as reference types should only be used - // in-flight, but never recorded into the type space. - TypeEntryDetails::Reference(_) => unreachable!(), - - // Unnamed types require no definition as they're already defined. - _ => (), - } + Type::Native(Native::new(&type_name.to_string(), traits, params.to_vec())).into() } - fn output_enum( - &self, - type_space: &TypeSpace, - output: &mut OutputSpace, - enum_details: &TypeEntryEnum, - mut derive_set: BTreeSet<&str>, - ) { - let TypeEntryEnum { - name, - rename, - description, - default, - tag_type, - variants, - deny_unknown_fields, - bespoke_impls, - schema: _, - } = enum_details; - - let doc = make_doc(name, description.as_ref()); - - // TODO this is a one-off for some useful traits; this should move into - // the creation of the enum type. - if variants - .iter() - .all(|variant| matches!(variant.details, VariantDetails::Simple)) - { - derive_set.extend(["Copy", "PartialOrd", "Ord", "PartialEq", "Eq", "Hash"]); - } - - let mut serde_options = Vec::new(); - if let Some(old_name) = rename { - serde_options.push(quote! { rename = #old_name }); - } - match tag_type { - EnumTagType::External => {} - EnumTagType::Internal { tag } => { - serde_options.push(quote! { tag = #tag }); - } - EnumTagType::Adjacent { tag, content } => { - serde_options.push(quote! { tag = #tag }); - serde_options.push(quote! { content = #content }); - } - EnumTagType::Untagged => { - serde_options.push(quote! { untagged }); - } - } - if *deny_unknown_fields { - serde_options.push(quote! { deny_unknown_fields }); - } - - let serde = (!serde_options.is_empty()).then(|| { - quote! { #[serde( #( #serde_options ),* )] } - }); - - let type_name = format_ident!("{}", name); - - let variants_decl = variants - .iter() - .map(|variant| output_variant(variant, type_space, output, name)) - .collect::>(); - - // It should not be possible to construct an untagged enum - // with more than one simple variant--it would not be usable. - if tag_type == &EnumTagType::Untagged { - assert!( - variants - .iter() - .filter(|variant| matches!(variant.details, VariantDetails::Simple)) - .count() - <= 1 - ) - } - - // Display and FromStr impls for enums that are made exclusively of - // simple variants (and are not untagged). - let simple_enum_impl = bespoke_impls - .contains(&TypeEntryEnumImpl::AllSimpleVariants) - .then(|| { - let (match_variants, match_strs): (Vec<_>, Vec<_>) = variants - .iter() - .map(|variant| { - let ident_name = variant.ident_name.as_ref().unwrap(); - let variant_name = format_ident!("{}", ident_name); - (variant_name, &variant.raw_name) - }) - .unzip(); - - quote! { - impl ::std::fmt::Display for #type_name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - match *self { - #(Self::#match_variants => f.write_str(#match_strs),)* - } - } - } - impl ::std::str::FromStr for #type_name { - type Err = self::error::ConversionError; - - fn from_str(value: &str) -> - ::std::result::Result - { - match value { - #(#match_strs => Ok(Self::#match_variants),)* - _ => Err("invalid value".into()), - } - } - } - impl ::std::convert::TryFrom<&str> for #type_name { - type Error = self::error::ConversionError; - - fn try_from(value: &str) -> - ::std::result::Result - { - value.parse() - } - } - impl ::std::convert::TryFrom<::std::string::String> for #type_name { - type Error = self::error::ConversionError; - - fn try_from(value: ::std::string::String) -> - ::std::result::Result - { - value.parse() - } - } - } - }); - - let default_impl = default.as_ref().map(|value| { - let default_stream = self.output_value(type_space, &value.0, "e! {}).unwrap(); - quote! { - impl ::std::default::Default for #type_name { - fn default() -> Self { - #default_stream - } - } - } - }); - - let untagged_newtype_from_string_impl = bespoke_impls - .contains(&TypeEntryEnumImpl::UntaggedFromStr) - .then(|| { - let variant_name = variants - .iter() - .map(|variant| format_ident!("{}", variant.ident_name.as_ref().unwrap())); - - quote! { - impl ::std::str::FromStr for #type_name { - type Err = self::error::ConversionError; - - fn from_str(value: &str) -> - ::std::result::Result - { - #( - // Try to parse() into each variant. - if let Ok(v) = value.parse() { - Ok(Self::#variant_name(v)) - } else - )* - { - Err("string conversion failed for all variants".into()) - } - } - } - impl ::std::convert::TryFrom<&str> for #type_name { - type Error = self::error::ConversionError; - - fn try_from(value: &str) -> - ::std::result::Result - { - value.parse() - } - } - impl ::std::convert::TryFrom<::std::string::String> for #type_name { - type Error = self::error::ConversionError; - - fn try_from(value: ::std::string::String) -> - ::std::result::Result - { - value.parse() - } - } - } - }); - - let untagged_newtype_to_string_impl = bespoke_impls - .contains(&TypeEntryEnumImpl::UntaggedDisplay) - .then(|| { - let variant_name = variants - .iter() - .map(|variant| format_ident!("{}", variant.ident_name.as_ref().unwrap())); - - quote! { - impl ::std::fmt::Display for #type_name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - match self { - #(Self::#variant_name(x) => x.fmt(f),)* - } - } - } - } - }); - - let convenience_from = { - // Build a map whose key is the type ID or type IDs of the Item and - // Tuple variants, and whose value is a tuple of the original index - // and the variant itself. Any key that is seen multiple times has - // a value of None. - // TODO this requires more consideration to handle single-item - // tuples. - let unique_variants = - variants - .iter() - .enumerate() - .fold(BTreeMap::new(), |mut map, (index, variant)| { - let key = match &variant.details { - VariantDetails::Item(type_id) => vec![type_id], - VariantDetails::Tuple(type_ids) => type_ids.iter().collect(), - _ => return map, - }; - - map.entry(key) - .and_modify(|v| *v = None) - .or_insert(Some((index, variant))); - map - }); - - // Remove any variants that are duplicates (i.e. the value is None) - // with the flatten(). Then order a new map according to the - // original order of variants. The allows for the order to be - // stable and for impl blocks to appear in the same order as their - // corresponding variants. - let ordered_variants = unique_variants - .into_values() - .flatten() - .collect::>(); - - // Generate a `From` impl block that converts the type - // into the appropriate variant of the enum. - let variant_from = ordered_variants.into_values().map(|variant| { - match &variant.details { - VariantDetails::Item(type_id) => { - let variant_type = type_space.id_to_entry.get(type_id).unwrap(); - - // TODO Strings might conflict with the way we're - // dealing with TryFrom right now. - (variant_type.details != TypeEntryDetails::String).then(|| { - let variant_type_ident = variant_type.type_ident(type_space, &None); - let variant_name = - format_ident!("{}", variant.ident_name.as_ref().unwrap()); - quote! { - impl ::std::convert::From<#variant_type_ident> for #type_name { - fn from(value: #variant_type_ident) - -> Self - { - Self::#variant_name(value) - } - } - } - }) - } - VariantDetails::Tuple(type_ids) => { - let variant_type_idents = type_ids.iter().map(|type_id| { - type_space - .id_to_entry - .get(type_id) - .unwrap() - .type_ident(type_space, &None) - }); - let variant_type_ident = if type_ids.len() != 1 { - quote! { ( #(#variant_type_idents),* ) } - } else { - // A single-item tuple requires a trailing - // comma. - quote! { ( #(#variant_type_idents,)* ) } - }; - let variant_name = - format_ident!("{}", variant.ident_name.as_ref().unwrap()); - let ii = (0..type_ids.len()).map(syn::Index::from); - Some(quote! { - impl ::std::convert::From<#variant_type_ident> for #type_name { - fn from(value: #variant_type_ident) -> Self { - Self::#variant_name( - #( value.#ii, )* - ) - } - } - }) - } - _ => None, - } - }); - - quote! { - #( #variant_from )* - } - }; - - let derives = strings_to_derives( - derive_set, - &self.extra_derives, - &type_space.settings.extra_derives, - ); - - let attrs = strings_to_attrs(&self.extra_attrs, &type_space.settings.extra_attrs); - - let item = quote! { - #doc - #(#attrs)* - #[derive(#(#derives),*)] - #serde - pub enum #type_name { - #(#variants_decl)* - } - - #simple_enum_impl - #default_impl - #untagged_newtype_to_string_impl - #untagged_newtype_from_string_impl - #convenience_from - }; - output.add_item(OutputSpaceMod::Crate, name, item); - } - - fn output_struct( - &self, - type_space: &TypeSpace, - output: &mut OutputSpace, - struct_details: &TypeEntryStruct, - mut derive_set: BTreeSet<&str>, - ) { - enum PropDefault { - None(String), - Default(TokenStream), - Custom(TokenStream), - } - - let TypeEntryStruct { - name, - rename, - description, - default, - properties, - deny_unknown_fields, - schema: _, - } = struct_details; - let doc = make_doc(name, description.as_ref()); - - // Generate the serde directives as needed. - let mut serde_options = Vec::new(); - if let Some(old_name) = rename { - serde_options.push(quote! { rename = #old_name }); - } - if *deny_unknown_fields { - serde_options.push(quote! { deny_unknown_fields }); - } - let serde = - (!serde_options.is_empty()).then(|| quote! { #[serde( #( #serde_options ),* )] }); - - let type_name = format_ident!("{}", name); - - // Gather the various components for all properties. - let mut prop_doc = Vec::new(); - let mut prop_serde = Vec::new(); - let mut prop_default = Vec::new(); - let mut prop_name = Vec::new(); - let mut prop_error = Vec::new(); - let mut prop_type = Vec::new(); - let mut prop_type_scoped = Vec::new(); - - properties.iter().for_each(|prop| { - prop_doc.push(prop.description.as_ref().map(|d| quote! { #[doc = #d] })); - prop_name.push(format_ident!("{}", prop.name)); - prop_error.push(format!( - "error converting supplied value for {}: {{e}}", - prop.name, - )); - - let prop_type_entry = type_space.id_to_entry.get(&prop.type_id).unwrap(); - prop_type.push(prop_type_entry.type_ident(type_space, &None)); - prop_type_scoped - .push(prop_type_entry.type_ident(type_space, &Some("super".to_string()))); - - let (serde, default_fn) = generate_serde_attr( - name, - &prop.name, - &prop.rename, - &prop.state, - prop_type_entry, - type_space, - output, - ); - - prop_serde.push(serde); - prop_default.push(match default_fn { - DefaultFunction::Default => PropDefault::Default(quote! { - Default::default() - }), - DefaultFunction::Custom(fn_name) => { - let default_fn = syn::parse_str::(&fn_name).unwrap(); - PropDefault::Custom(quote! { - #default_fn() - }) - } - DefaultFunction::None => { - let err_msg = format!("no value supplied for {}", prop.name); - PropDefault::None(err_msg) - } - }); - }); - - // If there's no whole-type default value and every property's default - // is the intrinsic `Default::default()`, the hand-written `impl Default` - // would be exactly what `#[derive(Default)]` produces (and would trip - // clippy's `derivable_impls` lint downstream). In that case we derive - // `Default` rather than emitting the manual impl below. - let derive_default = default.is_none() - && prop_default - .iter() - .all(|pd| matches!(pd, PropDefault::Default(_))); - if derive_default { - derive_set.insert("Default"); - } - - let derives = strings_to_derives( - derive_set, - &self.extra_derives, - &type_space.settings.extra_derives, - ); - - let attrs = strings_to_attrs(&self.extra_attrs, &type_space.settings.extra_attrs); - - output.add_item( - OutputSpaceMod::Crate, - name, - quote! { - #doc - #(#attrs)* - #[derive(#(#derives),*)] - #serde - pub struct #type_name { - #( - #prop_doc - #prop_serde - pub #prop_name: #prop_type, - )* - } - }, - ); - - // If there's a default value, generate an impl Default - if let Some(value) = default { - let default_stream = self.output_value(type_space, &value.0, "e! {}).unwrap(); - output.add_item( - OutputSpaceMod::Crate, - name, - quote! { - impl ::std::default::Default for #type_name { - fn default() -> Self { - #default_stream - } - } - }, - ); - } else if derive_default { - // Handled above via `#[derive(Default)]`. - } else if let Some(prop_default) = prop_default - .iter() - .map(|pd| match pd { - PropDefault::None(_) => None, - PropDefault::Default(token_stream) | PropDefault::Custom(token_stream) => { - Some(token_stream) - } - }) - .collect::>>() - { - // If all properties have a default, we can generate a Default impl - output.add_item( - OutputSpaceMod::Crate, - name, - quote! { - impl ::std::default::Default for #type_name { - fn default() -> Self { - Self { - #( - #prop_name: #prop_default, - )* - } - } - } - }, - ) - } - - if type_space.settings.struct_builder { - output.add_item( - OutputSpaceMod::Crate, - name, - quote! { - impl #type_name { - pub fn builder() -> builder::#type_name { - Default::default() - } - } - }, - ); - - // If there are no properties, all of this is kind of pointless, - // but at least this lets us avoid the lint warning. - let value_ident = if prop_name.is_empty() { - quote! { _value } - } else { - quote! { value } - }; - - let prop_default = prop_default.iter().map(|pd| match pd { - PropDefault::None(err_msg) => quote! { Err(#err_msg.to_string()) }, - PropDefault::Default(default_fn) => quote! { Ok(#default_fn) }, - PropDefault::Custom(custom_fn) => quote! { Ok(super::#custom_fn) }, - }); - - output.add_item( - OutputSpaceMod::Builder, - name, - quote! { - #[derive(Clone, Debug)] - pub struct #type_name { - #( - #prop_name: ::std::result::Result<#prop_type_scoped, ::std::string::String>, - )* - } - - impl ::std::default::Default for #type_name { - fn default() -> Self { - Self { - #( - #prop_name: #prop_default, - )* - } - } - } - - impl #type_name { - #( - pub fn #prop_name(mut self, value: T) -> Self - where - T: ::std::convert::TryInto<#prop_type_scoped>, - T::Error: ::std::fmt::Display, - { - self.#prop_name = value.try_into() - .map_err(|e| format!(#prop_error)); - self - } - )* - } - - // This is how the item is built. - impl ::std::convert::TryFrom<#type_name> - for super::#type_name - { - type Error = super::error::ConversionError; - - fn try_from(#value_ident: #type_name) - -> ::std::result::Result - { - Ok(Self { - #( - #prop_name: value.#prop_name?, - )* - }) - } - } - - // Construct a builder from the item. - impl ::std::convert::From for #type_name { - fn from(#value_ident: super::#type_name) -> Self { - Self { - #( - #prop_name: Ok(value.#prop_name), - )* - } - } - } - }, - ); - } + pub(crate) fn new_boolean() -> Self { + Type::Boolean.into() } - - fn output_newtype<'a>( - &self, - type_space: &'a TypeSpace, - output: &mut OutputSpace, - newtype_details: &TypeEntryNewtype, - mut derive_set: BTreeSet<&'a str>, - ) { - let TypeEntryNewtype { - name, - rename: _, - description, - default, - type_id, - constraints, - schema: _, - } = newtype_details; - let doc = make_doc(name, description.as_ref()); - - let type_name = format_ident!("{}", name); - let inner_type = type_space.id_to_entry.get(type_id).unwrap(); - let inner_type_name = inner_type.type_ident(type_space, &None); - - let is_str = matches!(inner_type.details, TypeEntryDetails::String); - - // If this is just a wrapper around a string, we can derive some more - // useful traits. - if is_str { - derive_set.extend(["PartialOrd", "Ord", "PartialEq", "Eq", "Hash"]); - } - - derive_set.extend(type_space.settings.extra_derives.iter().map(|s| s.as_str())); - - let constraint_impl = match constraints { - // In the unconstrained case we proxy impls through the inner type. - TypeEntryNewtypeConstraints::None => { - let str_impl = is_str.then(|| { - quote! { - impl ::std::str::FromStr for #type_name { - type Err = ::std::convert::Infallible; - - fn from_str(value: &str) -> - ::std::result::Result - { - Ok(Self(value.to_string())) - } - } - } - }); - - // TODO see the comment in has_impl related to this case. - let from_str_impl = (inner_type.has_impl(type_space, TypeSpaceImpl::FromStr) - && !is_str) - .then(|| { - quote! { - impl ::std::str::FromStr for #type_name { - type Err = <#inner_type_name as - ::std::str::FromStr>::Err; - - fn from_str(value: &str) -> - ::std::result::Result - { - Ok(Self(value.parse()?)) - } - } - } - }); - - let display_impl = inner_type - .has_impl(type_space, TypeSpaceImpl::Display) - .then(|| { - quote! { - impl ::std::fmt::Display for #type_name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - self.0.fmt(f) - } - } - } - }); - - quote! { - impl ::std::convert::From<#inner_type_name> for #type_name { - fn from(value: #inner_type_name) -> Self { - Self(value) - } - } - - #display_impl - #str_impl - #from_str_impl - } - } - - TypeEntryNewtypeConstraints::DenyValue(enum_values) - | TypeEntryNewtypeConstraints::EnumValue(enum_values) => { - // Note that string types with enumerated values are converted - // into simple enums rather than newtypes so we would not - // expect to see a string as the inner type here. - assert!( - matches!(constraints, TypeEntryNewtypeConstraints::DenyValue(_)) - || !matches!(&inner_type.details, TypeEntryDetails::String) - ); - - // We're going to impl Deserialize so we can remove it - // from the set of derived impls. - derive_set.remove("::serde::Deserialize"); - - let value_output = enum_values - .iter() - .map(|value| inner_type.output_value(type_space, &value.0, "e! {})); - - let value_string = enum_values - .iter() - .map(|value| serde_json::to_string(&value.0).unwrap()); - - // As with Deserialize, serde::JsonSchema requires a custom - // impl. If it's present in the set of derives, remove it and - // generate something that accurately models the type. - - let has_json_schema = derive_set.remove("schemars::JsonSchema") - || derive_set.remove("::schemars::JsonSchema"); - let json_schema = has_json_schema.then(|| match constraints { - TypeEntryNewtypeConstraints::DenyValue(_) => quote! { - impl ::schemars::JsonSchema for #type_name { - fn schema_name() -> ::std::string::String { - #name.to_string() - } - - fn json_schema(g: &mut ::schemars::r#gen::SchemaGenerator) - -> ::schemars::schema::Schema { - let mut schema = - <#inner_type_name as ::schemars::JsonSchema> - ::json_schema(g) - .into_object(); - let not = ::schemars::schema::SchemaObject { - enum_values: ::std::option::Option::Some([ - #( ::serde_json::from_str(#value_string).unwrap(), )* - ].into_iter().collect()), - ..::std::default::Default::default() - }; - schema.subschemas().not = Some( - ::std::boxed::Box::new(not.into()) - ); - schema.into() - } - } - }, - TypeEntryNewtypeConstraints::EnumValue(_) => quote! { - impl ::schemars::JsonSchema for #type_name { - fn schema_name() -> ::std::string::String { - #name.to_string() - } - - fn json_schema(g: &mut ::schemars::r#gen::SchemaGenerator) - -> ::schemars::schema::Schema { - let mut schema = - <#inner_type_name as ::schemars::JsonSchema> - ::json_schema(g) - .into_object(); - schema.enum_values = ::std::option::Option::Some([ - #( ::serde_json::from_str(#value_string).unwrap(), )* - ].into_iter().collect()); - schema.into() - } - } - }, - - _ => unreachable!(), - }); - - // TODO if the sub_type is a string we could probably impl - // TryFrom<&str> as well and FromStr. - - let not = matches!(constraints, TypeEntryNewtypeConstraints::EnumValue(_)) - .then(|| quote! { ! }); - - quote! { - // This is effectively the constructor for this type. - impl ::std::convert::TryFrom<#inner_type_name> for #type_name { - type Error = self::error::ConversionError; - - fn try_from( - value: #inner_type_name - ) -> ::std::result::Result - { - if #not [ - #(#value_output,)* - ].contains(&value) { - Err("invalid value".into()) - } else { - Ok(Self(value)) - } - } - } - - impl<'de> ::serde::Deserialize<'de> for #type_name { - fn deserialize( - deserializer: D, - ) -> ::std::result::Result - where - D: ::serde::Deserializer<'de>, - { - Self::try_from( - <#inner_type_name>::deserialize(deserializer)?, - ) - .map_err(|e| { - ::custom( - e.to_string(), - ) - }) - } - } - - #json_schema - } - } - - TypeEntryNewtypeConstraints::String { - max_length, - min_length, - pattern, - } => { - let max = max_length.map(|v| { - let v = v as usize; - let err = format!("longer than {} characters", v); - quote! { - if value.chars().count() > #v { - return Err(#err.into()); - } - } - }); - let min = min_length.map(|v| { - let v = v as usize; - let err = format!("shorter than {} characters", v); - quote! { - if value.chars().count() < #v { - return Err(#err.into()); - } - } - }); - - let pat = pattern.as_ref().map(|p| { - let err = format!("doesn't match pattern \"{}\"", p); - quote! { - static PATTERN: ::std::sync::LazyLock<::regress::Regex> = ::std::sync::LazyLock::new(|| { - ::regress::Regex::new(#p).unwrap() - }); - if PATTERN.find(value).is_none() { - return Err(#err.into()); - } - } - }); - - // We're going to impl Deserialize so we can remove it - // from the set of derived impls. - derive_set.remove("::serde::Deserialize"); - - // TODO: if a user were to derive schemars::JsonSchema, it - // wouldn't be accurate. - // The conversion work lives in TryFrom<&str>: the validation - // asserts what the type is, so it sits with the - // assertion-flavored conversions, and FromStr, - // TryFrom, and Deserialize all delegate to it. - quote! { - impl ::std::str::FromStr for #type_name { - type Err = self::error::ConversionError; - - fn from_str(value: &str) -> ::std::result::Result { - ::std::convert::TryFrom::try_from(value) - } - } - impl ::std::convert::TryFrom<&str> for #type_name { - type Error = self::error::ConversionError; - - fn try_from(value: &str) -> - ::std::result::Result - { - #max - #min - #pat - - Ok(Self(value.to_string())) - } - } - impl ::std::convert::TryFrom<::std::string::String> for #type_name { - type Error = self::error::ConversionError; - - fn try_from(value: ::std::string::String) -> - ::std::result::Result - { - ::std::convert::TryFrom::try_from(value.as_str()) - } - } - - impl<'de> ::serde::Deserialize<'de> for #type_name { - fn deserialize( - deserializer: D, - ) -> ::std::result::Result - where - D: ::serde::Deserializer<'de>, - { - ::std::convert::TryFrom::try_from( - ::std::string::String::deserialize(deserializer)?, - ) - .map_err(|e: self::error::ConversionError| { - ::custom( - e.to_string(), - ) - }) - } - } - } - } - }; - - // If there are no constraints, let consumers directly access the value. - let vis = match constraints { - TypeEntryNewtypeConstraints::None => Some(quote! {pub}), - _ => None, - }; - - let default_impl = default.as_ref().map(|value| { - let default_stream = self.output_value(type_space, &value.0, "e! {}).unwrap(); - quote! { - impl ::std::default::Default for #type_name { - fn default() -> Self { - #default_stream - } - } - } - }); - - // This isn't the cleanest. Unlike other types, we roll in the - // extra_derives here so that we can sniff out and override uses of - // "schemars::JsonSchema". - let derives = strings_to_derives(derive_set, &self.extra_derives, &[]); - - let attrs = strings_to_attrs(&self.extra_attrs, &type_space.settings.extra_attrs); - - let item = quote! { - #doc - #(#attrs)* - #[derive(#(#derives),*)] - #[serde(transparent)] - pub struct #type_name(#vis #inner_type_name); - - impl ::std::ops::Deref for #type_name { - type Target = #inner_type_name; - fn deref(&self) -> &#inner_type_name { - &self.0 - } - } - - impl ::std::convert::From<#type_name> for #inner_type_name { - fn from(value: #type_name) -> Self { - value.0 - } - } - - #default_impl - #constraint_impl - }; - output.add_item(OutputSpaceMod::Crate, name, item); + pub(crate) fn new_integer(type_name: S) -> Self { + Type::Integer(type_name.to_string()).into() } - - pub(crate) fn type_name(&self, type_space: &TypeSpace) -> String { - self.type_ident(type_space, &None).to_string() + pub(crate) fn new_float(type_name: S) -> Self { + Type::Float(type_name.to_string()).into() } - pub(crate) fn type_ident( - &self, - type_space: &TypeSpace, - type_mod: &Option, - ) -> TokenStream { - match &self.details { - // Named types. - TypeEntryDetails::Enum(TypeEntryEnum { name, .. }) - | TypeEntryDetails::Struct(TypeEntryStruct { name, .. }) - | TypeEntryDetails::Newtype(TypeEntryNewtype { name, .. }) => match &type_mod { - Some(type_mod) => { - let type_mod = format_ident!("{}", type_mod); - let type_name = format_ident!("{}", name); - quote! { #type_mod :: #type_name } - } - None => { - let type_name = format_ident!("{}", name); - quote! { #type_name } - } - }, - - TypeEntryDetails::Option(id) => { - let inner_ty = type_space - .id_to_entry - .get(id) - .expect("unresolved type id for option"); - let inner_ident = inner_ty.type_ident(type_space, type_mod); - - // Flatten nested Option types. This would only happen if the - // schema encoded it; it's an odd construction. - match &inner_ty.details { - TypeEntryDetails::Option(_) => inner_ident, - _ => quote! { ::std::option::Option<#inner_ident> }, - } - } - - TypeEntryDetails::Box(id) => { - let inner_ty = type_space - .id_to_entry - .get(id) - .expect("unresolved type id for box"); - - let item = inner_ty.type_ident(type_space, type_mod); - - quote! { ::std::boxed::Box<#item> } - } - - TypeEntryDetails::Vec(id) => { - let inner_ty = type_space - .id_to_entry - .get(id) - .expect("unresolved type id for array"); - let item = inner_ty.type_ident(type_space, type_mod); - - quote! { ::std::vec::Vec<#item> } - } - - TypeEntryDetails::Map(key_id, value_id) => { - let map_to_use = &type_space.settings.map_type; - let key_ty = type_space - .id_to_entry - .get(key_id) - .expect("unresolved type id for map key"); - let value_ty = type_space - .id_to_entry - .get(value_id) - .expect("unresolved type id for map value"); - - if key_ty.details == TypeEntryDetails::String - && value_ty.details == TypeEntryDetails::JsonValue - { - quote! { - ::serde_json::Map<::std::string::String, ::serde_json::Value> - } - } else { - let key_ident = key_ty.type_ident(type_space, type_mod); - let value_ident = value_ty.type_ident(type_space, type_mod); - let map_to_use = &map_to_use.0; - - quote! { #map_to_use<#key_ident, #value_ident> } - } - } - - TypeEntryDetails::Set(id) => { - let inner_ty = type_space - .id_to_entry - .get(id) - .expect("unresolved type id for set"); - let item = inner_ty.type_ident(type_space, type_mod); - // TODO we'll want this to be a Set of some kind, but we need - // to get the derives right first. - quote! { ::std::vec::Vec<#item> } - } - - TypeEntryDetails::Tuple(items) => { - let type_idents = items.iter().map(|item| { - type_space - .id_to_entry - .get(item) - .expect("unresolved type id for tuple") - .type_ident(type_space, type_mod) - }); - - if items.len() != 1 { - quote! { ( #(#type_idents),* ) } - } else { - // A single-item tuple requires a trailing comma. - quote! { ( #(#type_idents,)* ) } - } - } - - TypeEntryDetails::Array(item_id, length) => { - let item_ty = type_space - .id_to_entry - .get(item_id) - .expect("unresolved type id for array"); - let item_ident = item_ty.type_ident(type_space, type_mod); - - quote! { [#item_ident; #length]} - } - - TypeEntryDetails::Native(TypeEntryNative { - type_name, - impls: _, - parameters, - }) => { - let path = - syn::parse_str::(type_name).expect("type path wasn't valid"); - - let type_idents = (!parameters.is_empty()).then(|| { - let type_idents = parameters.iter().map(|type_id| { - type_space - .id_to_entry - .get(type_id) - .expect("unresolved type id for tuple") - .type_ident(type_space, type_mod) - }); - quote! { < #(#type_idents,)* > } - }); - - quote! { - #path - #type_idents - } - } - - TypeEntryDetails::Unit => quote! { () }, - TypeEntryDetails::String => quote! { ::std::string::String }, - TypeEntryDetails::Boolean => quote! { bool }, - TypeEntryDetails::JsonValue => quote! { ::serde_json::Value }, - TypeEntryDetails::Integer(name) | TypeEntryDetails::Float(name) => { - syn::parse_str::(name) - .unwrap() - .to_token_stream() - } - - TypeEntryDetails::Reference(_) => panic!("references should be resolved by now"), + /// The typespace type stored in this entry. + /// + /// # Panics + /// + /// Panics on a reference entry; references are unwrapped by + /// `assign_type` before storage so entries fetched from the type + /// space are always concrete types. + pub(crate) fn as_type(&self) -> &Type { + match self { + Self::Type(typ) => typ, + Self::Reference(_) => panic!("references should be resolved by now"), } } - pub(crate) fn type_parameter_ident( - &self, - type_space: &TypeSpace, - lifetime_name: Option<&str>, - ) -> TokenStream { - let lifetime = lifetime_name.map(|s| { - vec![ - TokenTree::from(Punct::new('\'', Spacing::Joint)), - TokenTree::from(format_ident!("{}", s)), - ] - .into_iter() - .collect::() - }); - match &self.details { - // We special-case enums for which all variants are simple to let - // them be passed as values rather than as references. - // TODO we should probably cache "simpleness" of all variants - // rather than iterating every time. We'll know it when the enum is - // constructed. - TypeEntryDetails::Enum(TypeEntryEnum { variants, .. }) - if variants - .iter() - .all(|variant| matches!(&variant.details, VariantDetails::Simple)) => - { - self.type_ident(type_space, &type_space.settings.type_mod) - } - TypeEntryDetails::Enum(_) - | TypeEntryDetails::Struct(_) - | TypeEntryDetails::Newtype(_) - | TypeEntryDetails::Vec(_) - | TypeEntryDetails::Map(..) - | TypeEntryDetails::Set(_) - | TypeEntryDetails::Box(_) - | TypeEntryDetails::Native(_) - | TypeEntryDetails::Array(..) - | TypeEntryDetails::JsonValue => { - let ident = self.type_ident(type_space, &type_space.settings.type_mod); - quote! { - & #lifetime #ident - } - } - - TypeEntryDetails::Option(id) => { - let inner_ty = type_space - .id_to_entry - .get(id) - .expect("unresolved type id for option"); - let inner_ident = inner_ty.type_parameter_ident(type_space, lifetime_name); - - // Flatten nested Option types. This would only happen if the - // schema encoded it; it's an odd construction. - match &inner_ty.details { - TypeEntryDetails::Option(_) => inner_ident, - _ => quote! { Option<#inner_ident> }, - } - } - - TypeEntryDetails::Tuple(items) => { - let type_streams = items.iter().map(|item| { - type_space - .id_to_entry - .get(item) - .expect("unresolved type id for tuple") - .type_parameter_ident(type_space, lifetime_name) - }); - - if items.len() != 1 { - quote! { ( #(#type_streams),* ) } - } else { - // Single-element tuples require special handling. In - // particular, they must have a trailing comma or else are - // treated as extraneously parenthesized types. - quote! { ( #(#type_streams,)* ) } - } - } - - TypeEntryDetails::Unit - | TypeEntryDetails::Boolean - | TypeEntryDetails::Integer(_) - | TypeEntryDetails::Float(_) => { - self.type_ident(type_space, &type_space.settings.type_mod) - } - TypeEntryDetails::String => quote! { & #lifetime str }, - - TypeEntryDetails::Reference(_) => panic!("references should be resolved by now"), + pub(crate) fn as_type_mut(&mut self) -> Option<&mut Type> { + match self { + Self::Type(typ) => Some(typ), + Self::Reference(_) => None, } } - pub(crate) fn describe(&self) -> String { - match &self.details { - TypeEntryDetails::Enum(TypeEntryEnum { name, .. }) => format!("enum {}", name), - TypeEntryDetails::Struct(TypeEntryStruct { name, .. }) => format!("struct {}", name), - TypeEntryDetails::Newtype(TypeEntryNewtype { name, type_id, .. }) => { - format!("newtype {} {}", name, type_id.0) - } - - TypeEntryDetails::Unit => "()".to_string(), - TypeEntryDetails::Option(type_id) => format!("option {}", type_id.0), - TypeEntryDetails::Vec(type_id) => format!("vec {}", type_id.0), - TypeEntryDetails::Map(key_id, value_id) => { - format!("map {} {}", key_id.0, value_id.0) - } - TypeEntryDetails::Set(type_id) => format!("set {}", type_id.0), - TypeEntryDetails::Box(type_id) => format!("box {}", type_id.0), - TypeEntryDetails::Tuple(type_ids) => { - format!( - "tuple ({})", - type_ids - .iter() - .map(|type_id| type_id.0.to_string()) - .collect::>() - .join(", ") - ) - } - TypeEntryDetails::Array(type_id, length) => { - format!("array {}; {}", type_id.0, length) - } - TypeEntryDetails::Boolean => "bool".to_string(), - TypeEntryDetails::Native(TypeEntryNative { - type_name: name, .. - }) - | TypeEntryDetails::Integer(name) - | TypeEntryDetails::Float(name) => name.clone(), - TypeEntryDetails::String => "string".to_string(), - - TypeEntryDetails::JsonValue => "json value".to_string(), - - TypeEntryDetails::Reference(_) => unreachable!(), + pub(crate) fn name(&self) -> Option<&str> { + match self { + Self::Type(typ) => typ.name(), + Self::Reference(_) => None, } } } -fn make_doc(name: &str, description: Option<&String>) -> TokenStream { - let desc = match description { - Some(desc) => desc, - None => &format!("`{}`", name), - }; - - quote! { #[doc = #desc] } -} - -fn strings_to_derives<'a>( - derive_set: BTreeSet<&'a str>, - type_derives: &'a BTreeSet, - extra_derives: &'a [String], -) -> impl Iterator + 'a { - let mut combined_derives = derive_set.clone(); - combined_derives.extend(extra_derives.iter().map(String::as_str)); - combined_derives.extend(type_derives.iter().map(String::as_str)); - combined_derives.into_iter().map(|derive| { - syn::parse_str::(derive) - .unwrap() - .into_token_stream() - }) -} - -fn strings_to_attrs<'a>( - type_attrs: &'a BTreeSet, - extra_attrs: &'a [String], -) -> impl Iterator + 'a { - let mut combined_attrs = BTreeSet::new(); - combined_attrs.extend(extra_attrs.iter().map(String::as_str)); - combined_attrs.extend(type_attrs.iter().map(String::as_str)); - combined_attrs - .into_iter() - .map(|attr| attr.parse::().unwrap()) -} - -/// Returns true iff... -/// - the enum is untagged -/// - all variants are single items (aka newtype variants) -/// - the type of the newtype variant implements the required trait -fn untagged_newtype_variants( - type_space: &TypeSpace, - tag_type: &EnumTagType, - variants: &[Variant], - req_impl: TypeSpaceImpl, - neg_impl: Option, -) -> bool { - tag_type == &EnumTagType::Untagged - && variants.iter().all(|variant| { - // If the variant is a single item... - match &variant.details { - VariantDetails::Item(type_id) => Some(type_id), - _ => None, - } - .map_or_else( - || false, - |type_id| { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - // ... and its type has the required impl - type_entry.has_impl(type_space, req_impl) - && neg_impl - .is_none_or(|neg_impl| !type_entry.has_impl(type_space, neg_impl)) - }, - ) - }) -} - -/// Returns true iff... -/// - the enum is untagged -/// - **any** variant is a single items **and** it is irrefutably a string -fn untagged_newtype_string( - type_space: &TypeSpace, - tag_type: &EnumTagType, - variants: &[Variant], -) -> bool { - tag_type == &EnumTagType::Untagged - && variants.iter().any(|variant| { - // If the variant is a single item... - match &variant.details { - VariantDetails::Item(type_id) => Some(type_id), - _ => None, - } - .map_or_else( - || false, - |type_id| { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - // ... and it is irrefutably a string - type_entry.has_impl(type_space, TypeSpaceImpl::FromStringIrrefutable) - }, - ) - }) -} - -#[cfg(test)] -mod tests { - use crate::{ - type_entry::{SchemaWrapper, TypeEntry, TypeEntryStruct}, - TypeEntryDetails, TypeSpace, - }; - - #[test] - fn test_ident() { - let ts = TypeSpace::default(); - - let type_mod = Some("the_mod".to_string()); - - let t = TypeEntry::new_integer("u32"); - let ident = t.type_ident(&ts, &type_mod); - assert_eq!(ident.to_string(), "u32"); - let parameter = t.type_parameter_ident(&ts, None); - assert_eq!(parameter.to_string(), "u32"); - - let t = TypeEntry::from(TypeEntryDetails::String); - let ident = t.type_ident(&ts, &type_mod); - assert_eq!(ident.to_string(), ":: std :: string :: String"); - let parameter = t.type_parameter_ident(&ts, None); - assert_eq!(parameter.to_string(), "& str"); - let parameter = t.type_parameter_ident(&ts, Some("static")); - assert_eq!(parameter.to_string(), "& 'static str"); - - let t = TypeEntry::from(TypeEntryDetails::Unit); - let ident = t.type_ident(&ts, &type_mod); - assert_eq!(ident.to_string(), "()"); - let parameter = t.type_parameter_ident(&ts, None); - assert_eq!(parameter.to_string(), "()"); - - let t = TypeEntry::from(TypeEntryDetails::Struct(TypeEntryStruct { - name: "SomeType".to_string(), - rename: None, - description: None, - default: None, - properties: vec![], - deny_unknown_fields: false, - schema: SchemaWrapper(schemars::schema::Schema::Bool(false)), - })); - - let ident = t.type_ident(&ts, &type_mod); - assert_eq!(ident.to_string(), "the_mod :: SomeType"); - let parameter = t.type_parameter_ident(&ts, None); - assert_eq!(parameter.to_string(), "& SomeType"); - let parameter = t.type_parameter_ident(&ts, Some("a")); - assert_eq!(parameter.to_string(), "& 'a SomeType"); +/// A named type's description: the schema's, or `` `Name` `` when the +/// schema has none. +/// +/// The schema itself is unused. It arrives through the converter's +/// `original_schema` parameters, which on this branch serve nothing +/// else; removing it means removing that thread. +fn make_doc(name: &str, description: Option<&String>, _schema: &Schema) -> String { + match description { + Some(desc) => desc.clone(), + None => format!("`{}`", name), } } diff --git a/typify-impl/src/util.rs b/typify-impl/src/util.rs index 9880b91b..747041c0 100644 --- a/typify-impl/src/util.rs +++ b/typify-impl/src/util.rs @@ -877,18 +877,11 @@ impl TypePatch { derives: Default::default(), attrs: Default::default(), }, - - Some(patch) => { - let name = patch.rename.clone().unwrap_or(type_name); - let derives = patch.derives.iter().cloned().collect(); - let attrs = patch.attrs.iter().cloned().collect(); - - Self { - name, - derives, - attrs, - } - } + Some(patch) => Self { + name: patch.rename.clone().unwrap_or(type_name), + derives: patch.derives.iter().cloned().collect(), + attrs: patch.attrs.iter().cloned().collect(), + }, } } } @@ -902,7 +895,7 @@ pub(crate) struct StringValidator { impl StringValidator { pub fn new(type_name: &Name, validation: Option<&StringValidation>) -> Result { let (max_length, min_length, pattern) = - validation.map_or(Ok((None, None, None)), |validation| { + validation.map_or(Ok::<_, Error>((None, None, None)), |validation| { let max = validation.max_length; let min = validation.min_length; let pattern = validation diff --git a/typify-impl/src/value.rs b/typify-impl/src/value.rs deleted file mode 100644 index 742708b9..00000000 --- a/typify-impl/src/value.rs +++ /dev/null @@ -1,1191 +0,0 @@ -// Copyright 2025 Oxide Computer Company - -use std::{collections::BTreeMap, str::FromStr}; - -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; - -use crate::{ - convert::STD_NUM_NONZERO_PREFIX, - type_entry::{ - EnumTagType, StructProperty, StructPropertyRename, TypeEntry, TypeEntryDetails, - TypeEntryEnum, TypeEntryNative, TypeEntryNewtype, TypeEntryStruct, Variant, VariantDetails, - }, - TypeId, TypeSpace, -}; - -impl TypeEntry { - /// Emit a [`TokenStream`] for the given [`Value`] - /// - /// This returns an Option for programming convenience, but we do not - /// expect it to fail. All validation should already have been done by - /// [`validate_default()`]. - /// - /// [`Value`]: serde_json::Value - pub fn output_value( - &self, - type_space: &TypeSpace, - value: &serde_json::Value, - scope: &TokenStream, - ) -> Option { - let v = match &self.details { - TypeEntryDetails::Enum(TypeEntryEnum { - name, - tag_type, - variants, - .. - }) => match tag_type { - EnumTagType::External => { - value_for_external_enum(type_space, name, variants, value, scope)? - } - EnumTagType::Internal { tag } => { - value_for_internal_enum(type_space, name, variants, value, tag, scope)? - } - EnumTagType::Adjacent { tag, content } => { - value_for_adjacent_enum(type_space, name, variants, value, tag, content, scope)? - } - EnumTagType::Untagged => { - value_for_untagged_enum(type_space, name, variants, value, scope)? - } - }, - TypeEntryDetails::Struct(TypeEntryStruct { - name, properties, .. - }) => { - let props = value_for_struct_props(properties, value, type_space, scope)?; - let ident = format_ident!("{}", name); - quote! { #scope #ident { #( #props ),* }} - } - TypeEntryDetails::Newtype(TypeEntryNewtype { name, type_id, .. }) => { - let inner = type_space - .id_to_entry - .get(type_id) - .unwrap() - .output_value(type_space, value, scope); - let ident = format_ident!("{}", name); - quote! { #scope #ident ( #inner )} - } - - TypeEntryDetails::Option(type_id) => { - if let serde_json::Value::Null = value { - quote! { ::std::option::Option::None } - } else { - let inner = type_space - .id_to_entry - .get(type_id) - .unwrap() - .output_value(type_space, value, scope)?; - quote! { ::std::option::Option::Some(#inner) } - } - } - TypeEntryDetails::Box(type_id) => { - let inner = type_space - .id_to_entry - .get(type_id) - .unwrap() - .output_value(type_space, value, scope)?; - quote! { ::std::boxed::Box::new(#inner) } - } - // TODO: this should become a HashSet<_> once we figure out the - // derives more precisely. - TypeEntryDetails::Set(type_id) | TypeEntryDetails::Vec(type_id) => { - let arr = value.as_array()?; - let inner = type_space.id_to_entry.get(type_id).unwrap(); - let values = arr - .iter() - .map(|arr_value| inner.output_value(type_space, arr_value, scope)) - .collect::>>()?; - quote! { vec![#(#values),*] } - } - TypeEntryDetails::Map(key_id, value_id) => { - let obj = value.as_object()?; - let key_ty = type_space.id_to_entry.get(key_id).unwrap(); - let value_ty = type_space.id_to_entry.get(value_id).unwrap(); - let kvs = obj - .iter() - .map(|(obj_key, obj_value)| { - Some(( - key_ty.output_value( - type_space, - &serde_json::Value::String(obj_key.clone()), - scope, - )?, - value_ty.output_value(type_space, obj_value, scope)?, - )) - }) - .collect::>>()?; - let (keys, values): (Vec<_>, Vec<_>) = kvs.into_iter().unzip(); - quote! { - [#( (#keys, #values) ),*] - .into_iter() - .collect() - } - } - TypeEntryDetails::Tuple(types) => { - let tup = value_for_tuple(type_space, value, types, scope)?; - quote! { ( #( #tup ),* )} - } - TypeEntryDetails::Array(type_id, _) => { - let arr = value.as_array()?; - let inner = type_space.id_to_entry.get(type_id).unwrap(); - let values = arr - .iter() - .map(|arr_value| inner.output_value(type_space, arr_value, scope)) - .collect::>>()?; - quote! { [#(#values),*] } - } - TypeEntryDetails::Unit => { - value.as_null()?; - quote! { () } - } - TypeEntryDetails::Native(TypeEntryNative { type_name, .. }) => { - // Serialize value to a string... not hard. - let text = value.to_string(); - let type_path = syn::parse_str::(type_name).unwrap(); - - // Deserialize the string to the type; the runtime expect() is - // unfortunate, but unavoidable without getting in the - // underpants of the serialized form of these built-in types. - quote! { - ::serde_json::from_str::< #type_path >(#text) - .expect("invalid default provided") - } - } - TypeEntryDetails::JsonValue => { - let text = value.to_string(); - quote! { - ::serde_json::from_str::<::serde_json::Value>(#text) - .expect("invalid default provided") - } - } - TypeEntryDetails::Boolean => { - let v = value.as_bool()?; - quote! { #v } - } - TypeEntryDetails::Integer(type_name) | TypeEntryDetails::Float(type_name) => { - if !value.is_number() { - return None; - } - if type_name.starts_with(STD_NUM_NONZERO_PREFIX) { - let type_path = syn::parse_str::(type_name).unwrap(); - let num = proc_macro2::Literal::from_str(value.to_string().as_str()).unwrap(); - - quote! { - #type_path::new(#num).unwrap() - } - } else { - let val = - match proc_macro2::Literal::from_str(&format!("{}_{}", value, type_name)) { - Ok(v) => v, - Err(_) => unreachable!(), - }; - TokenStream::from(proc_macro2::TokenTree::from(val)) - } - } - TypeEntryDetails::String => { - let s = value.as_str()?; - quote! { #s.to_string() } - } - TypeEntryDetails::Reference(_) => unreachable!(), - }; - Some(v) - } -} - -fn value_for_external_enum( - type_space: &TypeSpace, - type_name: &str, - variants: &[Variant], - value: &serde_json::Value, - scope: &TokenStream, -) -> Option { - if let Some(simple_name) = value.as_str() { - let variant = variants - .iter() - .find(|variant| simple_name == variant.raw_name)?; - matches!(&variant.details, VariantDetails::Simple).then(|| ())?; - - let var_ident = format_ident!("{}", &variant.ident_name.as_ref().unwrap()); - let type_ident = format_ident!("{}", type_name); - Some(quote! { #scope #type_ident::#var_ident }) - } else { - let map = value.as_object()?; - if map.len() != 1 { - return None; - } - - let (name, var_value) = map.iter().next()?; - - let variant = variants.iter().find(|variant| name == &variant.raw_name)?; - - let var_ident = format_ident!("{}", &variant.ident_name.as_ref().unwrap()); - let type_ident = format_ident!("{}", type_name); - match &variant.details { - VariantDetails::Simple => None, - VariantDetails::Item(type_id) => { - let item = value_for_item(type_space, var_value, type_id, scope); - Some(quote! { #scope #type_ident::#var_ident ( #item ) }) - } - VariantDetails::Tuple(types) => { - let tup = value_for_tuple(type_space, var_value, types, scope)?; - Some(quote! { #scope #type_ident::#var_ident ( #( #tup ),* ) }) - } - VariantDetails::Struct(props) => { - let props = value_for_struct_props(props, var_value, type_space, scope)?; - Some(quote! { #scope #type_ident::#var_ident { #( #props ),* } }) - } - } - } -} - -fn value_for_internal_enum( - type_space: &TypeSpace, - type_name: &str, - variants: &[Variant], - value: &serde_json::Value, - tag: &str, - scope: &TokenStream, -) -> Option { - let map = value.as_object()?; - let ser_name = map.get(tag).and_then(serde_json::Value::as_str)?; - let variant = variants - .iter() - .find(|variant| ser_name == variant.raw_name)?; - let var_ident = format_ident!("{}", &variant.ident_name.as_ref().unwrap()); - let type_ident = format_ident!("{}", type_name); - - match &variant.details { - VariantDetails::Simple => Some(quote! { #scope #type_ident::#var_ident }), - VariantDetails::Struct(props) => { - // Make an object without the tag. - let inner_value = serde_json::Value::Object( - map.clone() - .into_iter() - .filter(|(name, _)| name != tag) - .collect(), - ); - - let props = value_for_struct_props(props, &inner_value, type_space, scope)?; - Some(quote! { #scope #type_ident::#var_ident { #( #props ),* } }) - } - - VariantDetails::Item(_) | VariantDetails::Tuple(_) => unreachable!(), - } -} - -fn value_for_adjacent_enum( - type_space: &TypeSpace, - type_name: &str, - variants: &[Variant], - value: &serde_json::Value, - tag: &str, - content: &str, - scope: &TokenStream, -) -> Option { - let map = value.as_object()?; - - let (tag_value, content_value) = match ( - map.len(), - map.get(tag).and_then(serde_json::Value::as_str), - map.get(content), - ) { - (1, Some(tag_value), None) => (tag_value, None), - (2, Some(tag_value), content_value @ Some(_)) => (tag_value, content_value), - _ => return None, - }; - - let variant = variants - .iter() - .find(|variant| tag_value == variant.raw_name)?; - let type_ident = format_ident!("{}", type_name); - let var_ident = format_ident!("{}", &variant.ident_name.as_ref().unwrap()); - match (&variant.details, content_value) { - (VariantDetails::Simple, None) => Some(quote! { #scope #type_ident::#var_ident}), - (VariantDetails::Tuple(types), Some(content_value)) => { - let tup = value_for_tuple(type_space, content_value, types, scope)?; - Some(quote! { #scope #type_ident::#var_ident ( #( #tup ),* ) }) - } - (VariantDetails::Struct(props), Some(content_value)) => { - let props = value_for_struct_props(props, content_value, type_space, scope)?; - Some(quote! { #scope #type_ident::#var_ident { #( #props ),* } }) - } - _ => None, - } -} - -fn value_for_untagged_enum( - type_space: &TypeSpace, - type_name: &str, - variants: &[Variant], - value: &serde_json::Value, - scope: &TokenStream, -) -> Option { - let type_ident = format_ident!("{}", type_name); - variants.iter().find_map(|variant| { - let var_ident = format_ident!("{}", &variant.ident_name.as_ref().unwrap()); - match &variant.details { - VariantDetails::Simple => { - value.as_null()?; - Some(quote! { #scope #type_ident::#var_ident }) - } - VariantDetails::Item(type_id) => { - let item = value_for_item(type_space, value, type_id, scope)?; - Some(quote! { #scope #type_ident::#var_ident ( #item ) }) - } - VariantDetails::Tuple(types) => { - let tup = value_for_tuple(type_space, value, types, scope)?; - Some(quote! { #scope #type_ident::#var_ident ( #( #tup ),* ) }) - } - VariantDetails::Struct(props) => { - let props = value_for_struct_props(props, value, type_space, scope)?; - Some(quote! { #scope #type_ident::#var_ident { #( #props ),* } }) - } - } - }) -} - -fn value_for_item( - type_space: &TypeSpace, - value: &serde_json::Value, - type_id: &TypeId, - scope: &TokenStream, -) -> Option { - type_space - .id_to_entry - .get(type_id) - .unwrap() - .output_value(type_space, value, scope) -} - -fn value_for_tuple( - type_space: &TypeSpace, - value: &serde_json::Value, - types: &[TypeId], - scope: &TokenStream, -) -> Option> { - let arr = value.as_array()?; - if arr.len() != types.len() { - return None; - } - types - .iter() - .zip(arr) - .map(|(type_id, tup_value)| { - type_space - .id_to_entry - .get(type_id) - .unwrap() - .output_value(type_space, tup_value, scope) - }) - .collect() -} - -fn value_for_struct_props( - properties: &[StructProperty], - value: &serde_json::Value, - type_space: &TypeSpace, - scope: &TokenStream, -) -> Option> { - let map = value.as_object()?; - - let direct_props = properties.iter().filter_map(|prop| { - let name = match &prop.rename { - StructPropertyRename::None => &prop.name, - StructPropertyRename::Rename(rename) => rename, - StructPropertyRename::Flatten => return None, - }; - - let name_ident = format_ident!("{}", &prop.name); - - if let Some(value) = map.get(name) { - let type_entry = type_space.id_to_entry.get(&prop.type_id).unwrap(); - let prop_value = type_entry.output_value(type_space, value, scope)?; - - Some(quote! { #name_ident: #prop_value }) - } else { - Some(quote! { #name_ident: Default::default() }) - } - }); - - let prop_map = properties - .iter() - .filter_map(|prop| { - let name = match &prop.rename { - StructPropertyRename::None => &prop.name, - StructPropertyRename::Rename(rename) => rename, - StructPropertyRename::Flatten => return None, - }; - - Some((name, prop)) - }) - .collect::>(); - - let extra_value = serde_json::Value::Object( - map.clone() - .into_iter() - .filter(|(name, _)| !prop_map.contains_key(name)) - .collect(), - ); - - let flat_props = properties.iter().filter_map(|prop| match &prop.rename { - StructPropertyRename::Flatten => { - let type_entry = type_space.id_to_entry.get(&prop.type_id).unwrap(); - - // The flattened type must be a struct, map or option for a struct. - match &type_entry.details { - TypeEntryDetails::Struct(_) - | TypeEntryDetails::Option(_) - | TypeEntryDetails::Map(..) => (), - _ => unreachable!(), - } - - let flat_value = type_entry.output_value(type_space, &extra_value, scope)?; - let name_ident = format_ident!("{}", &prop.name); - Some(quote! { #name_ident: #flat_value }) - } - _ => None, - }); - - Some(direct_props.chain(flat_props).collect()) -} - -#[cfg(test)] -mod tests { - use quote::quote; - use schemars::JsonSchema; - use serde_json::json; - use std::collections::HashMap; - use uuid::Uuid; - - use crate::{test_util::get_type, type_entry::TypeEntry}; - - #[test] - fn test_value_option() { - let (type_space, type_id) = get_type::>(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!(null), "e! {}) - .map(|x| x.to_string()), - Some(":: std :: option :: Option :: None".to_string()), - ); - assert_eq!( - type_entry - .output_value(&type_space, &json!(42), "e! {}) - .map(|x| x.to_string()), - Some(":: std :: option :: Option :: Some (42_u32)".to_string()), - ); - } - - #[test] - fn test_value_box() { - let (type_space, type_id) = get_type::>(); - - let type_entry = TypeEntry { - details: crate::type_entry::TypeEntryDetails::Box(type_id), - extra_derives: Default::default(), - extra_attrs: Default::default(), - }; - - assert_eq!( - type_entry - .output_value(&type_space, &json!(null), "e! {}) - .map(|x| x.to_string()), - Some(":: std :: boxed :: Box :: new (:: std :: option :: Option :: None)".to_string()), - ); - assert_eq!( - type_entry - .output_value(&type_space, &json!(42), "e! {}) - .map(|x| x.to_string()), - Some( - ":: std :: boxed :: Box :: new (:: std :: option :: Option :: Some (42_u32))" - .to_string() - ), - ); - } - - #[test] - fn test_value_array() { - let (type_space, type_id) = get_type::>(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!([]), "e! {}) - .map(|x| x.to_string()), - Some("vec ! []".to_string()), - ); - assert_eq!( - type_entry - .output_value(&type_space, &json!([1, 2, 5]), "e! {}) - .map(|x| x.to_string()), - Some("vec ! [1_u32 , 2_u32 , 5_u32]".to_string()), - ); - } - - #[test] - fn test_value_map() { - let (type_space, type_id) = get_type::>(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!({}), "e! {}) - .map(|x| x.to_string()), - Some("[] . into_iter () . collect ()".to_string()), - ); - assert_eq!( - type_entry - .output_value(&type_space, &json!({"a": 1, "b": 2}), "e! {}) - .map(|x| x.to_string()), - Some(r#"[("a" . to_string () , 1_u32) , ("b" . to_string () , 2_u32)] . into_iter () . collect ()"#.to_string()), - ); - } - - #[test] - fn test_value_tuple() { - let (type_space, type_id) = get_type::<(u32, u32, String)>(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!([1, 2, "three"]), "e! {}) - .map(|x| x.to_string()), - Some(r#"(1_u32 , 2_u32 , "three" . to_string ())"#.to_string()), - ); - } - - #[test] - fn test_value_builtin() { - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!("not-a-uuid"), "e! {}) - .map(|x| x.to_string()), - Some( - quote! { - ::serde_json::from_str::<::uuid::Uuid>("\"not-a-uuid\"") - .expect("invalid default provided") - } - .to_string() - ), - ); - } - - #[test] - fn test_value_bool() { - let (type_space, type_id) = get_type::>(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!(true), "e! {}) - .map(|x| x.to_string()), - Some(":: std :: option :: Option :: Some (true)".to_string()), - ); - assert_eq!( - type_entry - .output_value(&type_space, &json!(false), "e! {}) - .map(|x| x.to_string()), - Some(":: std :: option :: Option :: Some (false)".to_string()), - ); - assert_eq!( - type_entry - .output_value(&type_space, &json!(null), "e! {}) - .map(|x| x.to_string()), - Some(":: std :: option :: Option :: None".to_string()), - ); - } - - #[test] - fn test_value_numbers_and_string() { - let (type_space, type_id) = get_type::<(u32, i64, f64, String)>(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!([0, 0, 0, "zero"]), "e! {}) - .map(|x| x.to_string()), - Some(r#"(0_u32 , 0_i64 , 0_f64 , "zero" . to_string ())"#.to_string()), - ); - } - - #[test] - fn test_struct_simple_super_scoped() { - #[derive(JsonSchema)] - #[allow(dead_code)] - struct Test { - a: String, - b: u32, - c: Option, - d: Option, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value( - &type_space, - &json!( - { - "a": "aaaa", - "b": 7, - "c": "cccc" - } - ), - "e! {super::} - ) - .map(|x| x.to_string()), - Some( - quote! { - super::Test { - a: "aaaa".to_string(), - b: 7_u32, - c: ::std::option::Option::Some("cccc".to_string()), - d: Default::default() - } - } - .to_string() - ) - ); - } - - #[test] - fn test_struct_simple() { - #[derive(JsonSchema)] - #[allow(dead_code)] - struct Test { - a: String, - b: u32, - c: Option, - d: Option, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value( - &type_space, - &json!( - { - "a": "aaaa", - "b": 7, - "c": "cccc" - } - ), - "e! {} - ) - .map(|x| x.to_string()), - Some( - quote! { - Test { - a: "aaaa".to_string(), - b: 7_u32, - c: ::std::option::Option::Some("cccc".to_string()), - d: Default::default() - } - } - .to_string() - ) - ); - } - #[test] - fn test_enum_external_super_scoped() { - #[derive(JsonSchema)] - #[allow(dead_code)] - enum Test { - A, - B(String, String), - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!("A"), "e! { super:: }) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::A - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "B": ["xx", "yy"] - }), - "e! { super:: } - ) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::B("xx".to_string(), "yy".to_string()) - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "C": { "cc": "xx", "dd": "yy" } - }), - "e! { super:: } - ) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::C { - cc: "xx".to_string(), - dd: "yy".to_string() - } - } - .to_string() - ) - ); - } - - #[test] - fn test_enum_external() { - #[derive(JsonSchema)] - #[allow(dead_code)] - enum Test { - A, - B(String, String), - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!("A"), "e! {}) - .map(|x| x.to_string()), - Some( - quote! { - Test::A - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "B": ["xx", "yy"] - }), - "e! {} - ) - .map(|x| x.to_string()), - Some( - quote! { - Test::B("xx".to_string(), "yy".to_string()) - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "C": { "cc": "xx", "dd": "yy" } - }), - "e! {} - ) - .map(|x| x.to_string()), - Some( - quote! { - Test::C { - cc: "xx".to_string(), - dd: "yy".to_string() - } - } - .to_string() - ) - ); - } - - #[test] - fn test_enum_internal_super_scoped() { - #[derive(JsonSchema)] - #[allow(dead_code)] - #[serde(tag = "tag")] - enum Test { - A, - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "tag": "A" - }), - "e! { super:: } - ) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::A - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "tag": "C", - "cc": "xx", - "dd": "yy" - }), - "e! { super:: } - ) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::C { - cc: "xx".to_string(), - dd: "yy".to_string() - } - } - .to_string() - ) - ); - } - - #[test] - fn test_enum_internal() { - #[derive(JsonSchema)] - #[allow(dead_code)] - #[serde(tag = "tag")] - enum Test { - A, - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "tag": "A" - }), - "e! {} - ) - .map(|x| x.to_string()), - Some( - quote! { - Test::A - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "tag": "C", - "cc": "xx", - "dd": "yy" - }), - "e! {} - ) - .map(|x| x.to_string()), - Some( - quote! { - Test::C { - cc: "xx".to_string(), - dd: "yy".to_string() - } - } - .to_string() - ) - ); - } - - #[test] - fn test_enum_adjacent_super_scoped() { - #[derive(JsonSchema)] - #[allow(dead_code)] - #[serde(tag = "tag", content = "content")] - enum Test { - A, - B(String, String), - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "tag": "A" - }), - "e! { super:: } - ) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::A - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "tag": "B", - "content": ["xx", "yy"] - }), - "e! { super:: } - ) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::B("xx".to_string(), "yy".to_string()) - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "tag": "C", - "content": { "cc": "xx", "dd": "yy" } - }), - "e! { super:: } - ) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::C { - cc: "xx".to_string(), - dd: "yy".to_string() - } - } - .to_string() - ) - ); - } - - #[test] - fn test_enum_adjacent() { - #[derive(JsonSchema)] - #[allow(dead_code)] - #[serde(tag = "tag", content = "content")] - enum Test { - A, - B(String, String), - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "tag": "A" - }), - "e! {} - ) - .map(|x| x.to_string()), - Some( - quote! { - Test::A - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "tag": "B", - "content": ["xx", "yy"] - }), - "e! {} - ) - .map(|x| x.to_string()), - Some( - quote! { - Test::B("xx".to_string(), "yy".to_string()) - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!({ - "tag": "C", - "content": { "cc": "xx", "dd": "yy" } - }), - "e! {} - ) - .map(|x| x.to_string()), - Some( - quote! { - Test::C { - cc: "xx".to_string(), - dd: "yy".to_string() - } - } - .to_string() - ) - ); - } - - #[test] - fn test_enum_untagged_super_scoped() { - #[derive(JsonSchema)] - #[allow(dead_code)] - #[serde(untagged)] - enum Test { - A, - B(String, String), - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!(null), "e! { super:: }) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::Null - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value(&type_space, &json!(["xx", "yy"]), "e! { super:: }) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::Array("xx".to_string(), "yy".to_string()) - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!( - { "cc": "xx", "dd": "yy" } - ), - "e! { super:: } - ) - .map(|x| x.to_string()), - Some( - quote! { - super::Test::Object { - cc: "xx".to_string(), - dd: "yy".to_string() - } - } - .to_string() - ) - ); - } - - #[test] - fn test_enum_untagged() { - #[derive(JsonSchema)] - #[allow(dead_code)] - #[serde(untagged)] - enum Test { - A, - B(String, String), - C { cc: String, dd: String }, - } - - let (type_space, type_id) = get_type::(); - let type_entry = type_space.id_to_entry.get(&type_id).unwrap(); - - assert_eq!( - type_entry - .output_value(&type_space, &json!(null), "e! {}) - .map(|x| x.to_string()), - Some( - quote! { - Test::Null - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value(&type_space, &json!(["xx", "yy"]), "e! {}) - .map(|x| x.to_string()), - Some( - quote! { - Test::Array("xx".to_string(), "yy".to_string()) - } - .to_string() - ) - ); - assert_eq!( - type_entry - .output_value( - &type_space, - &json!( - { "cc": "xx", "dd": "yy" } - ), - "e! {} - ) - .map(|x| x.to_string()), - Some( - quote! { - Test::Object { - cc: "xx".to_string(), - dd: "yy".to_string() - } - } - .to_string() - ) - ); - } -} diff --git a/typify-impl/tests/generator.out b/typify-impl/tests/generator.out index 8fdca7ae..66879f49 100644 --- a/typify-impl/tests/generator.out +++ b/typify-impl/tests/generator.out @@ -7,10 +7,10 @@ mod types { Debug, Eq, Hash, - JsonSchema, Ord, PartialEq, PartialOrd, + schemars :: JsonSchema, )] pub struct AllTheTraits { pub ok: ::std::string::String, @@ -21,7 +21,9 @@ mod types { } } #[doc = "`CompoundType`"] - #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, JsonSchema)] + #[derive( + :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, schemars :: JsonSchema, + )] pub struct CompoundType { pub value1: ::std::string::String, pub value2: u64, @@ -32,7 +34,9 @@ mod types { } } #[doc = "`Pair`"] - #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, JsonSchema)] + #[derive( + :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, schemars :: JsonSchema, + )] pub struct Pair { #[serde(default = "defaults::pair_a")] pub a: StringEnum, @@ -61,10 +65,10 @@ mod types { Debug, Eq, Hash, - JsonSchema, Ord, PartialEq, PartialOrd, + schemars :: JsonSchema, )] pub enum StringEnum { One, @@ -289,7 +293,7 @@ mod types { pub fn do_stuff( body: &types::CompoundType, string: &str, - opt_int: Option, + opt_int: ::std::option::Option, strenum: types::StringEnum, ) -> types::CompoundType { todo!() 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")); +} diff --git a/typify-impl/tests/test_generation.rs b/typify-impl/tests/test_generation.rs index ca4ad92b..b668abfc 100644 --- a/typify-impl/tests/test_generation.rs +++ b/typify-impl/tests/test_generation.rs @@ -3,6 +3,7 @@ use quote::quote; use schemars::{r#gen::SchemaGenerator, schema::Schema, JsonSchema}; use serde::Serialize; +use typify_impl::typespace::TypespaceTrait; use typify_impl::{TypeSpace, TypeSpacePatch, TypeSpaceSettings}; #[allow(dead_code)] @@ -51,9 +52,10 @@ fn add_type(generator: &mut SchemaGenerator) -> Schema { fn test_generation() { let mut type_space = TypeSpace::new( TypeSpaceSettings::default() - .with_derive("JsonSchema".to_string()) - .with_type_mod("types") - .with_struct_builder(true) + .map_typespace_settings(|s| { + s.with_required_trait(TypespaceTrait::JsonSchema) + .with_struct_builder(true) + }) .with_patch( "AllTheTraits", TypeSpacePatch::default() @@ -78,20 +80,30 @@ fn test_generation() { .unwrap(); let tid = type_space.add_type(&body_schema).unwrap(); - let t = type_space.get_type(&tid).unwrap(); - let ret = t.ident(); - let body = t.parameter_ident(); - let string_id = type_space.add_type(&string_schema).unwrap(); - let string = type_space.get_type(&string_id).unwrap().parameter_ident(); let opt_int_id = type_space.add_type(&opt_int_schema).unwrap(); - let opt_int = type_space.get_type(&opt_int_id).unwrap().parameter_ident(); let strenum_id = type_space.add_type(&strenum_schema).unwrap(); - let strenum = type_space.get_type(&strenum_id).unwrap().parameter_ident(); let _ = type_space.add_type(&pair_schema).unwrap(); let _ = type_space.add_type(&all_the_traits).unwrap(); - let types = type_space.to_stream(); + // Identifier queries are typespace's job. The generated types land + // in the `types` module below, so ask for identifiers scoped to it + // (typify1 expressed the same thing with a type_mod setting). + let typespace = type_space.to_typespace().unwrap(); + let t = typespace.get_type(&tid); + let ret = t.ident_in("types"); + let body = t.parameter_ident(Some("types"), None); + let string = typespace + .get_type(&string_id) + .parameter_ident(Some("types"), None); + let opt_int = typespace + .get_type(&opt_int_id) + .parameter_ident(Some("types"), None); + let strenum = typespace + .get_type(&strenum_id) + .parameter_ident(Some("types"), None); + + let types = type_space.to_stream().unwrap(); let file = quote! { mod types { diff --git a/typify-impl/tests/test_github.rs b/typify-impl/tests/test_github.rs index dd148201..0c85428a 100644 --- a/typify-impl/tests/test_github.rs +++ b/typify-impl/tests/test_github.rs @@ -18,7 +18,7 @@ fn test_github() { type_space.add_root_schema(schema).unwrap(); - let file = type_space.to_stream(); + let file = type_space.to_stream().unwrap(); let fmt = rustfmt_wrapper::rustfmt(file.to_string()).unwrap(); @@ -78,7 +78,7 @@ fn test_vega() { type_space.add_root_schema(schema).unwrap(); - let file = type_space.to_stream(); + let file = type_space.to_stream().unwrap(); let fmt = rustfmt_wrapper::rustfmt(file.to_string()).unwrap(); diff --git a/typify-macro/Cargo.toml b/typify-macro/Cargo.toml index 408a663e..4958ae95 100644 --- a/typify-macro/Cargo.toml +++ b/typify-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typify-macro" -version = "0.8.0" +version = "0.10.0-alpha.2" edition = "2021" license = "Apache-2.0" description = "typify macro implementation" diff --git a/typify-macro/src/lib.rs b/typify-macro/src/lib.rs index fd3a2dbf..f723ea77 100644 --- a/typify-macro/src/lib.rs +++ b/typify-macro/src/lib.rs @@ -13,7 +13,8 @@ use serde_tokenstream::{ParseWrapper, TokenStreamWrapper}; use syn::LitStr; use token_utils::TypeAndImpls; use typify_impl::{ - CrateVers, MapType, TypeSpace, TypeSpacePatch, TypeSpaceSettings, UnknownPolicy, + typespace::settings::{ContainerType, TraitSpec}, + CrateVers, TypeSpace, TypeSpacePatch, TypeSpaceSettings, UnknownPolicy, }; mod token_utils; @@ -206,14 +207,37 @@ fn do_import_types(item: TokenStream) -> Result { map_type, attrs, } = serde_tokenstream::from_tokenstream(&item.into())?; + // A derive names a trait typespace models or a foreign one; typespace + // tells them apart, and a bad name is an error at the derive's span. + let derives = derives + .into_iter() + .map(|derive| { + let path = derive.into_inner(); + TraitSpec::parse(&path.to_token_stream().to_string()).map_err(|err| { + syn::Error::new(syn::spanned::Spanned::span(&path), err.to_string()) + }) + }) + .collect::, syn::Error>>()?; + let mut settings = TypeSpaceSettings::default(); - derives.into_iter().for_each(|derive| { - settings.with_derive(derive.to_token_stream().to_string()); - }); - attrs.into_iter().for_each(|attr| { - settings.with_attr(attr.to_token_stream().to_string()); + settings.map_typespace_settings(|mut typespace| { + typespace = typespace.with_struct_builder(struct_builder); + for derive in derives { + typespace = typespace.with_extra_required_trait(derive); + } + for attr in attrs { + typespace = typespace.with_attr(attr.to_token_stream().to_string()); + } + // A map key is always string-like, so any container the path names + // can probably carry the HashMap preset's obligations. + if let Some(map_type) = map_type { + typespace = typespace.with_map_type( + ContainerType::hash_map() + .with_path(&map_type.into_inner().to_token_stream().to_string()), + ); + } + typespace }); - settings.with_struct_builder(struct_builder); patch.into_iter().for_each(|(type_name, patch)| { settings.with_patch(type_name.to_token_stream(), &patch.into()); @@ -238,10 +262,6 @@ fn do_import_types(item: TokenStream) -> Result { ); settings.with_unknown_crates(unknown_crates); - if let Some(map_type) = map_type { - settings.with_map_type(MapType(map_type.into_inner())); - } - (schema.into_inner(), settings) }; @@ -271,9 +291,15 @@ fn do_import_types(item: TokenStream) -> Result { .add_root_schema(root_schema) .map_err(|e| into_syn_err(e, schema.span()))?; + // Rendering finalizes the type graph and can fail; report the + // error as a compile error at the macro invocation. + let types = type_space + .to_stream() + .map_err(|e| into_syn_err(e, schema.span()))?; + let path_str = path.to_string_lossy(); let output = quote! { - #type_space + #types // Force a rebuild when the given file is modified. const _: &str = include_str!(#path_str); diff --git a/typify-test/build.rs b/typify-test/build.rs index fafaaeb0..971ae7af 100644 --- a/typify-test/build.rs +++ b/typify-test/build.rs @@ -6,6 +6,7 @@ use std::{env, fs, path::Path}; use schemars::schema::Schema; use schemars::JsonSchema; use serde::Serialize; +use typify::typespace::settings::ContainerType; use typify::{TypeSpace, TypeSpaceSettings}; #[allow(dead_code)] @@ -179,7 +180,7 @@ fn main() { TriplePattern::add(&mut type_space); let contents = - prettyplease::unparse(&syn::parse2::(type_space.to_stream()).unwrap()); + prettyplease::unparse(&syn::parse2::(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"); @@ -191,21 +192,21 @@ fn main() { WithMap::add(&mut type_space); let contents = - prettyplease::unparse(&syn::parse2::(type_space.to_stream()).unwrap()); + prettyplease::unparse(&syn::parse2::(type_space.to_stream().unwrap()).unwrap()); let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf(); out_file.push("codegen_hashmap.rs"); fs::write(out_file, contents).unwrap(); // Generate with a custom map type to validate requirements. - let mut settings = TypeSpaceSettings::default(); - settings.with_map_type("CustomMap"); - let mut type_space = TypeSpace::new(&settings); + let mut type_space = TypeSpace::new(TypeSpaceSettings::default().map_typespace_settings(|s| { + s.with_map_type(ContainerType::hash_map().with_path("CustomMap")) + })); WithMap::add(&mut type_space); let contents = - prettyplease::unparse(&syn::parse2::(type_space.to_stream()).unwrap()); + prettyplease::unparse(&syn::parse2::(type_space.to_stream().unwrap()).unwrap()); let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf(); out_file.push("codegen_custommap.rs"); diff --git a/typify/Cargo.toml b/typify/Cargo.toml index 7315bbe2..3b2e6a13 100644 --- a/typify/Cargo.toml +++ b/typify/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typify" -version = "0.8.0" +version = "0.10.0-alpha.2" edition = "2021" license = "Apache-2.0" description = "JSON schema to rust type code generator" diff --git a/typify/src/lib.rs b/typify/src/lib.rs index 079162d2..a712b3a6 100644 --- a/typify/src/lib.rs +++ b/typify/src/lib.rs @@ -151,19 +151,16 @@ pub use typify_impl::accept_as_ident; pub use typify_impl::CrateVers; pub use typify_impl::Error; -pub use typify_impl::MapType; -pub use typify_impl::Type; -pub use typify_impl::TypeDetails; -pub use typify_impl::TypeEnum; -pub use typify_impl::TypeEnumVariant; pub use typify_impl::TypeId; -pub use typify_impl::TypeNewtype; pub use typify_impl::TypeSpace; pub use typify_impl::TypeSpaceImpl; pub use typify_impl::TypeSpacePatch; pub use typify_impl::TypeSpaceSettings; -pub use typify_impl::TypeStruct; -pub use typify_impl::TypeStructPropInfo; pub use typify_impl::UnknownPolicy; +// The typespace crate is the query and rendering surface for converted +// types: [`TypeSpace::to_typespace`] yields a `typespace::Typespace` +// whose view API answers the type queries (identifiers, structure, +// trait impls) that used to live on wrapper types in this crate. +pub use typify_impl::typespace; #[cfg(feature = "macro")] pub use typify_macro::import_types; diff --git a/typify/tests/schemas.rs b/typify/tests/schemas.rs index 6de0051b..780de23b 100644 --- a/typify/tests/schemas.rs +++ b/typify/tests/schemas.rs @@ -7,6 +7,8 @@ use glob::glob; use quote::quote; use schemars::schema::RootSchema; use serde_json::json; +use typify::typespace::settings::ContainerType; +use typify::typespace::TypespaceTrait; use typify::{TypeSpace, TypeSpacePatch, TypeSpaceSettings}; use typify_impl::TypeSpaceImpl; @@ -17,7 +19,8 @@ fn test_schemas() { for entry in glob("tests/schemas/*.json").expect("Failed to read glob pattern") { let entry = entry.unwrap(); let out_path = entry.clone().with_extension("rs"); - validate_schema(entry, out_path, &mut TypeSpaceSettings::default()).unwrap(); + validate_schema(entry.clone(), out_path, TypeSpaceSettings::default()) + .expect(&format!("failed for {}", entry.to_string_lossy())) } // Make sure it all compiles. @@ -27,10 +30,14 @@ fn test_schemas() { /// Ensure that setting the global config to use a custom map type works. #[test] fn test_custom_map() { + let mut settings = TypeSpaceSettings::default(); + settings.map_typespace_settings(|s| { + s.with_map_type(ContainerType::btree_map().with_path("std::collections::BTreeMap")) + }); validate_schema( "tests/schemas/maps.json".into(), "tests/schemas/maps_custom.rs".into(), - TypeSpaceSettings::default().with_map_type("std::collections::BTreeMap"), + settings, ) .unwrap(); @@ -41,10 +48,12 @@ fn test_custom_map() { /// implementation. #[test] fn test_various_enums_json_schema() { + let mut settings = TypeSpaceSettings::default(); + settings.map_typespace_settings(|s| s.with_required_trait(TypespaceTrait::JsonSchema)); validate_schema( "tests/schemas/various-enums.json".into(), "tests/schemas/various-enums-json-schema.rs".into(), - TypeSpaceSettings::default().with_derive("schemars::JsonSchema".to_string()), + settings, ) .unwrap(); @@ -54,7 +63,7 @@ fn test_various_enums_json_schema() { fn validate_schema( path: std::path::PathBuf, out_path: std::path::PathBuf, - typespace: &mut TypeSpaceSettings, + mut settings: TypeSpaceSettings, ) -> Result<(), Box> { let file = File::open(path)?; let reader = BufReader::new(file); @@ -70,7 +79,7 @@ fn validate_schema( let schema = serde_json::from_value(schema_raw).unwrap(); let mut type_space = TypeSpace::new( - typespace + settings .with_replacement( "HandGeneratedType", "String", @@ -95,15 +104,17 @@ fn validate_schema( typify::CrateVers::Version("1.0.0".parse().unwrap()), None, ) - .with_struct_builder(true), + .map_typespace_settings(|s| s.with_struct_builder(true)), ); type_space.add_root_schema(root_schema)?; + let types = type_space.to_stream()?; + // Make a file with the generated code. let code = quote! { #![deny(warnings)] - #type_space + #types fn main() {} };