From bb61c064db0ac291a22694b37c552bb3a4e74532 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Tue, 8 Sep 2026 09:59:10 -0700 Subject: [PATCH 01/23] replace typify's type generation with the typespace crate typespace becomes a path dependency and takes over the type representation, the query surface, and rendering. typify's own TypeEntry/TypeEntryDetails tree, its query and render facade, and the structural dedup of unnamed types all come out; conversion now builds typespace types directly and to_stream finalizes a TypespaceBuilder and renders through codespace. to_stream and the consumer entry points become fallible so finalization errors (dangling references, name collisions, unsatisfiable trait requirements) surface as Error::Typespace. typespace_ext.rs holds the glue: native type capabilities, patch derives and attributes, and the notes on the gaps that remain relative to typify1's renderer. --- Cargo.lock | 27 + Cargo.toml | 4 + cargo-typify/src/lib.rs | 3 +- example-build/build.rs | 2 +- typify-impl/Cargo.toml | 1 + typify-impl/src/conversions.rs | 2 +- typify-impl/src/convert.rs | 209 ++- typify-impl/src/cycles.rs | 46 +- typify-impl/src/defaults.rs | 327 ++-- typify-impl/src/enums.rs | 339 ++-- typify-impl/src/lib.rs | 757 +++------ typify-impl/src/structs.rs | 296 ++-- typify-impl/src/test_util.rs | 48 +- typify-impl/src/type_entry.rs | 2288 ++++---------------------- typify-impl/src/typespace_ext.rs | 101 ++ typify-impl/src/util.rs | 19 +- typify-impl/tests/test_generation.rs | 21 +- typify-impl/tests/test_github.rs | 4 +- typify-macro/src/lib.rs | 8 +- typify-test/build.rs | 6 +- typify/src/lib.rs | 12 +- typify/tests/schemas.rs | 11 +- 22 files changed, 1281 insertions(+), 3250 deletions(-) create mode 100644 typify-impl/src/typespace_ext.rs diff --git a/Cargo.lock b/Cargo.lock index 5400cb98..dfcd005c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,6 +248,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "codespace" +version = "0.0.1-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61018db61b1cdf32bed05d3addbd3a49d03be38e1a7674e067e6ded193d0cf49" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -1270,6 +1281,21 @@ dependencies = [ "toml 1.0.1+spec-1.1.0", ] +[[package]] +name = "typespace" +version = "0.0.1-alpha.1" +dependencies = [ + "codespace", + "heck", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 3.0.4", + "thiserror 2.0.20", +] + [[package]] name = "typify" version = "0.8.0" @@ -1310,6 +1336,7 @@ dependencies = [ "serde_json", "syn 3.0.6", "thiserror 2.0.20", + "typespace", "unicode-ident", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index f98da9ea..9d9532e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,10 @@ 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" } +# Discovery: the standalone type-modeling crate that typify's code +# generation is expected to migrate to. Path dependency for now. +typespace = { path = "../typespace/typespace" } + assert_cmd = "2.2.2" chrono = { version = "0.4.45", features = ["serde"] } clap = { version = "4.6.1", features = ["derive"] } diff --git a/cargo-typify/src/lib.rs b/cargo-typify/src/lib.rs index f90edd05..01c7d2e2 100644 --- a/cargo-typify/src/lib.rs +++ b/cargo-typify/src/lib.rs @@ -193,7 +193,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/example-build/build.rs b/example-build/build.rs index 9cecec6b..1fb32b19 100644 --- a/example-build/build.rs +++ b/example-build/build.rs @@ -12,7 +12,7 @@ fn main() { 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/typify-impl/Cargo.toml b/typify-impl/Cargo.toml index 36b72134..8faf0ca2 100644 --- a/typify-impl/Cargo.toml +++ b/typify-impl/Cargo.toml @@ -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..816d7547 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -3,20 +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}; +use crate::{Error, Name, Result, TypeSpace}; pub const STD_NUM_NONZERO_PREFIX: &str = "::std::num::NonZero"; @@ -803,10 +802,28 @@ impl TypeSpace { match format.as_ref().map(String::as_str) { Some("uuid") => { self.uses_uuid = true; + // ::uuid::Uuid additionally implements Display, + // FromStr, Eq, PartialEq, Ord, PartialOrd, and Hash. It + // also implements Default, but typify does not declare + // Default on natives. 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, + ] + .into_iter() + .collect::(), ), metadata, )) @@ -814,43 +831,131 @@ impl TypeSpace { Some("date") => { self.uses_chrono = true; + // ::chrono::naive::NaiveDate additionally implements + // Display, FromStr, PartialEq, Eq, PartialOrd, Ord, and + // Hash. It also implements Default, but typify does not + // declare Default on natives. 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, + ] + .into_iter() + .collect::(), ), metadata, )) } Some("date-time") => { self.uses_chrono = true; + // ::chrono::DateTime implements PartialEq, Eq, + // PartialOrd, Ord, Hash, Debug, and Display for any + // TimeZone, and Utc additionally gets FromStr. It also + // implements Default, but typify does not declare + // Default on natives. 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, + ] + .into_iter() + .collect::(), ), metadata, )) } + // ::std::net::IpAddr implements Clone, Eq, PartialEq, + // Hash, PartialOrd, Ord, Display, and FromStr. It 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, + ] + .into_iter() + .collect::(), ), metadata, )), + // ::std::net::Ipv4Addr implements Clone, PartialEq, Eq, + // Hash, PartialOrd, Ord, Debug, Display, and FromStr. It + // 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, + ] + .into_iter() + .collect::(), ), metadata, )), + // ::std::net::Ipv6Addr implements Clone, PartialEq, Eq, + // Hash, PartialOrd, Ord, Debug, Display, and FromStr. It + // has no 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, + ] + .into_iter() + .collect::(), ), metadata, )), @@ -871,7 +976,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 { @@ -883,7 +988,7 @@ impl TypeSpace { 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 +1042,7 @@ impl TypeSpace { Some(Ok(Variant::new( variant_name.clone(), None, - VariantDetails::Simple, + VariantDetails::Unit, ))) } @@ -1207,7 +1312,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 +1466,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>( @@ -1808,7 +1910,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 +1923,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 +1957,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)), } } @@ -1876,12 +1972,12 @@ impl TypeSpace { 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)), } } @@ -1897,8 +1993,8 @@ impl TypeSpace { 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... @@ -1914,7 +2010,7 @@ impl TypeSpace { 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>( @@ -2131,7 +2227,8 @@ mod tests { &schema.schema, ) .unwrap(); - let output = ty.type_name(&type_space); + let type_id = type_space.assign_type(ty); + let output = type_space.to_typespace().unwrap().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 +2258,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 +2362,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() { [] => {} @@ -2347,7 +2413,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 index 0ebc6f76..1cb7dfc0 100644 --- a/typify-impl/src/cycles.rs +++ b/typify-impl/src/cycles.rs @@ -5,13 +5,7 @@ use std::{ ops::Range, }; -use crate::{ - type_entry::{ - TypeEntry, TypeEntryDetails, TypeEntryEnum, TypeEntryNewtype, TypeEntryStruct, - VariantDetails, - }, - TypeId, TypeSpace, -}; +use crate::{type_entry::TypeEntry, TypeId, TypeSpace}; impl TypeSpace { /// We need to root out any containment cycles, breaking them by inserting @@ -141,36 +135,16 @@ impl TypeSpace { /// For types that could potentially participate in a cycle, return a list of /// mutable references to the child types. +/// +/// typespace models exactly this notion as `Type::contained_children_mut` +/// (finalization uses it for its own cycle breaking), so we delegate to +/// it. typify continues to break cycles itself--before entry +/// finalization--because queries such as `has_impl` recurse through +/// contained types and must not encounter a cycle. 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(), + match type_entry { + TypeEntry::Type(typ) => typ.contained_children_mut(), + TypeEntry::Reference(_) => Vec::new(), } } diff --git a/typify-impl/src/defaults.rs b/typify-impl/src/defaults.rs index 6a343575..40195954 100644 --- a/typify-impl/src/defaults.rs +++ b/typify-impl/src/defaults.rs @@ -1,21 +1,39 @@ -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use std::collections::BTreeMap; use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::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, + convert::STD_NUM_NONZERO_PREFIX, type_entry::TypeEntry, DefaultImpl, Error, Result, TypeId, + TypeSpace, }; +use typespace::build::{ + EnumTagType, EnumVariant, StructProperty, StructPropertySerde, StructPropertyState, Type, + VariantDetails, +}; + +/// The disposition of a valid default value; see +/// [`TypeEntry::validate_value`]. +/// +/// typify1 used this to route rendering: `Generic` defaults shared one +/// of the stock functions below, `Specific` defaults got a bespoke +/// generated function, and `Intrinsic` defaults used +/// `#[serde(default)]`. Rendering is now typespace's job +/// (IMPLEMENTATION GAP: typespace renders every explicit default via a +/// runtime `serde_json::from_value` call rather than a typed +/// expression; see the note in `TypeSpace::to_stream`), but validation +/// still uses these distinctions to track which shared impls would be +/// needed. +#[derive(Debug)] +pub(crate) enum DefaultKind { + Intrinsic, + Specific, + Generic(DefaultImpl), +} + // Implementations for "stock" default functions so we don't litter the // namespace with many that are effectively identical. impl From<&DefaultImpl> for TokenStream { @@ -62,41 +80,25 @@ impl From<&DefaultImpl> for TokenStream { 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); - } + if let Some(default) = self.as_type().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 + match self.as_type() { + Type::Struct(type_struct) => { + type_struct + .get_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 { + Type::Enum(type_enum) => { + type_enum.get_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)) @@ -113,16 +115,13 @@ impl TypeEntry { } fn check_property_defaults( - property: &StructProperty, + property: &StructProperty, type_space: &mut TypeSpace, ) -> Result<()> { - if let StructProperty { - state: StructPropertyState::Default(WrappedValue(prop_default)), - type_id, - .. - } = property + if let StructPropertyState::DefaultValue(typespace::build::JsonValue(prop_default)) = + property.state() { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); + let type_entry = type_space.id_to_entry.get(property.type_id()).unwrap(); if let DefaultKind::Generic(default_fn) = type_entry.validate_value(type_space, prop_default)? { @@ -136,9 +135,7 @@ impl TypeEntry { /// /// 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. + /// or requires a bespoke function to generate the value. /// /// [`Value`]: serde_json::Value pub(crate) fn validate_value( @@ -146,40 +143,42 @@ impl TypeEntry { 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) + match self.as_type() { + Type::Enum(type_enum) => { + let variants = type_enum.get_variants(); + match type_enum.get_tag_type().expect("built enum has a 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) + } + Type::Struct(type_struct) => { + validate_default_struct_props(type_struct.get_properties(), type_space, default) .ok_or_else(Error::invalid_value) } - TypeEntryDetails::Newtype(TypeEntryNewtype { type_id, .. }) => { + Type::NewtypeStruct(type_newtype) => { + let inner = type_newtype.get_inner(); // 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)?; + let _ = validate_type_id(inner, type_space, default)?; Ok(DefaultKind::Specific) } - TypeEntryDetails::Option(type_id) => { + Type::Option(type_id) => { if let serde_json::Value::Null = default { Ok(DefaultKind::Intrinsic) } else { @@ -188,9 +187,9 @@ impl TypeEntry { Ok(DefaultKind::Specific) } } - TypeEntryDetails::Box(type_id) => validate_type_id(type_id, type_space, default), + Type::Box(type_id) => validate_type_id(type_id, type_space, default), - TypeEntryDetails::Vec(type_id) => { + Type::Vec(type_id) => { if let serde_json::Value::Array(v) = default { if v.is_empty() { Ok(DefaultKind::Intrinsic) @@ -205,7 +204,7 @@ impl TypeEntry { Err(Error::invalid_value()) } } - TypeEntryDetails::Map(key_id, value_id) => { + Type::Map(key_id, value_id) => { if let serde_json::Value::Object(m) = default { if m.is_empty() { Ok(DefaultKind::Intrinsic) @@ -225,7 +224,7 @@ impl TypeEntry { Err(Error::invalid_value()) } } - TypeEntryDetails::Set(type_id) => { + Type::Set(type_id) => { if let serde_json::Value::Array(v) = default { if v.is_empty() { Ok(DefaultKind::Intrinsic) @@ -247,11 +246,11 @@ impl TypeEntry { Err(Error::invalid_value()) } } - TypeEntryDetails::Tuple(ids) => { + Type::Tuple(ids) => { validate_default_tuple(ids, type_space, default).ok_or_else(Error::invalid_value) } - TypeEntryDetails::Array(type_id, length) => { + Type::Array(type_id, length) => { let Some(arr) = default.as_array() else { return Err(Error::invalid_value()); }; @@ -265,32 +264,32 @@ impl TypeEntry { } Ok(DefaultKind::Specific) } - TypeEntryDetails::Unit => { + Type::Unit => { if let serde_json::Value::Null = default { Ok(DefaultKind::Intrinsic) } else { Err(Error::invalid_value()) } } - TypeEntryDetails::Native(_) => { + Type::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. + // will fail an `unwrap()` in the generated code if this Value + // is not valid for this built-in type. Ok(DefaultKind::Specific) } - TypeEntryDetails::JsonValue => Ok(DefaultKind::Specific), - TypeEntryDetails::Boolean => match default { + Type::JsonValue => Ok(DefaultKind::Specific), + Type::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()) { + Type::Integer(itype) => match (default.as_u64(), default.as_i64()) { (None, None) => Err(Error::invalid_value()), (Some(0), _) => Ok(DefaultKind::Intrinsic), (_, Some(0)) => unreachable!(), @@ -303,7 +302,7 @@ impl TypeEntry { } (_, Some(_)) => Ok(DefaultKind::Generic(DefaultImpl::I64)), }, - TypeEntryDetails::Float(_) => { + Type::Float(_) => { if let Some(value) = default.as_f64() { if value == 0.0 { Ok(DefaultKind::Intrinsic) @@ -314,7 +313,7 @@ impl TypeEntry { Err(Error::invalid_value()) } } - TypeEntryDetails::String => { + Type::String => { if let Some("") = default.as_str() { Ok(DefaultKind::Intrinsic) } else { @@ -322,77 +321,23 @@ impl TypeEntry { } } - 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)) + // typify never constructs unit structs, tuple structs, or + // type aliases; Type is non_exhaustive besides. + _ => unreachable!("unexpected typespace type variant"), } } } pub(crate) fn validate_default_for_external_enum( type_space: &TypeSpace, - variants: &[Variant], + variants: &[EnumVariant], 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(|| ())?; + .find(|variant| simple_name == variant.json_name())?; + matches!(variant.details(), VariantDetails::Unit).then(|| ())?; Some(DefaultKind::Specific) } else { @@ -403,10 +348,12 @@ pub(crate) fn validate_default_for_external_enum( let (name, value) = map.iter().next()?; - let variant = variants.iter().find(|variant| name == &variant.raw_name)?; + let variant = variants + .iter() + .find(|variant| name == variant.json_name())?; - match &variant.details { - VariantDetails::Simple => None, + match variant.details() { + VariantDetails::Unit => 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) => { @@ -418,16 +365,18 @@ pub(crate) fn validate_default_for_external_enum( pub(crate) fn validate_default_for_internal_enum( type_space: &TypeSpace, - variants: &[Variant], + variants: &[EnumVariant], 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)?; + let variant = variants + .iter() + .find(|variant| name == variant.json_name())?; - match &variant.details { - VariantDetails::Simple => Some(DefaultKind::Specific), + match variant.details() { + VariantDetails::Unit => Some(DefaultKind::Specific), VariantDetails::Struct(props) => { // Make an object without the tag. let inner_default = serde_json::Value::Object( @@ -446,7 +395,7 @@ pub(crate) fn validate_default_for_internal_enum( pub(crate) fn validate_default_for_adjacent_enum( type_space: &TypeSpace, - variants: &[Variant], + variants: &[EnumVariant], default: &serde_json::Value, tag: &str, content: &str, @@ -465,10 +414,10 @@ pub(crate) fn validate_default_for_adjacent_enum( let variant = variants .iter() - .find(|variant| tag_value == variant.raw_name)?; + .find(|variant| tag_value == variant.json_name())?; - match (&variant.details, content_value) { - (VariantDetails::Simple, None) => Some(DefaultKind::Specific), + match (variant.details(), content_value) { + (VariantDetails::Unit, None) => Some(DefaultKind::Specific), (VariantDetails::Tuple(tup), Some(content_value)) => { validate_default_tuple(tup, type_space, content_value) } @@ -481,14 +430,14 @@ pub(crate) fn validate_default_for_adjacent_enum( pub(crate) fn validate_default_for_untagged_enum( type_space: &TypeSpace, - variants: &[Variant], + variants: &[EnumVariant], 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 => { + match variant.details() { + VariantDetails::Unit => { default.as_null()?; Some(DefaultKind::Specific) } @@ -528,7 +477,7 @@ fn validate_default_tuple( } fn validate_default_struct_props( - properties: &[StructProperty], + properties: &[StructProperty], type_space: &TypeSpace, default: &serde_json::Value, ) -> Option { @@ -579,43 +528,41 @@ fn validate_default_struct_props( named_properties .iter() .filter(|(_, (_, required))| *required) - .try_for_each(|(name, _)| map.get(*name).map(|_| ()))?; + .try_for_each(|(name, _)| map.get(name.as_str()).map(|_| ()))?; Some(DefaultKind::Specific) } fn all_props<'a>( - property: &'a StructProperty, + 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, +) -> Vec<(Option, &'a TypeId, bool)> { + // The JSON name of the property; typespace stores the Rust name and + // an optional serde rename, so the JSON name is the rename when + // present and the Rust name otherwise. + let maybe_name = match property.json_name() { + StructPropertySerde::None => Some(property.rust_name().to_string()), + StructPropertySerde::Rename(rename) => Some(rename.clone()), + StructPropertySerde::Flatten => None, }; if let Some(name) = maybe_name { - let required = match &property.state { - StructPropertyState::Required => true, - StructPropertyState::Optional | StructPropertyState::Default(_) => false, - }; + let required = matches!(property.state(), StructPropertyState::Required); - vec![(Some(name), &property.type_id, required)] + 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 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) + let (properties, all_required) = match type_entry.as_type() { + Type::Struct(type_struct) => { + let optional = matches!(property.state(), StructPropertyState::Optional); + (type_struct.get_properties(), !optional) } - TypeEntryDetails::Option(type_id) => { + Type::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) + if let Type::Struct(type_struct) = type_entry.as_type() { + (type_struct.get_properties(), false) } else { unreachable!() } @@ -623,7 +570,7 @@ fn all_props<'a>( // 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)], + Type::Map(_, value_id) => return vec![(None, value_id, false)], _ => unreachable!(), }; @@ -643,11 +590,7 @@ mod tests { use serde_json::json; use uuid::Uuid; - use crate::{ - test_util::get_type, - type_entry::{DefaultKind, TypeEntry}, - DefaultImpl, - }; + use crate::{defaults::DefaultKind, test_util::get_type, type_entry::TypeEntry, DefaultImpl}; #[test] fn test_default_option() { @@ -671,11 +614,7 @@ mod tests { 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(), - }; + let type_entry = TypeEntry::from(typespace::build::Type::Box(type_id)); assert!(type_entry .validate_value(&type_space, &json!("forty-two")) diff --git a/typify-impl/src/enums.rs b/typify-impl/src/enums.rs index 43bbbc0f..5e296cbd 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,14 @@ 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}; + #[allow(dead_code)] #[derive(Serialize, JsonSchema, Schema)] #[serde(deny_unknown_fields)] @@ -1045,55 +927,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 +984,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 +1074,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 +1162,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 +1256,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 +1290,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,7 +1313,7 @@ mod tests { } } }; - assert_eq!(actual.to_string(), expected.to_string()); + assert_eq!(actual, expected.to_string()); } #[test] @@ -1510,9 +1335,10 @@ 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"); + + // typify1's output; see test_result. let expected = quote! { #[doc = "`ResultX`"] #[derive(::serde::Deserialize, ::serde::Serialize, A, B, C, Clone, D, Debug)] @@ -1527,7 +1353,7 @@ mod tests { } } }; - assert_eq!(actual.to_string(), expected.to_string()); + assert_eq!(actual, expected.to_string()); } #[test] @@ -1541,4 +1367,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..196bbb80 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! typify backend implementation. @@ -8,20 +8,23 @@ use std::collections::{BTreeMap, BTreeSet}; use conversions::SchemaCache; use log::{debug, info}; -use output::OutputSpace; use proc_macro2::TokenStream; -use quote::{format_ident, quote, ToTokens}; +use 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; @@ -31,23 +34,22 @@ mod cycles; mod defaults; mod enums; mod merge; -mod output; mod rust_extension; mod structs; mod type_entry; +mod typespace_ext; 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, @@ -68,84 +70,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 +114,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,7 +132,6 @@ pub struct TypeSpace { definitions: BTreeMap, id_to_entry: BTreeMap, - type_to_id: BTreeMap, name_to_id: BTreeMap, ref_to_id: BTreeMap, @@ -213,7 +155,6 @@ 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(), @@ -332,9 +273,13 @@ impl From for MapType { } /// Settings that alter type generation. +// [typespace migration note] typify1 had a `type_mod` setting that +// qualified every named-type identifier query with a module path; +// typespace makes scope a per-query argument (`ident_in`, +// `parameter_ident_in`), so the setting is gone and callers pass the +// scope where they ask for identifiers. #[derive(Default, Debug, Clone)] pub struct TypeSpaceSettings { - type_mod: Option, extra_derives: Vec, extra_attrs: Vec, struct_builder: bool, @@ -449,13 +394,41 @@ 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 +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. + /// `FromStringIrrefutable` has no typespace equivalent (see + /// typespace_ext.rs gap note 2) 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, + ] + .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 => {} + } + } + traits } +} +impl TypeSpaceSettings { /// 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) { @@ -719,7 +692,7 @@ 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); } @@ -748,19 +721,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 +733,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 +741,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 +768,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(()) @@ -872,15 +842,6 @@ 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, - }) - } - /// Whether the generated code needs `chrono` crate. pub fn uses_chrono(&self) -> bool { self.uses_chrono @@ -901,84 +862,134 @@ impl TypeSpace { self.uses_uuid } - /// 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, - }) + /// The typespace settings derived from typify's settings. + /// + /// typify has always emitted `Serialize`, `Deserialize`, `Clone`, and + /// `Debug` on every generated type, so those four are requested as + /// required traits regardless of user settings. Global extra derives + /// forward to typespace's settings-wide `with_derive`, and the map + /// type forwards to `with_map_type` along with the traits a map key + /// requires. `ContainerType::new` accepts an arbitrary type path, so + /// typify cannot know whether the configured container needs + /// hashed-lookup traits (`Hash`, `Eq`, `PartialEq`) or ordered-lookup + /// traits (`Ord`, `PartialOrd`, along with `Eq` and `PartialEq`, which + /// `Ord` and `PartialOrd` require); the union of both is forwarded as + /// the key obligation, matching typify's own generated key types, + /// which always derive all five (see + /// `typify/tests/schemas/maps_custom.rs`); the value obligation is + /// empty. `ContainerType::new` also claims only what rendering + /// assumes of the container itself (`Default` unconditionally, and + /// `Clone`, `Debug`, `Serialize`, `Deserialize` following the + /// parameters), which is the conservative answer for a path typify + /// cannot otherwise reason about. Global extra attributes have no + /// typespace home yet; see the implementation gaps on + /// [`TypeSpace::to_stream`] and typespace_ext.rs gap note 3. + fn typespace_settings(&self) -> typespace::settings::Settings { + let map_key_obligation = [ + typespace::TypespaceTrait::Hash, + typespace::TypespaceTrait::Eq, + typespace::TypespaceTrait::PartialEq, + typespace::TypespaceTrait::Ord, + typespace::TypespaceTrait::PartialOrd, + ] + .into_iter() + .collect::(); + let map_type = typespace::settings::ContainerType::new( + &self.settings.map_type.0.to_token_stream().to_string(), + [map_key_obligation, typespace::TypespaceTraitSet::empty()], + ) + .with_provision( + typespace::TypespaceTrait::JsonSchema, + typespace::settings::TraitProvision::IfParameters, + ); + let mut 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_map_type(map_type); + for derive in &self.settings.extra_derives { + match derive.as_str() { + "::schemars::JsonSchema" | "schemars::JsonSchema" | "JsonSchema" => { + settings = settings.with_required_trait(typespace::TypespaceTrait::JsonSchema) + } + _ => { + settings = settings.with_derive(derive.clone()); + } + } + } + settings = settings.with_struct_builder(self.settings.struct_builder); + settings } - /// 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 - .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); + /// 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.typespace_settings()); + + 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(_) => {} + } } - output.into_stream() + // typify breaks containment cycles itself (see cycles.rs) by + // inserting Box entries before output, so typespace finalization + // should never need to manufacture a Box of its own. Provide a + // real generator anyway--if it ever fires we'd rather get a + // distinctive id than a panic--by mapping the inner id into a + // range that TypeSpace::assign never produces. + Ok(builder.finalize(|inner: &TypeId| TypeId(inner.0 | (1 << 63)))?) } - /// 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()) - } - } - }, - ); + /// All code for processed types. + /// + /// Rendering is delegated to typespace: the stored types are + /// inserted into a `TypespaceBuilder`, finalized, and rendered + /// through codespace. Finalization errors (dangling references, + /// name collisions, unsatisfiable trait requirements) surface as + /// [`Error::Typespace`]. Known IMPLEMENTATION GAPS relative to + /// typify1's renderer (each of these is a construct typespace can + /// or should express but does not yet render): + /// + /// - no `error` module / `ConversionError` type (nothing typespace + /// emits refers to it yet, so the output is self-consistent); + /// this method injects the module itself to match typify1's + /// unconditional emission; + /// - no bespoke enum impls: Display/FromStr for all-simple-variant + /// enums, untagged Display/FromStr proxies, TryFrom<&str>, and + /// the convenience `From` impls; + /// - no `impl Default` for whole-type default values, and explicit + /// property defaults are runtime `serde_json::from_value` calls + /// rather than typify1's typed expressions (see value.rs, retained + /// unbuilt for reference) with shared `defaults::` helpers; + /// - no string-constraint validation (min/max length, pattern) on + /// newtypes, and no value-list constraints at all; + /// - no `#[serde(deny_unknown_fields)]` on structs or enums; + /// - no derives beyond serde and the settings-wide extra derives + /// (typify1 derived Debug/Clone always, Copy/Eq/Ord/Hash for + /// simple enums, and applied per-type derives and attrs from + /// patches); + /// - no struct builder types (TypeSpaceSettings::with_struct_builder); + pub fn to_stream(&self) -> Result { + let typespace = self.to_typespace()?; + + let codespace = typespace.to_codespace(); + + Ok(codespace.into_stream()) } /// Allocated the next TypeId. @@ -988,14 +999,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 +1015,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 +1024,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 +1046,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 +1061,44 @@ 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()) + if let Some(entry) = self.id_to_entry.get(id) { + if matches!(entry, TypeEntry::Type(typespace::build::Type::Option(_))) { + return id.clone(); + } + } + self.assign_type(typespace::build::Type::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() + // 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() } /// 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()) + self.assign_type(typespace::build::Type::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()) - } - 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!(), - } - } - - /// 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, - } - } -} - -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 { + let native_name = native.name().rsplit("::").next().unwrap(); + !native.parameters().is_empty() + || matches!(type_name, Name::Required(req) if req == native_name) } #[cfg(test)] @@ -1275,11 +1110,10 @@ 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, MapType, Name, TypeSpace, + TypeSpaceSettings, }; + use typespace::build::{Type, VariantDetails}; #[test] fn test_map_type_from_str() { @@ -1367,16 +1201,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 +1231,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 +1261,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 +1280,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 +1306,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 +1348,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/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..c7d6b5b5 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -1,241 +1,99 @@ // 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 (see the gap notes in typespace_ext.rs for the +//! typify-specific data typespace has no home for); +//! - 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, Result, 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. +// [typespace migration note] Struct-variant property names were +// `&'a str` borrowed out of typify's own representation; typespace's +// `StructProperty::rust_name` is a `syn::Ident`, which can only give up +// its name as an owned String. INTERFACE GAP: consider `rust_name() +// -> &str` (or storing the name as a String) in typespace. This applies +// to typespace's view module too, whose property names are owned +// Strings for the same reason. + +/// 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 +101,102 @@ 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; + // INTERFACE GAP: container-level serde rename (set when a patch + // renames the type) has no home in typespace's TypeEnum; see + // typespace_ext.rs gap note 4. 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(); } - } + let typ = type_enum.build().unwrap(); - 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(); - } -} - -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 +205,81 @@ 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; + // INTERFACE GAP: container-level serde rename; see + // TypeEntryEnum::from_metadata. 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; + // INTERFACE GAP: container-level serde rename; see + // TypeEntryEnum::from_metadata. 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 +290,25 @@ 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, + // INTERFACE GAP: typespace has no representation for a newtype + // constrained to a fixed set of permitted values; the validation + // (TryFrom constructor plus bespoke Deserialize impl in typify1) + // is dropped. The ideal call is: + // + let constraints = NewtypeConstraints::AllowList( + enum_values.iter().cloned().map(JsonValue::new).collect(), + ); + // + // See typespace_ext.rs gap note 1. + let _ = enum_values; + 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 +319,22 @@ 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, + // INTERFACE GAP: as with from_metadata_with_enum_values, but for + // a set of rejected values. The ideal call is: + // + let constraints = + NewtypeConstraints::DenyList(enum_values.iter().cloned().map(JsonValue::new).collect()); + // + // See typespace_ext.rs gap note 1. + let _ = enum_values; + 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 +345,120 @@ 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, 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) - } - - // We should never get here as reference types should only be used - // in-flight, but never recorded into the type space. - TypeEntryDetails::Reference(_) => unreachable!(), + .collect::(); - // Unnamed types require no definition as they're already defined. - _ => (), - } + Type::Native(Native::new(type_name, 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 finalize(&mut self, type_space: &mut TypeSpace) -> Result<()> { + // typify1 cached the enum "bespoke impl" analysis (all-simple- + // variants, untagged Display/FromStr proxies) here; those + // answers are now computed on demand in has_impl since typespace + // has no place to store them (and no rendering that would + // consume them; that's an IMPLEMENTATION GAP--see to_stream). + self.check_defaults(type_space) } -} - -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"); + pub(crate) fn name(&self) -> Option<&str> { + match self { + Self::Type(typ) => typ.name(), + Self::Reference(_) => None, + } + } +} + +/// Flatten typify's doc block into typespace's single description +/// string. +/// +/// typify rendered a type's documentation as a description attribute +/// followed by a `
` block containing the pretty-printed JSON +/// schema, as line-by-line `#[doc]` attributes, falling back to +/// `` `Name` `` when the schema has no description. +/// +/// INTERFACE GAP: typespace's TypeCommon has only a single description +/// string with no structured place for the schema block, so the whole +/// thing is flattened into one multi-line string here (which typespace +/// emits as one `#[doc = "..."]` attribute). The rendered rustdoc is +/// equivalent but the generated source is not. It also means the schema +/// must be captured at construction time rather than carried alongside +/// the type as typify1 did. +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/typespace_ext.rs b/typify-impl/src/typespace_ext.rs new file mode 100644 index 00000000..c8e4e29e --- /dev/null +++ b/typify-impl/src/typespace_ext.rs @@ -0,0 +1,101 @@ +// Copyright 2026 Oxide Computer Company + +//! Interface gaps between typify and the typespace crate. +//! +//! This file used to shim proposed typespace interfaces (a `TypeExt` +//! trait with `name`/`set_default`/`dedup_key`, a local `DedupKey` +//! mirror, `EnumVariant::json_name`, and `Enum::all_simple_variants`); +//! typespace now provides all of them and the shims are gone. What +//! remains is the record of gaps that cannot be shimmed from outside +//! the crate; each is annotated at its call site as well. +//! +//! 1. INTERFACE GAP: value-list newtype constraints. +//! `NewtypeConstraints` is non_exhaustive, so new variants cannot +//! be added here. typify supports newtypes constrained to a fixed +//! set of permitted values (`enum` with a non-string type) or a set +//! of rejected values (`not`/`enum`), rendered as a TryFrom +//! constructor plus a bespoke Deserialize impl. Proposed: +//! +//! ```text +//! pub enum NewtypeConstraints { +//! None, +//! EnumValues(Vec), +//! DenyValues(Vec), +//! String { ... }, +//! Array { ... }, +//! } +//! ``` +//! +//! Call sites: `TypeEntryNewtype::from_metadata_with_enum_values` +//! and `from_metadata_with_deny_values` in type_entry.rs, which +//! currently construct `NewtypeConstraints::None` and drop the +//! validation. +//! +//! 2. INTERFACE GAP (narrowed): native capability markers. +//! `TypespaceTrait::Default` now exists and typify maps declared +//! `Default` impls onto it. The remaining piece is +//! `FromStringIrrefutable`: typespace tracks that capability +//! crate-privately (fed by `Native::new_string_like`), so a +//! consumer-declared irrefutable-FromStr native (from x-rust-type +//! or a conversion policy) has no typespace home. typify keeps its +//! `Vec` side channel on `TypeEntry` for that one +//! marker. With typify's own `has_impl` machinery deleted and the +//! structural-dedup lookup that used to pair this with typespace's +//! `DedupKey` also removed, the side channel is unconsumed data +//! retained at the collection sites; no query can see it. The +//! query-side loss is recorded in gap 7. +//! +//! 3. INTERFACE GAP (narrowed): rendering of extra derives and +//! attributes. Every path into typespace exists: the settings-wide +//! derives go to typespace's `with_derive` (see +//! `TypeSpace::typespace_settings`), and a +//! `TypeSpacePatch`'s `with_derive` / `with_attr` values reach the +//! named shapes' `extra_derives` / `extra_attrs` (see `TypePatch` +//! in util.rs and its three call sites in type_entry.rs). What +//! remains is on typespace's side: it renders only the +//! settings-wide derives, so the settings-wide attrs and both +//! per-type lists are stored and never emitted. +//! +//! 4. INTERFACE GAP (latent): container-level serde rename. typify1's +//! renderer supported `#[serde(rename = "...")]` on the container, +//! though nothing ever populated the field (the patch mechanism +//! renames the type without preserving the serialized name). The +//! shapes have `name` but no `rename`; if typespace wants to fix +//! the underlying bug--a renamed type should keep its wire +//! name--the shape metadata is where the rename belongs. Call +//! sites: the INTERFACE GAP comments in `TypeEntry*::from_metadata` +//! in type_entry.rs. +//! +//! 5. CLOSED (was: pre-finalization identifier rendering). typify's +//! `TypeEntry::type_ident` / `type_parameter_ident` are gone; +//! identifier queries go through typespace's view API on the +//! finalized typespace (`ident`, `ident_in`, `parameter_ident`, +//! `parameter_ident_in`, `parameter_ident_with_lifetime`), with +//! scope as a per-query argument. The `type_mod` setting is gone +//! with them: callers pass the scope where they ask for +//! identifiers. Behavioral differences from typify1's renderer: +//! maps render as BTreeMap until `with_map_type` is forwarded to +//! typespace settings (a separate, gated change); typespace passes +//! `String` parameters by value where typify1 used `&str`; and +//! typespace passes all enums by reference where typify1 passed +//! all-simple-variant enums by value. +//! +//! 6. CLOSED (was: fallible finalization at render time). +//! `TypeSpace::to_stream` and `TypeSpace::to_typespace` return +//! `Result`; typespace finalization errors surface as +//! `Error::Typespace` instead of a panic. Consumers (typify-macro, +//! cargo-typify, build scripts) propagate or unwrap them. +//! +//! 7. INTERFACE GAP: queries the deleted typify facade answered that +//! typespace's view API cannot. `view::Type::has_impl` takes +//! typespace's `TypeSpaceImpl` (Display/FromStr/Eq/Ord/Hash), so +//! there is no way to ask for `Default` (though +//! `TypespaceTrait::Default` exists and propagates) or +//! `FromStringIrrefutable` (crate-private; see gap 2). typify's +//! enum-analysis answers (all-simple-variant enums implement +//! Display/FromStr, untagged newtype-variant proxies) went with +//! `has_impl` and have no typespace equivalent until the bespoke +//! impls render. Also gone with no replacement: `Type::describe` +//! (debug summary string) and `Type::builder` (the +//! `mod builder` ident for struct_builder settings; typespace does +//! not render builders at all). 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/tests/test_generation.rs b/typify-impl/tests/test_generation.rs index ca4ad92b..f4fa109d 100644 --- a/typify-impl/tests/test_generation.rs +++ b/typify-impl/tests/test_generation.rs @@ -52,7 +52,6 @@ fn test_generation() { let mut type_space = TypeSpace::new( TypeSpaceSettings::default() .with_derive("JsonSchema".to_string()) - .with_type_mod("types") .with_struct_builder(true) .with_patch( "AllTheTraits", @@ -78,20 +77,24 @@ 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_in("types"); + let string = typespace.get_type(&string_id).parameter_ident_in("types"); + let opt_int = typespace.get_type(&opt_int_id).parameter_ident_in("types"); + let strenum = typespace.get_type(&strenum_id).parameter_ident_in("types"); + + 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/src/lib.rs b/typify-macro/src/lib.rs index fd3a2dbf..8e0f591f 100644 --- a/typify-macro/src/lib.rs +++ b/typify-macro/src/lib.rs @@ -271,9 +271,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..975fa449 100644 --- a/typify-test/build.rs +++ b/typify-test/build.rs @@ -179,7 +179,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,7 +191,7 @@ 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"); @@ -205,7 +205,7 @@ 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_custommap.rs"); diff --git a/typify/src/lib.rs b/typify/src/lib.rs index 079162d2..b75d8d21 100644 --- a/typify/src/lib.rs +++ b/typify/src/lib.rs @@ -152,18 +152,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..b5258ecb 100644 --- a/typify/tests/schemas.rs +++ b/typify/tests/schemas.rs @@ -17,7 +17,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, &mut TypeSpaceSettings::default()) + .expect(&format!("failed for {}", entry.to_string_lossy())) } // Make sure it all compiles. @@ -99,11 +100,15 @@ fn validate_schema( ); 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)] + // TODO 9/3/2026 + // revert + #![allow(warnings)] - #type_space + #types fn main() {} }; From fc31b2541f7640c8eca49ee1ef3b31ab532261a9 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Tue, 8 Sep 2026 09:59:25 -0700 Subject: [PATCH 02/23] turn on typespace's typify compatibility mode with_typify_compat withholds Default from a tuple struct, a unit struct, and a newtype, which typify never derives it for. --- typify-impl/src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 196bbb80..0e210b55 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -881,7 +881,9 @@ impl TypeSpace { /// assumes of the container itself (`Default` unconditionally, and /// `Clone`, `Debug`, `Serialize`, `Deserialize` following the /// parameters), which is the conservative answer for a path typify - /// cannot otherwise reason about. Global extra attributes have no + /// cannot otherwise reason about. `with_typify_compat` withholds + /// `Default` from a tuple struct, a unit struct, and a newtype, which + /// typify never derives it for. Global extra attributes have no /// typespace home yet; see the implementation gaps on /// [`TypeSpace::to_stream`] and typespace_ext.rs gap note 3. fn typespace_settings(&self) -> typespace::settings::Settings { @@ -908,7 +910,8 @@ impl TypeSpace { .with_required_trait(typespace::TypespaceTrait::Clone) .with_required_trait(typespace::TypespaceTrait::Debug) .with_desired_trait(typespace::TypespaceTrait::Default) - .with_map_type(map_type); + .with_map_type(map_type) + .with_typify_compat(true); for derive in &self.settings.extra_derives { match derive.as_str() { "::schemars::JsonSchema" | "schemars::JsonSchema" | "JsonSchema" => { From 9a31beb5013b9bc730ef46f327a55669df5b3e13 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Tue, 8 Sep 2026 09:59:43 -0700 Subject: [PATCH 03/23] forward typify's requested traits and attributes into typespace Ask for Eq, PartialEq, Ord, PartialOrd, Hash, Display, FromStr, and Copy as desired traits so typespace derives them where it can, and pass the settings-wide extra attributes through with_attr. --- typify-impl/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 0e210b55..74bbfd1d 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -910,6 +910,14 @@ impl TypeSpace { .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(map_type) .with_typify_compat(true); for derive in &self.settings.extra_derives { @@ -922,6 +930,9 @@ impl TypeSpace { } } } + for attr in &self.settings.extra_attrs { + settings = settings.with_attr(attr.clone()); + } settings = settings.with_struct_builder(self.settings.struct_builder); settings } From a0bde86651f9662646c04016ea6aae43365c5102 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Tue, 8 Sep 2026 13:21:28 -0700 Subject: [PATCH 04/23] update tests --- typify/tests/schemas.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/typify/tests/schemas.rs b/typify/tests/schemas.rs index b5258ecb..dc8d3746 100644 --- a/typify/tests/schemas.rs +++ b/typify/tests/schemas.rs @@ -104,9 +104,7 @@ fn validate_schema( // Make a file with the generated code. let code = quote! { - // TODO 9/3/2026 - // revert - #![allow(warnings)] + #![deny(warnings)] #types From 91077f940ea7d29fffc2512e9579606d0a1e3105 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Wed, 9 Sep 2026 09:38:14 -0700 Subject: [PATCH 05/23] declare JsonSchema on the built-in natives that implement it --- typify-impl/src/convert.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index 816d7547..f606d06d 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -805,7 +805,8 @@ impl TypeSpace { // ::uuid::Uuid additionally implements Display, // FromStr, Eq, PartialEq, Ord, PartialOrd, and Hash. It // also implements Default, but typify does not declare - // Default on natives. + // Default on natives. JsonSchema comes from schemars' + // uuid1 feature. Ok(( TypeEntry::new_native( "::uuid::Uuid", @@ -821,6 +822,7 @@ impl TypeSpace { TypespaceTrait::Ord, TypespaceTrait::PartialOrd, TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, ] .into_iter() .collect::(), @@ -834,7 +836,8 @@ impl TypeSpace { // ::chrono::naive::NaiveDate additionally implements // Display, FromStr, PartialEq, Eq, PartialOrd, Ord, and // Hash. It also implements Default, but typify does not - // declare Default on natives. + // declare Default on natives. JsonSchema comes from + // schemars' chrono feature. Ok(( TypeEntry::new_native( "::chrono::naive::NaiveDate", @@ -850,6 +853,7 @@ impl TypeSpace { TypespaceTrait::Ord, TypespaceTrait::PartialOrd, TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, ] .into_iter() .collect::(), @@ -863,7 +867,8 @@ impl TypeSpace { // PartialOrd, Ord, Hash, Debug, and Display for any // TimeZone, and Utc additionally gets FromStr. It also // implements Default, but typify does not declare - // Default on natives. + // Default on natives. JsonSchema comes from schemars' + // chrono feature. Ok(( TypeEntry::new_native( "::chrono::DateTime<::chrono::offset::Utc>", @@ -879,6 +884,7 @@ impl TypeSpace { TypespaceTrait::Ord, TypespaceTrait::PartialOrd, TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, ] .into_iter() .collect::(), @@ -889,7 +895,7 @@ impl TypeSpace { // ::std::net::IpAddr implements Clone, Eq, PartialEq, // Hash, PartialOrd, Ord, Display, and FromStr. It has no - // Default impl. + // Default impl. JsonSchema needs no schemars feature. Some("ip") => Ok(( TypeEntry::new_native( "::std::net::IpAddr", @@ -905,6 +911,7 @@ impl TypeSpace { TypespaceTrait::Ord, TypespaceTrait::PartialOrd, TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, ] .into_iter() .collect::(), @@ -913,7 +920,8 @@ impl TypeSpace { )), // ::std::net::Ipv4Addr implements Clone, PartialEq, Eq, // Hash, PartialOrd, Ord, Debug, Display, and FromStr. It - // has no Default impl. + // has no Default impl. JsonSchema needs no schemars + // feature. Some("ipv4") => Ok(( TypeEntry::new_native( "::std::net::Ipv4Addr", @@ -929,6 +937,7 @@ impl TypeSpace { TypespaceTrait::Ord, TypespaceTrait::PartialOrd, TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, ] .into_iter() .collect::(), @@ -937,7 +946,8 @@ impl TypeSpace { )), // ::std::net::Ipv6Addr implements Clone, PartialEq, Eq, // Hash, PartialOrd, Ord, Debug, Display, and FromStr. It - // has no Default impl. + // has no Default impl. JsonSchema needs no schemars + // feature. Some("ipv6") => Ok(( TypeEntry::new_native( "::std::net::Ipv6Addr", @@ -953,6 +963,7 @@ impl TypeSpace { TypespaceTrait::Ord, TypespaceTrait::PartialOrd, TypespaceTrait::Hash, + TypespaceTrait::JsonSchema, ] .into_iter() .collect::(), From e26b3cd58b1b3b0acad63c15aec3df124ed0faca Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Wed, 9 Sep 2026 09:43:29 -0700 Subject: [PATCH 06/23] assume JsonSchema on consumer-named natives like the rest of the baseline --- typify-impl/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 74bbfd1d..4119df36 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -404,15 +404,18 @@ impl TypeSpaceImpl { /// 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. - /// `FromStringIrrefutable` has no typespace equivalent (see - /// typespace_ext.rs gap note 2) and is dropped. + /// 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 + /// (see typespace_ext.rs gap note 2) 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::(); From ca3be225328b3bc92086c985190e305e23e49025 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Wed, 9 Sep 2026 17:03:52 -0700 Subject: [PATCH 07/23] less is more --- typify-impl/src/convert.rs | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index f606d06d..c0ed5fb9 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -802,11 +802,8 @@ impl TypeSpace { match format.as_ref().map(String::as_str) { Some("uuid") => { self.uses_uuid = true; - // ::uuid::Uuid additionally implements Display, - // FromStr, Eq, PartialEq, Ord, PartialOrd, and Hash. It - // also implements Default, but typify does not declare - // Default on natives. JsonSchema comes from schemars' - // uuid1 feature. + // ::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", @@ -833,11 +830,8 @@ impl TypeSpace { Some("date") => { self.uses_chrono = true; - // ::chrono::naive::NaiveDate additionally implements - // Display, FromStr, PartialEq, Eq, PartialOrd, Ord, and - // Hash. It also implements Default, but typify does not - // declare Default on natives. JsonSchema comes from - // schemars' chrono feature. + // ::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", @@ -863,12 +857,8 @@ impl TypeSpace { } Some("date-time") => { self.uses_chrono = true; - // ::chrono::DateTime implements PartialEq, Eq, - // PartialOrd, Ord, Hash, Debug, and Display for any - // TimeZone, and Utc additionally gets FromStr. It also - // implements Default, but typify does not declare - // Default on natives. JsonSchema comes from schemars' - // chrono feature. + // ::chrono::DateTime implements Default, but not usefully. + // We assume the chrono feaeture in schemars. Ok(( TypeEntry::new_native( "::chrono::DateTime<::chrono::offset::Utc>", @@ -893,9 +883,7 @@ impl TypeSpace { )) } - // ::std::net::IpAddr implements Clone, Eq, PartialEq, - // Hash, PartialOrd, Ord, Display, and FromStr. It has no - // Default impl. JsonSchema needs no schemars feature. + // ::std::net::IpAddr has no Default impl. Some("ip") => Ok(( TypeEntry::new_native( "::std::net::IpAddr", @@ -918,10 +906,7 @@ impl TypeSpace { ), metadata, )), - // ::std::net::Ipv4Addr implements Clone, PartialEq, Eq, - // Hash, PartialOrd, Ord, Debug, Display, and FromStr. It - // has no Default impl. JsonSchema needs no schemars - // feature. + // ::std::net::Ipv4Addr has no Default impl. Some("ipv4") => Ok(( TypeEntry::new_native( "::std::net::Ipv4Addr", @@ -944,10 +929,7 @@ impl TypeSpace { ), metadata, )), - // ::std::net::Ipv6Addr implements Clone, PartialEq, Eq, - // Hash, PartialOrd, Ord, Debug, Display, and FromStr. It - // has no Default impl. JsonSchema needs no schemars - // feature. + // ::std::net::Ipv6Addr has not Default impl. Some("ipv6") => Ok(( TypeEntry::new_native( "::std::net::Ipv6Addr", From e998b14883081ecf742b7eb38352f611ef28400c Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Thu, 17 Sep 2026 10:40:51 -0700 Subject: [PATCH 08/23] follow typespace's API to its current surface Five breaks, all from typespace changes that landed after the last sync: TraitProvision moved to the crate root when it absorbed TraitDisposition, so settings:: no longer names it. Native::new takes the path as &str. Native::name is gone; path() answers a syn::Type. native_name_match reads the last path segment rather than splitting rendered tokens, which is what it wanted anyway: quoting a path spaces out its separators. view::Type::name answers a Cow borrowed from the typespace, so the typespace has to outlive the name in convert's test. parameter_ident_in and parameter_ident_with_lifetime merged into one parameter_ident(scope, lifetime). --- typify-impl/src/convert.rs | 5 ++++- typify-impl/src/lib.rs | 15 ++++++++++++--- typify-impl/src/type_entry.rs | 4 ++-- typify-impl/tests/test_generation.rs | 14 ++++++++++---- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index c0ed5fb9..a5dc5964 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -2221,7 +2221,10 @@ mod tests { ) .unwrap(); let type_id = type_space.assign_type(ty); - let output = type_space.to_typespace().unwrap().get_type(&type_id).name(); + // 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); diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 4119df36..43f40f6a 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -905,7 +905,7 @@ impl TypeSpace { ) .with_provision( typespace::TypespaceTrait::JsonSchema, - typespace::settings::TraitProvision::IfParameters, + typespace::TraitProvision::IfParameters, ); let mut settings = typespace::settings::Settings::minimal() .with_required_trait(typespace::TypespaceTrait::Serialize) @@ -1113,9 +1113,18 @@ impl TypeSpace { /// 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 { - let native_name = native.name().rsplit("::").next().unwrap(); + // 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, Name::Required(req) if req == native_name) + || matches!( + (type_name, native_name.as_deref()), + (Name::Required(req), Some(name)) if req == name + ) } #[cfg(test)] diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index c7d6b5b5..e4a0bb08 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -377,7 +377,7 @@ impl TypeEntry { type_name: S, traits: typespace::TypespaceTraitSet, ) -> Self { - Type::Native(Native::new(type_name, traits, Vec::new())).into() + Type::Native(Native::new(&type_name.to_string(), traits, Vec::new())).into() } pub(crate) fn new_native_params(type_name: S, params: &[TypeId]) -> Self { @@ -390,7 +390,7 @@ impl TypeEntry { .into_iter() .collect::(); - Type::Native(Native::new(type_name, traits, params.to_vec())).into() + Type::Native(Native::new(&type_name.to_string(), traits, params.to_vec())).into() } pub(crate) fn new_boolean() -> Self { diff --git a/typify-impl/tests/test_generation.rs b/typify-impl/tests/test_generation.rs index f4fa109d..ad9a5e9f 100644 --- a/typify-impl/tests/test_generation.rs +++ b/typify-impl/tests/test_generation.rs @@ -89,10 +89,16 @@ fn test_generation() { let typespace = type_space.to_typespace().unwrap(); let t = typespace.get_type(&tid); let ret = t.ident_in("types"); - let body = t.parameter_ident_in("types"); - let string = typespace.get_type(&string_id).parameter_ident_in("types"); - let opt_int = typespace.get_type(&opt_int_id).parameter_ident_in("types"); - let strenum = typespace.get_type(&strenum_id).parameter_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(); From cdc8cd9164c65003d441911067102edd67ed7970 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sat, 19 Sep 2026 10:23:11 -0700 Subject: [PATCH 09/23] adapt the empty-anyOf test across the rebase test_empty_any_of arrived on main after this branch's previous base and speaks the deleted TypeEntryDetails vocabulary; it now matches on the typespace-backed TypeEntry. --- typify-impl/src/convert.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index a5dc5964..10836af3 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -2381,9 +2381,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:?}"), } } From 8ff0b4039b0a2daf7279eeec13a3f1c2194da301 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sat, 19 Sep 2026 21:43:38 -0700 Subject: [PATCH 10/23] update deps --- Cargo.lock | 27 +++++++++++++++++++++++++-- Cargo.toml | 4 +--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dfcd005c..a33f7f1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,7 +256,7 @@ checksum = "61018db61b1cdf32bed05d3addbd3a49d03be38e1a7674e067e6ded193d0cf49" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1033,6 +1033,27 @@ 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" @@ -1284,6 +1305,7 @@ dependencies = [ [[package]] name = "typespace" version = "0.0.1-alpha.1" +source = "git+https://github.com/oxidecomputer/typespace?branch=main#9b967f500074f2425ce56ff7ef70a1f89c3be4b8" dependencies = [ "codespace", "heck", @@ -1292,7 +1314,8 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 3.0.4", + "strum", + "syn 3.0.5", "thiserror 2.0.20", ] diff --git a/Cargo.toml b/Cargo.toml index 9d9532e5..b391f65a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,7 @@ 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" } -# Discovery: the standalone type-modeling crate that typify's code -# generation is expected to migrate to. Path dependency for now. -typespace = { path = "../typespace/typespace" } +typespace = { git = "https://github.com/oxidecomputer/typespace", branch = "main" } assert_cmd = "2.2.2" chrono = { version = "0.4.45", features = ["serde"] } From 9f27f56b85ad441d1f8e4436d7bc011f419309ad Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sat, 19 Sep 2026 21:47:10 -0700 Subject: [PATCH 11/23] mismerge --- typify-impl/src/lib.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 43f40f6a..f2de6ae5 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -176,18 +176,6 @@ pub(crate) enum DefaultImpl { 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); From 3b6e5904c4e80274eba068226dd39ac1bb3bcc05 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 20 Sep 2026 08:18:51 -0700 Subject: [PATCH 12/23] rip out dead and dying code --- typify-impl/src/defaults.rs | 253 ++----- typify-impl/src/lib.rs | 20 +- typify-impl/src/type_entry.rs | 11 +- typify-impl/src/value.rs | 1191 --------------------------------- 4 files changed, 71 insertions(+), 1404 deletions(-) delete mode 100644 typify-impl/src/value.rs diff --git a/typify-impl/src/defaults.rs b/typify-impl/src/defaults.rs index 40195954..74a5740f 100644 --- a/typify-impl/src/defaults.rs +++ b/typify-impl/src/defaults.rs @@ -2,12 +2,8 @@ use std::collections::BTreeMap; -use proc_macro2::TokenStream; -use quote::quote; - use crate::{ - convert::STD_NUM_NONZERO_PREFIX, type_entry::TypeEntry, DefaultImpl, Error, Result, TypeId, - TypeSpace, + convert::STD_NUM_NONZERO_PREFIX, type_entry::TypeEntry, Error, Result, TypeId, TypeSpace, }; use typespace::build::{ @@ -15,122 +11,7 @@ use typespace::build::{ VariantDetails, }; -/// The disposition of a valid default value; see -/// [`TypeEntry::validate_value`]. -/// -/// typify1 used this to route rendering: `Generic` defaults shared one -/// of the stock functions below, `Specific` defaults got a bespoke -/// generated function, and `Intrinsic` defaults used -/// `#[serde(default)]`. Rendering is now typespace's job -/// (IMPLEMENTATION GAP: typespace renders every explicit default via a -/// runtime `serde_json::from_value` call rather than a typed -/// expression; see the note in `TypeSpace::to_stream`), but validation -/// still uses these distinctions to track which shared impls would be -/// needed. -#[derive(Debug)] -pub(crate) enum DefaultKind { - Intrinsic, - Specific, - Generic(DefaultImpl), -} - -// 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. - if let Some(default) = self.as_type().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.as_type() { - Type::Struct(type_struct) => { - type_struct - .get_properties() - .iter() - .try_for_each(|prop| Self::check_property_defaults(prop, type_space))?; - } - - Type::Enum(type_enum) => { - type_enum.get_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 StructPropertyState::DefaultValue(typespace::build::JsonValue(prop_default)) = - property.state() - { - let type_entry = type_space.id_to_entry.get(property.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", @@ -142,7 +23,7 @@ impl TypeEntry { &self, type_space: &TypeSpace, default: &serde_json::Value, - ) -> Result { + ) -> Result<()> { match self.as_type() { Type::Enum(type_enum) => { let variants = type_enum.get_variants(); @@ -176,15 +57,15 @@ impl TypeEntry { // we'll need a custom function to make a default of the outer // newtype. let _ = validate_type_id(inner, type_space, default)?; - Ok(DefaultKind::Specific) + Ok(()) } Type::Option(type_id) => { if let serde_json::Value::Null = default { - Ok(DefaultKind::Intrinsic) + Ok(()) } else { // Make sure the default is valid for the sub-type. let _ = validate_type_id(type_id, type_space, default)?; - Ok(DefaultKind::Specific) + Ok(()) } } Type::Box(type_id) => validate_type_id(type_id, type_space, default), @@ -192,13 +73,13 @@ impl TypeEntry { Type::Vec(type_id) => { if let serde_json::Value::Array(v) = default { if v.is_empty() { - Ok(DefaultKind::Intrinsic) + Ok(()) } 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) + Ok(()) } } else { Err(Error::invalid_value()) @@ -207,7 +88,7 @@ impl TypeEntry { Type::Map(key_id, value_id) => { if let serde_json::Value::Object(m) = default { if m.is_empty() { - Ok(DefaultKind::Intrinsic) + Ok(()) } 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(); @@ -218,7 +99,7 @@ impl TypeEntry { )?; let _ = value_ty.validate_value(type_space, value)?; } - Ok(DefaultKind::Specific) + Ok(()) } } else { Err(Error::invalid_value()) @@ -227,7 +108,7 @@ impl TypeEntry { Type::Set(type_id) => { if let serde_json::Value::Array(v) = default { if v.is_empty() { - Ok(DefaultKind::Intrinsic) + Ok(()) } else { let type_entry = type_space.id_to_entry.get(type_id).unwrap(); for (i, value) in v.iter().enumerate() { @@ -240,7 +121,7 @@ impl TypeEntry { } let _ = type_entry.validate_value(type_space, value)?; } - Ok(DefaultKind::Specific) + Ok(()) } } else { Err(Error::invalid_value()) @@ -262,11 +143,11 @@ impl TypeEntry { for value in arr { let _ = type_entry.validate_value(type_space, value)?; } - Ok(DefaultKind::Specific) + Ok(()) } Type::Unit => { if let serde_json::Value::Null = default { - Ok(DefaultKind::Intrinsic) + Ok(()) } else { Err(Error::invalid_value()) } @@ -279,35 +160,35 @@ impl TypeEntry { // names to treat as built-in). So we just do no checking and // will fail an `unwrap()` in the generated code if this Value // is not valid for this built-in type. - Ok(DefaultKind::Specific) + Ok(()) } - Type::JsonValue => Ok(DefaultKind::Specific), + Type::JsonValue => Ok(()), Type::Boolean => match default { - serde_json::Value::Bool(false) => Ok(DefaultKind::Intrinsic), - serde_json::Value::Bool(true) => Ok(DefaultKind::Generic(DefaultImpl::Boolean)), + serde_json::Value::Bool(false) => Ok(()), + serde_json::Value::Bool(true) => Ok(()), _ => Err(Error::invalid_value()), }, // Note that min and max values are handled already by the // conversion routines since we have those close at hand. Type::Integer(itype) => match (default.as_u64(), default.as_i64()) { (None, None) => Err(Error::invalid_value()), - (Some(0), _) => Ok(DefaultKind::Intrinsic), + (Some(0), _) => Ok(()), (_, Some(0)) => unreachable!(), (Some(_), _) => { if itype.starts_with(STD_NUM_NONZERO_PREFIX) { - Ok(DefaultKind::Generic(DefaultImpl::NZU64)) + Ok(()) } else { - Ok(DefaultKind::Generic(DefaultImpl::U64)) + Ok(()) } } - (_, Some(_)) => Ok(DefaultKind::Generic(DefaultImpl::I64)), + (_, Some(_)) => Ok(()), }, Type::Float(_) => { if let Some(value) = default.as_f64() { if value == 0.0 { - Ok(DefaultKind::Intrinsic) + Ok(()) } else { - Ok(DefaultKind::Generic(DefaultImpl::I64)) + Ok(()) } } else { Err(Error::invalid_value()) @@ -315,9 +196,9 @@ impl TypeEntry { } Type::String => { if let Some("") = default.as_str() { - Ok(DefaultKind::Intrinsic) + Ok(()) } else { - Ok(DefaultKind::Specific) + Ok(()) } } @@ -332,14 +213,14 @@ pub(crate) fn validate_default_for_external_enum( type_space: &TypeSpace, variants: &[EnumVariant], default: &serde_json::Value, -) -> Option { +) -> Option<()> { if let Some(simple_name) = default.as_str() { let variant = variants .iter() .find(|variant| simple_name == variant.json_name())?; matches!(variant.details(), VariantDetails::Unit).then(|| ())?; - Some(DefaultKind::Specific) + Some(()) } else { let map = default.as_object()?; if map.len() != 1 { @@ -368,7 +249,7 @@ pub(crate) fn validate_default_for_internal_enum( variants: &[EnumVariant], default: &serde_json::Value, tag: &str, -) -> Option { +) -> Option<()> { let map = default.as_object()?; let name = map.get(tag).and_then(serde_json::Value::as_str)?; let variant = variants @@ -376,7 +257,7 @@ pub(crate) fn validate_default_for_internal_enum( .find(|variant| name == variant.json_name())?; match variant.details() { - VariantDetails::Unit => Some(DefaultKind::Specific), + VariantDetails::Unit => Some(()), VariantDetails::Struct(props) => { // Make an object without the tag. let inner_default = serde_json::Value::Object( @@ -399,7 +280,7 @@ pub(crate) fn validate_default_for_adjacent_enum( default: &serde_json::Value, tag: &str, content: &str, -) -> Option { +) -> Option<()> { let map = default.as_object()?; let (tag_value, content_value) = match ( @@ -417,7 +298,7 @@ pub(crate) fn validate_default_for_adjacent_enum( .find(|variant| tag_value == variant.json_name())?; match (variant.details(), content_value) { - (VariantDetails::Unit, None) => Some(DefaultKind::Specific), + (VariantDetails::Unit, None) => Some(()), (VariantDetails::Tuple(tup), Some(content_value)) => { validate_default_tuple(tup, type_space, content_value) } @@ -432,14 +313,14 @@ pub(crate) fn validate_default_for_untagged_enum( type_space: &TypeSpace, variants: &[EnumVariant], default: &serde_json::Value, -) -> Option { +) -> 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::Unit => { default.as_null()?; - Some(DefaultKind::Specific) + Some(()) } VariantDetails::Item(type_id) => validate_type_id(type_id, type_space, default).ok(), VariantDetails::Tuple(tup) => validate_default_tuple(tup, type_space, default), @@ -454,7 +335,7 @@ fn validate_type_id( type_id: &TypeId, type_space: &TypeSpace, default: &serde_json::Value, -) -> Result { +) -> Result<()> { let type_entry = type_space.id_to_entry.get(type_id).unwrap(); type_entry.validate_value(type_space, default) } @@ -463,7 +344,7 @@ fn validate_default_tuple( types: &[TypeId], type_space: &TypeSpace, default: &serde_json::Value, -) -> Option { +) -> Option<()> { let arr = default.as_array()?; if arr.len() != types.len() { return None; @@ -473,14 +354,14 @@ fn validate_default_tuple( .iter() .zip(arr.iter()) .all(|(type_id, value)| validate_type_id(type_id, type_space, value).is_ok()) - .then_some(DefaultKind::Specific) + .then_some(()) } fn validate_default_struct_props( properties: &[StructProperty], type_space: &TypeSpace, default: &serde_json::Value, -) -> Option { +) -> Option<()> { let map = default.as_object()?; // Gather up all properties including those of flattened struct properties: @@ -530,7 +411,7 @@ fn validate_default_struct_props( .filter(|(_, (_, required))| *required) .try_for_each(|(name, _)| map.get(name.as_str()).map(|_| ()))?; - Some(DefaultKind::Specific) + Some(()) } fn all_props<'a>( @@ -590,7 +471,7 @@ mod tests { use serde_json::json; use uuid::Uuid; - use crate::{defaults::DefaultKind, test_util::get_type, type_entry::TypeEntry, DefaultImpl}; + use crate::{test_util::get_type, type_entry::TypeEntry}; #[test] fn test_default_option() { @@ -602,11 +483,11 @@ mod tests { .is_err()); assert!(matches!( type_entry.validate_value(&type_space, &json!(null)), - Ok(DefaultKind::Intrinsic) + Ok(()) )); assert!(matches!( type_entry.validate_value(&type_space, &json!(42)), - Ok(DefaultKind::Specific) + Ok(()) )); } @@ -621,11 +502,11 @@ mod tests { .is_err()); assert!(matches!( type_entry.validate_value(&type_space, &json!(null)), - Ok(DefaultKind::Intrinsic) + Ok(()) )); assert!(matches!( type_entry.validate_value(&type_space, &json!(42)), - Ok(DefaultKind::Specific) + Ok(()) )); } @@ -639,11 +520,11 @@ mod tests { .is_err()); assert!(matches!( type_entry.validate_value(&type_space, &json!([])), - Ok(DefaultKind::Intrinsic), + Ok(()), )); assert!(matches!( type_entry.validate_value(&type_space, &json!([1, 2, 5])), - Ok(DefaultKind::Specific), + Ok(()), )); } @@ -655,11 +536,11 @@ mod tests { assert!(type_entry.validate_value(&type_space, &json!([])).is_err()); assert!(matches!( type_entry.validate_value(&type_space, &json!({})), - Ok(DefaultKind::Intrinsic), + Ok(()), )); assert!(matches!( type_entry.validate_value(&type_space, &json!({"a": 1, "b": 2})), - Ok(DefaultKind::Specific), + Ok(()), )); } @@ -673,7 +554,7 @@ mod tests { .is_err()); assert!(matches!( type_entry.validate_value(&type_space, &json!([1, 2, "three"])), - Ok(DefaultKind::Specific), + Ok(()), )); } @@ -684,7 +565,7 @@ mod tests { assert!(matches!( type_entry.validate_value(&type_space, &json!("not-a-uuid")), - Ok(DefaultKind::Specific) + Ok(()) )); } @@ -695,11 +576,11 @@ mod tests { assert!(matches!( type_entry.validate_value(&type_space, &json!(false)), - Ok(DefaultKind::Intrinsic), + Ok(()), )); assert!(matches!( type_entry.validate_value(&type_space, &json!(true)), - Ok(DefaultKind::Generic(DefaultImpl::Boolean)), + Ok(()), )); } @@ -713,11 +594,11 @@ mod tests { .is_err()); assert!(matches!( type_entry.validate_value(&type_space, &json!(0)), - Ok(DefaultKind::Intrinsic), + Ok(()), )); assert!(matches!( type_entry.validate_value(&type_space, &json!(42)), - Ok(DefaultKind::Generic(DefaultImpl::U64)), + Ok(()), )); let (type_space, type_id) = get_type::(); @@ -725,11 +606,11 @@ mod tests { assert!(matches!( type_entry.validate_value(&type_space, &json!("")), - Ok(DefaultKind::Intrinsic), + Ok(()), )); assert!(matches!( type_entry.validate_value(&type_space, &json!("howdy")), - Ok(DefaultKind::Specific), + Ok(()), )); } @@ -758,7 +639,7 @@ mod tests { } ) ), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(type_entry .validate_value( @@ -801,7 +682,7 @@ mod tests { assert!(matches!( type_entry.validate_value(&type_space, &json!("A")), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(matches!( type_entry.validate_value( @@ -810,7 +691,7 @@ mod tests { "B": ["xx", "yy"] }) ), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(matches!( type_entry.validate_value( @@ -819,7 +700,7 @@ mod tests { "C": { "cc": "xx", "dd": "yy" } }) ), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(type_entry .validate_value(&type_space, &json!({ "A": null })) @@ -847,7 +728,7 @@ mod tests { "tag": "A" }) ), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(matches!( type_entry.validate_value( @@ -858,7 +739,7 @@ mod tests { "dd": "yy" }) ), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(type_entry .validate_value( @@ -900,7 +781,7 @@ mod tests { "tag": "A" }) ), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(matches!( type_entry.validate_value( @@ -910,7 +791,7 @@ mod tests { "content": ["xx", "yy"] }) ), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(matches!( type_entry.validate_value( @@ -920,7 +801,7 @@ mod tests { "content": { "cc": "xx", "dd": "yy" } }) ), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(type_entry.validate_value(&type_space, &json!("A")).is_err()); assert!(type_entry @@ -949,15 +830,15 @@ mod tests { assert!(matches!( type_entry.validate_value(&type_space, &json!(null)), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(matches!( type_entry.validate_value(&type_space, &json!(["xx", "yy"])), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(matches!( type_entry.validate_value(&type_space, &json!( { "cc": "xx", "dd": "yy" })), - Ok(DefaultKind::Specific), + Ok(()), )); assert!(type_entry.validate_value(&type_space, &json!({})).is_err()); } diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index f2de6ae5..55fa2605 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -4,7 +4,7 @@ #![deny(missing_docs)] -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; use conversions::SchemaCache; use log::{debug, info}; @@ -144,9 +144,6 @@ pub struct TypeSpace { settings: TypeSpaceSettings, cache: SchemaCache, - - // Shared functions for generating default values - defaults: BTreeSet, } impl Default for TypeSpace { @@ -163,19 +160,10 @@ impl Default for TypeSpace { uses_regress: 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, -} - /// Type name to use in generated code. #[derive(Clone)] pub struct MapType(pub syn::Type); @@ -697,9 +685,8 @@ 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(); + let type_entry = self.id_to_entry.get(&type_id).unwrap().clone(); debug!("finalizing type entry: {} {:#?}", index, &type_entry); - type_entry.finalize(self)?; self.id_to_entry.insert(type_id, type_entry); } @@ -790,8 +777,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); } diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index e4a0bb08..316bd950 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -27,7 +27,7 @@ use unicode_ident::is_xid_continue; use crate::{ sanitize, util::{get_type_name, metadata_description, unique, TypePatch}, - Case, Name, Result, TypeId, TypeSpace, + Case, Name, TypeId, TypeSpace, }; use typespace::build::{ @@ -424,15 +424,6 @@ impl TypeEntry { } } - pub(crate) fn finalize(&mut self, type_space: &mut TypeSpace) -> Result<()> { - // typify1 cached the enum "bespoke impl" analysis (all-simple- - // variants, untagged Display/FromStr proxies) here; those - // answers are now computed on demand in has_impl since typespace - // has no place to store them (and no rendering that would - // consume them; that's an IMPLEMENTATION GAP--see to_stream). - self.check_defaults(type_space) - } - pub(crate) fn name(&self) -> Option<&str> { match self { Self::Type(typ) => typ.name(), 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() - ) - ); - } -} From 629f19c7ef85c8a04cca4bac45c08e9f5da9e755 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 20 Sep 2026 09:25:10 -0700 Subject: [PATCH 13/23] remove default checking --- Cargo.lock | 2 +- typify-impl/src/convert.rs | 21 - typify-impl/src/defaults.rs | 845 ------------------------------------ typify-impl/src/lib.rs | 7 - 4 files changed, 1 insertion(+), 874 deletions(-) delete mode 100644 typify-impl/src/defaults.rs diff --git a/Cargo.lock b/Cargo.lock index a33f7f1a..34fcf508 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1305,7 +1305,7 @@ dependencies = [ [[package]] name = "typespace" version = "0.0.1-alpha.1" -source = "git+https://github.com/oxidecomputer/typespace?branch=main#9b967f500074f2425ce56ff7ef70a1f89c3be4b8" +source = "git+https://github.com/oxidecomputer/typespace?branch=main#6086e2a8916ab88a45b283b894dac5a15fef1339" dependencies = [ "codespace", "heck", diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index 10836af3..03251aa6 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -17,8 +17,6 @@ use crate::util::get_type_name; use crate::{Error, Name, Result, TypeSpace}; -pub const STD_NUM_NONZERO_PREFIX: &str = "::std::num::NonZero"; - impl TypeSpace { pub(crate) fn convert_schema<'a>( &mut self, @@ -1772,13 +1770,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( @@ -1838,13 +1829,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( @@ -2043,11 +2027,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( diff --git a/typify-impl/src/defaults.rs b/typify-impl/src/defaults.rs deleted file mode 100644 index 74a5740f..00000000 --- a/typify-impl/src/defaults.rs +++ /dev/null @@ -1,845 +0,0 @@ -// Copyright 2026 Oxide Computer Company - -use std::collections::BTreeMap; - -use crate::{ - convert::STD_NUM_NONZERO_PREFIX, type_entry::TypeEntry, Error, Result, TypeId, TypeSpace, -}; - -use typespace::build::{ - EnumTagType, EnumVariant, StructProperty, StructPropertySerde, StructPropertyState, Type, - VariantDetails, -}; - -impl TypeEntry { - /// 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. - /// - /// [`Value`]: serde_json::Value - pub(crate) fn validate_value( - &self, - type_space: &TypeSpace, - default: &serde_json::Value, - ) -> Result<()> { - match self.as_type() { - Type::Enum(type_enum) => { - let variants = type_enum.get_variants(); - match type_enum.get_tag_type().expect("built enum has a 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) - } - } - } - Type::Struct(type_struct) => { - validate_default_struct_props(type_struct.get_properties(), type_space, default) - .ok_or_else(Error::invalid_value) - } - - Type::NewtypeStruct(type_newtype) => { - let inner = type_newtype.get_inner(); - // 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(inner, type_space, default)?; - Ok(()) - } - Type::Option(type_id) => { - if let serde_json::Value::Null = default { - Ok(()) - } else { - // Make sure the default is valid for the sub-type. - let _ = validate_type_id(type_id, type_space, default)?; - Ok(()) - } - } - Type::Box(type_id) => validate_type_id(type_id, type_space, default), - - Type::Vec(type_id) => { - if let serde_json::Value::Array(v) = default { - if v.is_empty() { - Ok(()) - } 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(()) - } - } else { - Err(Error::invalid_value()) - } - } - Type::Map(key_id, value_id) => { - if let serde_json::Value::Object(m) = default { - if m.is_empty() { - Ok(()) - } 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(()) - } - } else { - Err(Error::invalid_value()) - } - } - Type::Set(type_id) => { - if let serde_json::Value::Array(v) = default { - if v.is_empty() { - Ok(()) - } 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(()) - } - } else { - Err(Error::invalid_value()) - } - } - Type::Tuple(ids) => { - validate_default_tuple(ids, type_space, default).ok_or_else(Error::invalid_value) - } - - Type::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(()) - } - Type::Unit => { - if let serde_json::Value::Null = default { - Ok(()) - } else { - Err(Error::invalid_value()) - } - } - Type::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 generated code if this Value - // is not valid for this built-in type. - Ok(()) - } - Type::JsonValue => Ok(()), - Type::Boolean => match default { - serde_json::Value::Bool(false) => Ok(()), - serde_json::Value::Bool(true) => Ok(()), - _ => Err(Error::invalid_value()), - }, - // Note that min and max values are handled already by the - // conversion routines since we have those close at hand. - Type::Integer(itype) => match (default.as_u64(), default.as_i64()) { - (None, None) => Err(Error::invalid_value()), - (Some(0), _) => Ok(()), - (_, Some(0)) => unreachable!(), - (Some(_), _) => { - if itype.starts_with(STD_NUM_NONZERO_PREFIX) { - Ok(()) - } else { - Ok(()) - } - } - (_, Some(_)) => Ok(()), - }, - Type::Float(_) => { - if let Some(value) = default.as_f64() { - if value == 0.0 { - Ok(()) - } else { - Ok(()) - } - } else { - Err(Error::invalid_value()) - } - } - Type::String => { - if let Some("") = default.as_str() { - Ok(()) - } else { - Ok(()) - } - } - - // typify never constructs unit structs, tuple structs, or - // type aliases; Type is non_exhaustive besides. - _ => unreachable!("unexpected typespace type variant"), - } - } -} - -pub(crate) fn validate_default_for_external_enum( - type_space: &TypeSpace, - variants: &[EnumVariant], - default: &serde_json::Value, -) -> Option<()> { - if let Some(simple_name) = default.as_str() { - let variant = variants - .iter() - .find(|variant| simple_name == variant.json_name())?; - matches!(variant.details(), VariantDetails::Unit).then(|| ())?; - - Some(()) - } 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.json_name())?; - - match variant.details() { - VariantDetails::Unit => 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: &[EnumVariant], - 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.json_name())?; - - match variant.details() { - VariantDetails::Unit => Some(()), - 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: &[EnumVariant], - 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.json_name())?; - - match (variant.details(), content_value) { - (VariantDetails::Unit, None) => Some(()), - (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: &[EnumVariant], - 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::Unit => { - default.as_null()?; - Some(()) - } - 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(()) -} - -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.as_str()).map(|_| ()))?; - - Some(()) -} - -fn all_props<'a>( - property: &'a StructProperty, - type_space: &'a TypeSpace, -) -> Vec<(Option, &'a TypeId, bool)> { - // The JSON name of the property; typespace stores the Rust name and - // an optional serde rename, so the JSON name is the rename when - // present and the Rust name otherwise. - let maybe_name = match property.json_name() { - StructPropertySerde::None => Some(property.rust_name().to_string()), - StructPropertySerde::Rename(rename) => Some(rename.clone()), - StructPropertySerde::Flatten => None, - }; - - if let Some(name) = maybe_name { - let required = matches!(property.state(), StructPropertyState::Required); - - 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.as_type() { - Type::Struct(type_struct) => { - let optional = matches!(property.state(), StructPropertyState::Optional); - (type_struct.get_properties(), !optional) - } - Type::Option(type_id) => { - let type_entry = type_space.id_to_entry.get(type_id).unwrap(); - if let Type::Struct(type_struct) = type_entry.as_type() { - (type_struct.get_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" - Type::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::TypeEntry}; - - #[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(()) - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(42)), - Ok(()) - )); - } - - #[test] - fn test_default_box() { - let (type_space, type_id) = get_type::>(); - - let type_entry = TypeEntry::from(typespace::build::Type::Box(type_id)); - - assert!(type_entry - .validate_value(&type_space, &json!("forty-two")) - .is_err()); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(null)), - Ok(()) - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(42)), - Ok(()) - )); - } - - #[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(()), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!([1, 2, 5])), - Ok(()), - )); - } - - #[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(()), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!({"a": 1, "b": 2})), - Ok(()), - )); - } - - #[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(()), - )); - } - - #[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(()) - )); - } - - #[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(()), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(true)), - Ok(()), - )); - } - - #[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(()), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(42)), - Ok(()), - )); - - 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(()), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!("howdy")), - Ok(()), - )); - } - - #[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(()), - )); - 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(()), - )); - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "B": ["xx", "yy"] - }) - ), - Ok(()), - )); - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "C": { "cc": "xx", "dd": "yy" } - }) - ), - Ok(()), - )); - 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(()), - )); - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "tag": "C", - "cc": "xx", - "dd": "yy" - }) - ), - Ok(()), - )); - 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(()), - )); - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "tag": "B", - "content": ["xx", "yy"] - }) - ), - Ok(()), - )); - assert!(matches!( - type_entry.validate_value( - &type_space, - &json!({ - "tag": "C", - "content": { "cc": "xx", "dd": "yy" } - }) - ), - Ok(()), - )); - 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(()), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!(["xx", "yy"])), - Ok(()), - )); - assert!(matches!( - type_entry.validate_value(&type_space, &json!( { "cc": "xx", "dd": "yy" })), - Ok(()), - )); - assert!(type_entry.validate_value(&type_space, &json!({})).is_err()); - } -} diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 55fa2605..ba7b58dc 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -31,7 +31,6 @@ mod test_util; mod conversions; mod convert; mod cycles; -mod defaults; mod enums; mod merge; mod rust_extension; @@ -57,12 +56,6 @@ pub enum Error { }, } -impl Error { - fn invalid_value() -> Self { - Self::InvalidValue - } -} - #[allow(missing_docs)] pub type Result = std::result::Result; From 627e6b6f774ad5bda956d0cbc27ba63eaa543bcf Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 20 Sep 2026 14:24:00 -0700 Subject: [PATCH 14/23] pruning and cleanup --- typify-impl/src/cycles.rs | 222 ------------------------------- typify-impl/src/lib.rs | 50 +------ typify-impl/src/type_entry.rs | 20 +-- typify-impl/src/typespace_ext.rs | 101 -------------- 4 files changed, 6 insertions(+), 387 deletions(-) delete mode 100644 typify-impl/src/cycles.rs delete mode 100644 typify-impl/src/typespace_ext.rs diff --git a/typify-impl/src/cycles.rs b/typify-impl/src/cycles.rs deleted file mode 100644 index 1cb7dfc0..00000000 --- a/typify-impl/src/cycles.rs +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright 2023 Oxide Computer Company - -use std::{ - collections::{BTreeMap, BTreeSet}, - ops::Range, -}; - -use crate::{type_entry::TypeEntry, 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. -/// -/// typespace models exactly this notion as `Type::contained_children_mut` -/// (finalization uses it for its own cycle breaking), so we delegate to -/// it. typify continues to break cycles itself--before entry -/// finalization--because queries such as `has_impl` recurse through -/// contained types and must not encounter a cycle. -fn get_child_ids(type_entry: &mut TypeEntry) -> Vec<&mut TypeId> { - match type_entry { - TypeEntry::Type(typ) => typ.contained_children_mut(), - TypeEntry::Reference(_) => 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/lib.rs b/typify-impl/src/lib.rs index ba7b58dc..e18f32d2 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; use conversions::SchemaCache; -use log::{debug, info}; +use log::info; use proc_macro2::TokenStream; use quote::ToTokens; use schemars::schema::{Metadata, RootSchema, Schema}; @@ -30,13 +30,11 @@ mod test_util; mod conversions; mod convert; -mod cycles; mod enums; mod merge; mod rust_extension; mod structs; mod type_entry; -mod typespace_ext; mod util; mod validate; @@ -377,7 +375,7 @@ impl TypeSpaceImpl { /// 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 - /// (see typespace_ext.rs gap note 2) and is dropped. + /// and is dropped. pub(crate) fn native_traits(impls: &[Self]) -> typespace::TypespaceTraitSet { let mut traits = [ typespace::TypespaceTrait::Clone, @@ -671,15 +669,10 @@ impl TypeSpace { } } - // 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 type_entry = self.id_to_entry.get(&type_id).unwrap().clone(); - debug!("finalizing type entry: {} {:#?}", index, &type_entry); self.id_to_entry.insert(type_id, type_entry); } @@ -853,9 +846,7 @@ impl TypeSpace { /// parameters), which is the conservative answer for a path typify /// cannot otherwise reason about. `with_typify_compat` withholds /// `Default` from a tuple struct, a unit struct, and a newtype, which - /// typify never derives it for. Global extra attributes have no - /// typespace home yet; see the implementation gaps on - /// [`TypeSpace::to_stream`] and typespace_ext.rs gap note 3. + /// typify never derives it for. fn typespace_settings(&self) -> typespace::settings::Settings { let map_key_obligation = [ typespace::TypespaceTrait::Hash, @@ -930,12 +921,6 @@ impl TypeSpace { } } - // typify breaks containment cycles itself (see cycles.rs) by - // inserting Box entries before output, so typespace finalization - // should never need to manufacture a Box of its own. Provide a - // real generator anyway--if it ever fires we'd rather get a - // distinctive id than a panic--by mapping the inner id into a - // range that TypeSpace::assign never produces. Ok(builder.finalize(|inner: &TypeId| TypeId(inner.0 | (1 << 63)))?) } @@ -945,29 +930,7 @@ impl TypeSpace { /// inserted into a `TypespaceBuilder`, finalized, and rendered /// through codespace. Finalization errors (dangling references, /// name collisions, unsatisfiable trait requirements) surface as - /// [`Error::Typespace`]. Known IMPLEMENTATION GAPS relative to - /// typify1's renderer (each of these is a construct typespace can - /// or should express but does not yet render): - /// - /// - no `error` module / `ConversionError` type (nothing typespace - /// emits refers to it yet, so the output is self-consistent); - /// this method injects the module itself to match typify1's - /// unconditional emission; - /// - no bespoke enum impls: Display/FromStr for all-simple-variant - /// enums, untagged Display/FromStr proxies, TryFrom<&str>, and - /// the convenience `From` impls; - /// - no `impl Default` for whole-type default values, and explicit - /// property defaults are runtime `serde_json::from_value` calls - /// rather than typify1's typed expressions (see value.rs, retained - /// unbuilt for reference) with shared `defaults::` helpers; - /// - no string-constraint validation (min/max length, pattern) on - /// newtypes, and no value-list constraints at all; - /// - no `#[serde(deny_unknown_fields)]` on structs or enums; - /// - no derives beyond serde and the settings-wide extra derives - /// (typify1 derived Debug/Clone always, Copy/Eq/Ord/Hash for - /// simple enums, and applied per-type derives and attrs from - /// patches); - /// - no struct builder types (TypeSpaceSettings::with_struct_builder); + /// [`Error::Typespace`]. pub fn to_stream(&self) -> Result { let typespace = self.to_typespace()?; @@ -1069,11 +1032,6 @@ impl TypeSpace { } typespace::build::Type::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(typespace::build::Type::Box(id.clone()).into()) - } } /// Whether a native type's name matches the required name for a diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index 316bd950..a415994d 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -10,8 +10,7 @@ //! (see `TypeSpace::to_stream`). What remains here is: //! //! - `TypeEntry`, which wraps a typespace type or an in-flight -//! reference (see the gap notes in typespace_ext.rs for the -//! typify-specific data typespace has no home for); +//! reference. //! - the `from_metadata` constructors that turn schema metadata into //! named typespace types. //! @@ -178,8 +177,7 @@ impl TypeEntryEnum { let name = get_type_name(&type_name, metadata).unwrap(); // INTERFACE GAP: container-level serde rename (set when a patch - // renames the type) has no home in typespace's TypeEnum; see - // typespace_ext.rs gap note 4. + // renames the type) has no home in typespace's TypeEnum. let description = metadata_description(metadata); let type_patch = TypePatch::new(type_space, name); @@ -290,17 +288,9 @@ impl TypeEntryNewtype { enum_values: &[serde_json::Value], schema: Schema, ) -> TypeEntry { - // INTERFACE GAP: typespace has no representation for a newtype - // constrained to a fixed set of permitted values; the validation - // (TryFrom constructor plus bespoke Deserialize impl in typify1) - // is dropped. The ideal call is: - // let constraints = NewtypeConstraints::AllowList( enum_values.iter().cloned().map(JsonValue::new).collect(), ); - // - // See typespace_ext.rs gap note 1. - let _ = enum_values; Self::make( type_space, type_name, @@ -319,14 +309,8 @@ impl TypeEntryNewtype { enum_values: &[serde_json::Value], schema: Schema, ) -> TypeEntry { - // INTERFACE GAP: as with from_metadata_with_enum_values, but for - // a set of rejected values. The ideal call is: - // let constraints = NewtypeConstraints::DenyList(enum_values.iter().cloned().map(JsonValue::new).collect()); - // - // See typespace_ext.rs gap note 1. - let _ = enum_values; Self::make( type_space, type_name, diff --git a/typify-impl/src/typespace_ext.rs b/typify-impl/src/typespace_ext.rs deleted file mode 100644 index c8e4e29e..00000000 --- a/typify-impl/src/typespace_ext.rs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2026 Oxide Computer Company - -//! Interface gaps between typify and the typespace crate. -//! -//! This file used to shim proposed typespace interfaces (a `TypeExt` -//! trait with `name`/`set_default`/`dedup_key`, a local `DedupKey` -//! mirror, `EnumVariant::json_name`, and `Enum::all_simple_variants`); -//! typespace now provides all of them and the shims are gone. What -//! remains is the record of gaps that cannot be shimmed from outside -//! the crate; each is annotated at its call site as well. -//! -//! 1. INTERFACE GAP: value-list newtype constraints. -//! `NewtypeConstraints` is non_exhaustive, so new variants cannot -//! be added here. typify supports newtypes constrained to a fixed -//! set of permitted values (`enum` with a non-string type) or a set -//! of rejected values (`not`/`enum`), rendered as a TryFrom -//! constructor plus a bespoke Deserialize impl. Proposed: -//! -//! ```text -//! pub enum NewtypeConstraints { -//! None, -//! EnumValues(Vec), -//! DenyValues(Vec), -//! String { ... }, -//! Array { ... }, -//! } -//! ``` -//! -//! Call sites: `TypeEntryNewtype::from_metadata_with_enum_values` -//! and `from_metadata_with_deny_values` in type_entry.rs, which -//! currently construct `NewtypeConstraints::None` and drop the -//! validation. -//! -//! 2. INTERFACE GAP (narrowed): native capability markers. -//! `TypespaceTrait::Default` now exists and typify maps declared -//! `Default` impls onto it. The remaining piece is -//! `FromStringIrrefutable`: typespace tracks that capability -//! crate-privately (fed by `Native::new_string_like`), so a -//! consumer-declared irrefutable-FromStr native (from x-rust-type -//! or a conversion policy) has no typespace home. typify keeps its -//! `Vec` side channel on `TypeEntry` for that one -//! marker. With typify's own `has_impl` machinery deleted and the -//! structural-dedup lookup that used to pair this with typespace's -//! `DedupKey` also removed, the side channel is unconsumed data -//! retained at the collection sites; no query can see it. The -//! query-side loss is recorded in gap 7. -//! -//! 3. INTERFACE GAP (narrowed): rendering of extra derives and -//! attributes. Every path into typespace exists: the settings-wide -//! derives go to typespace's `with_derive` (see -//! `TypeSpace::typespace_settings`), and a -//! `TypeSpacePatch`'s `with_derive` / `with_attr` values reach the -//! named shapes' `extra_derives` / `extra_attrs` (see `TypePatch` -//! in util.rs and its three call sites in type_entry.rs). What -//! remains is on typespace's side: it renders only the -//! settings-wide derives, so the settings-wide attrs and both -//! per-type lists are stored and never emitted. -//! -//! 4. INTERFACE GAP (latent): container-level serde rename. typify1's -//! renderer supported `#[serde(rename = "...")]` on the container, -//! though nothing ever populated the field (the patch mechanism -//! renames the type without preserving the serialized name). The -//! shapes have `name` but no `rename`; if typespace wants to fix -//! the underlying bug--a renamed type should keep its wire -//! name--the shape metadata is where the rename belongs. Call -//! sites: the INTERFACE GAP comments in `TypeEntry*::from_metadata` -//! in type_entry.rs. -//! -//! 5. CLOSED (was: pre-finalization identifier rendering). typify's -//! `TypeEntry::type_ident` / `type_parameter_ident` are gone; -//! identifier queries go through typespace's view API on the -//! finalized typespace (`ident`, `ident_in`, `parameter_ident`, -//! `parameter_ident_in`, `parameter_ident_with_lifetime`), with -//! scope as a per-query argument. The `type_mod` setting is gone -//! with them: callers pass the scope where they ask for -//! identifiers. Behavioral differences from typify1's renderer: -//! maps render as BTreeMap until `with_map_type` is forwarded to -//! typespace settings (a separate, gated change); typespace passes -//! `String` parameters by value where typify1 used `&str`; and -//! typespace passes all enums by reference where typify1 passed -//! all-simple-variant enums by value. -//! -//! 6. CLOSED (was: fallible finalization at render time). -//! `TypeSpace::to_stream` and `TypeSpace::to_typespace` return -//! `Result`; typespace finalization errors surface as -//! `Error::Typespace` instead of a panic. Consumers (typify-macro, -//! cargo-typify, build scripts) propagate or unwrap them. -//! -//! 7. INTERFACE GAP: queries the deleted typify facade answered that -//! typespace's view API cannot. `view::Type::has_impl` takes -//! typespace's `TypeSpaceImpl` (Display/FromStr/Eq/Ord/Hash), so -//! there is no way to ask for `Default` (though -//! `TypespaceTrait::Default` exists and propagates) or -//! `FromStringIrrefutable` (crate-private; see gap 2). typify's -//! enum-analysis answers (all-simple-variant enums implement -//! Display/FromStr, untagged newtype-variant proxies) went with -//! `has_impl` and have no typespace equivalent until the bespoke -//! impls render. Also gone with no replacement: `Type::describe` -//! (debug summary string) and `Type::builder` (the -//! `mod builder` ident for struct_builder settings; typespace does -//! not render builders at all). From 65593ffaa558218546a6a2a7beb92f343ee31fa7 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 20 Sep 2026 14:26:01 -0700 Subject: [PATCH 15/23] add inserted_type --- typify-impl/src/lib.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index e18f32d2..067c7e98 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -898,6 +898,22 @@ impl TypeSpace { settings } + /// 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, + } + } + /// Finalize the collected types into a [`typespace::Typespace`]. /// /// The typespace is the query surface for the collected types: use From 2cfa6444ad59645a823525fa5b3c23cc98633635 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 20 Sep 2026 17:15:21 -0700 Subject: [PATCH 16/23] clean up stale comments --- Cargo.lock | 2 +- typify-impl/src/lib.rs | 5 ----- typify-impl/src/type_entry.rs | 33 +++++---------------------------- 3 files changed, 6 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34fcf508..9e1bc7af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1305,7 +1305,7 @@ dependencies = [ [[package]] name = "typespace" version = "0.0.1-alpha.1" -source = "git+https://github.com/oxidecomputer/typespace?branch=main#6086e2a8916ab88a45b283b894dac5a15fef1339" +source = "git+https://github.com/oxidecomputer/typespace?branch=main#03dda159e7e70e46788a98a629507475a281ccc0" dependencies = [ "codespace", "heck", diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 067c7e98..980170ee 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -240,11 +240,6 @@ impl From for MapType { } /// Settings that alter type generation. -// [typespace migration note] typify1 had a `type_mod` setting that -// qualified every named-type identifier query with a module path; -// typespace makes scope a per-query argument (`ident_in`, -// `parameter_ident_in`), so the setting is gone and callers pass the -// scope where they ask for identifiers. #[derive(Default, Debug, Clone)] pub struct TypeSpaceSettings { extra_derives: Vec, diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index a415994d..598bda76 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -34,14 +34,6 @@ use typespace::build::{ StructProperty, Type, VariantDetails, }; -// [typespace migration note] Struct-variant property names were -// `&'a str` borrowed out of typify's own representation; typespace's -// `StructProperty::rust_name` is a `syn::Ident`, which can only give up -// its name as an owned String. INTERFACE GAP: consider `rust_name() -// -> &str` (or storing the name as a String) in typespace. This applies -// to typespace's view module too, whose property names are owned -// Strings for the same reason. - /// A typespace type under construction, or an in-flight reference to /// another type ID. /// @@ -176,8 +168,6 @@ impl TypeEntryEnum { .collect(); let name = get_type_name(&type_name, metadata).unwrap(); - // INTERFACE GAP: container-level serde rename (set when a patch - // renames the type) has no home in typespace's TypeEnum. let description = metadata_description(metadata); let type_patch = TypePatch::new(type_space, name); @@ -208,8 +198,6 @@ impl TypeEntryStruct { schema: Schema, ) -> TypeEntry { let name = get_type_name(&type_name, metadata).unwrap(); - // INTERFACE GAP: container-level serde rename; see - // TypeEntryEnum::from_metadata. let description = metadata_description(metadata); let default = metadata .as_ref() @@ -245,8 +233,6 @@ impl TypeEntryNewtype { schema: Schema, ) -> TypeEntry { let name = get_type_name(&type_name, metadata).unwrap(); - // INTERFACE GAP: container-level serde rename; see - // TypeEntryEnum::from_metadata. let description = metadata_description(metadata); let type_patch = TypePatch::new(type_space, name); @@ -416,21 +402,12 @@ impl TypeEntry { } } -/// Flatten typify's doc block into typespace's single description -/// string. -/// -/// typify rendered a type's documentation as a description attribute -/// followed by a `
` block containing the pretty-printed JSON -/// schema, as line-by-line `#[doc]` attributes, falling back to -/// `` `Name` `` when the schema has no description. +/// A named type's description: the schema's, or `` `Name` `` when the +/// schema has none. /// -/// INTERFACE GAP: typespace's TypeCommon has only a single description -/// string with no structured place for the schema block, so the whole -/// thing is flattened into one multi-line string here (which typespace -/// emits as one `#[doc = "..."]` attribute). The rendered rustdoc is -/// equivalent but the generated source is not. It also means the schema -/// must be captured at construction time rather than carried alongside -/// the type as typify1 did. +/// 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(), From 9e2c03fa74a69c75b60c2b424b1fb221b6f17091 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 27 Sep 2026 18:00:46 -0700 Subject: [PATCH 17/23] hand rendering settings to typespace TypeSpaceSettings keeps only what governs conversion (replacements, patches, conversions, crate policy) and carries a typespace::Settings for everything about rendering; map_typespace_settings adjusts it and new() holds typify's defaults. The extra_derives, extra_attrs, struct_builder, and map_type fields and MapType are gone. Derive names from the CLI and the macro go through TraitSpec::parse, so typespace decides whether a name is a trait it models or a foreign derive; a bare foreign name is now an error. A map named by path takes the HashMap preset with that path, since a JSON map key is always string-like. --- Cargo.lock | 8 +- cargo-typify/Cargo.toml | 1 + cargo-typify/src/lib.rs | 57 ++-- cargo-typify/tests/integration.rs | 6 +- cargo-typify/tests/outputs/derive.rs | 10 +- cargo-typify/tests/outputs/multi_derive.rs | 25 +- example-build/build.rs | 4 +- typify-impl/src/enums.rs | 17 +- typify-impl/src/lib.rs | 300 +++++---------------- typify-impl/tests/generator.out | 14 +- typify-impl/tests/test_generation.rs | 7 +- typify-macro/src/lib.rs | 42 ++- typify-test/build.rs | 7 +- typify/src/lib.rs | 1 - typify/tests/schemas.rs | 20 +- 15 files changed, 221 insertions(+), 298 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e1bc7af..fbb20fc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,7 @@ dependencies = [ "schemars", "semver", "serde_json", + "syn 3.0.6", "tempfile", "typify", ] @@ -256,7 +257,7 @@ checksum = "61018db61b1cdf32bed05d3addbd3a49d03be38e1a7674e067e6ded193d0cf49" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 3.0.6", ] [[package]] @@ -1305,17 +1306,18 @@ dependencies = [ [[package]] name = "typespace" version = "0.0.1-alpha.1" -source = "git+https://github.com/oxidecomputer/typespace?branch=main#03dda159e7e70e46788a98a629507475a281ccc0" +source = "git+https://github.com/oxidecomputer/typespace?branch=main#391d823b72351a8b8d97856e2e5d0d0f529f2a1b" dependencies = [ "codespace", "heck", "log", "proc-macro2", "quote", + "schemars", "serde", "serde_json", "strum", - "syn 3.0.5", + "syn 3.0.6", "thiserror 2.0.20", ] diff --git a/cargo-typify/Cargo.toml b/cargo-typify/Cargo.toml index 95717a21..ae860a92 100644 --- a/cargo-typify/Cargo.toml +++ b/cargo-typify/Cargo.toml @@ -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 01c7d2e2..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, 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 1fb32b19..6fd8e080 100644 --- a/example-build/build.rs +++ b/example-build/build.rs @@ -8,7 +8,9 @@ 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 = diff --git a/typify-impl/src/enums.rs b/typify-impl/src/enums.rs index 5e296cbd..745d8a9f 100644 --- a/typify-impl/src/enums.rs +++ b/typify-impl/src/enums.rs @@ -745,6 +745,7 @@ mod tests { use quote::quote; use typespace::build::{EnumTagType, Type, VariantDetails}; + use typespace::settings::ForeignTrait; #[allow(dead_code)] #[derive(Serialize, JsonSchema, Schema)] @@ -1318,13 +1319,12 @@ mod tests { #[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 @@ -1338,10 +1338,9 @@ mod tests { let _ = type_space.assign_type(type_entry); let actual = render_items_for(&type_space, "ResultX"); - // typify1's output; see test_result. 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), diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 980170ee..5e9b0870 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -9,7 +9,6 @@ use std::collections::BTreeMap; use conversions::SchemaCache; use log::info; use proc_macro2::TokenStream; -use quote::ToTokens; use schemars::schema::{Metadata, RootSchema, Schema}; use thiserror::Error; use type_entry::{TypeEntry, TypeEntryNewtype}; @@ -155,104 +154,91 @@ impl Default for TypeSpace { } } -/// 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") + Self::new() } } -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()) - } -} - -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) - } -} - -impl From for MapType { - fn from(t: syn::Type) -> Self { - Self(t) + let typespace = std::mem::replace( + &mut self.typespace, + typespace::settings::Settings::minimal(), + ); + self.typespace = f(typespace); + self } } -/// Settings that alter type generation. -#[derive(Default, Debug, Clone)] -pub struct TypeSpaceSettings { - 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)] @@ -394,28 +380,6 @@ impl TypeSpaceImpl { } impl TypeSpaceSettings { - /// 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); - } - 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 - } - /// 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. @@ -462,7 +426,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()), @@ -518,25 +482,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 { @@ -820,79 +765,6 @@ impl TypeSpace { self.uses_uuid } - /// The typespace settings derived from typify's settings. - /// - /// typify has always emitted `Serialize`, `Deserialize`, `Clone`, and - /// `Debug` on every generated type, so those four are requested as - /// required traits regardless of user settings. Global extra derives - /// forward to typespace's settings-wide `with_derive`, and the map - /// type forwards to `with_map_type` along with the traits a map key - /// requires. `ContainerType::new` accepts an arbitrary type path, so - /// typify cannot know whether the configured container needs - /// hashed-lookup traits (`Hash`, `Eq`, `PartialEq`) or ordered-lookup - /// traits (`Ord`, `PartialOrd`, along with `Eq` and `PartialEq`, which - /// `Ord` and `PartialOrd` require); the union of both is forwarded as - /// the key obligation, matching typify's own generated key types, - /// which always derive all five (see - /// `typify/tests/schemas/maps_custom.rs`); the value obligation is - /// empty. `ContainerType::new` also claims only what rendering - /// assumes of the container itself (`Default` unconditionally, and - /// `Clone`, `Debug`, `Serialize`, `Deserialize` following the - /// parameters), which is the conservative answer for a path typify - /// cannot otherwise reason about. `with_typify_compat` withholds - /// `Default` from a tuple struct, a unit struct, and a newtype, which - /// typify never derives it for. - fn typespace_settings(&self) -> typespace::settings::Settings { - let map_key_obligation = [ - typespace::TypespaceTrait::Hash, - typespace::TypespaceTrait::Eq, - typespace::TypespaceTrait::PartialEq, - typespace::TypespaceTrait::Ord, - typespace::TypespaceTrait::PartialOrd, - ] - .into_iter() - .collect::(); - let map_type = typespace::settings::ContainerType::new( - &self.settings.map_type.0.to_token_stream().to_string(), - [map_key_obligation, typespace::TypespaceTraitSet::empty()], - ) - .with_provision( - typespace::TypespaceTrait::JsonSchema, - typespace::TraitProvision::IfParameters, - ); - let mut 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(map_type) - .with_typify_compat(true); - for derive in &self.settings.extra_derives { - match derive.as_str() { - "::schemars::JsonSchema" | "schemars::JsonSchema" | "JsonSchema" => { - settings = settings.with_required_trait(typespace::TypespaceTrait::JsonSchema) - } - _ => { - settings = settings.with_derive(derive.clone()); - } - } - } - for attr in &self.settings.extra_attrs { - settings = settings.with_attr(attr.clone()); - } - settings = settings.with_struct_builder(self.settings.struct_builder); - settings - } - /// The type inserted under `type_id`, as it was inserted. /// /// This is the declaration typify handed typespace, available before @@ -917,7 +789,7 @@ impl TypeSpace { /// 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.typespace_settings()); + let mut builder = typespace::TypespaceBuilder::new(self.settings.typespace.clone()); for (type_id, type_entry) in &self.id_to_entry { match type_entry { @@ -1072,36 +944,10 @@ mod tests { use std::collections::HashSet; use crate::{ - test_util::validate_output, type_entry::TypeEntry, MapType, Name, TypeSpace, - TypeSpaceSettings, + test_util::validate_output, type_entry::TypeEntry, Name, TypeSpace, TypeSpaceSettings, }; use typespace::build::{Type, VariantDetails}; - #[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(); - } - #[allow(dead_code)] #[derive(Serialize, JsonSchema)] struct Blah { 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_generation.rs b/typify-impl/tests/test_generation.rs index ad9a5e9f..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,8 +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_struct_builder(true) + .map_typespace_settings(|s| { + s.with_required_trait(TypespaceTrait::JsonSchema) + .with_struct_builder(true) + }) .with_patch( "AllTheTraits", TypeSpacePatch::default() diff --git a/typify-macro/src/lib.rs b/typify-macro/src/lib.rs index 8e0f591f..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) }; diff --git a/typify-test/build.rs b/typify-test/build.rs index 975fa449..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)] @@ -198,9 +199,9 @@ fn main() { 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); diff --git a/typify/src/lib.rs b/typify/src/lib.rs index b75d8d21..a712b3a6 100644 --- a/typify/src/lib.rs +++ b/typify/src/lib.rs @@ -151,7 +151,6 @@ 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::TypeId; pub use typify_impl::TypeSpace; pub use typify_impl::TypeSpaceImpl; diff --git a/typify/tests/schemas.rs b/typify/tests/schemas.rs index dc8d3746..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,7 @@ 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.clone(), out_path, &mut TypeSpaceSettings::default()) + validate_schema(entry.clone(), out_path, TypeSpaceSettings::default()) .expect(&format!("failed for {}", entry.to_string_lossy())) } @@ -28,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(); @@ -42,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(); @@ -55,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); @@ -71,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", @@ -96,7 +104,7 @@ 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)?; From 4cea6d3cc8226780f540bc100013b604efd17d0f Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Mon, 28 Sep 2026 15:04:10 -0700 Subject: [PATCH 18/23] update to typespace v0.0.1-alpha.2 --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbb20fc0..c4ee20ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1305,8 +1305,9 @@ dependencies = [ [[package]] name = "typespace" -version = "0.0.1-alpha.1" -source = "git+https://github.com/oxidecomputer/typespace?branch=main#391d823b72351a8b8d97856e2e5d0d0f529f2a1b" +version = "0.0.1-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e351db219780cb453ee29422f86eaba6fa2538c45febb6043c2424595412fd02" dependencies = [ "codespace", "heck", diff --git a/Cargo.toml b/Cargo.toml index b391f65a..b7a97886 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ 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" } -typespace = { git = "https://github.com/oxidecomputer/typespace", branch = "main" } +typespace = "0.0.1-alpha.2" assert_cmd = "2.2.2" chrono = { version = "0.4.45", features = ["serde"] } From f0e7036c45062421f622e2e2cda7521d2e82e194 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Mon, 28 Sep 2026 15:22:04 -0700 Subject: [PATCH 19/23] prepare for 0.10.0-alpha.1 release --- CHANGELOG.adoc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 72e237ed..52b1b7d4 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -15,6 +15,15 @@ https://github.com/oxidecomputer/typify/compare/v0.8.0\...HEAD[Full list of commits] +**Big Release** + +Type generation moves to the `typespace` crate. This impacts a lot of API +surface area for both builder and macro consumers including how settings are +configured and how rendered code is produced, including item ordering in the +rendered code. + +Feedback is very welcome as we head to a proper release. + == 0.8.0 (released 2026-09-09) https://github.com/oxidecomputer/typify/compare/v0.7.0\...v0.8.0[Full list of commits] From 76c40c9048a22d046fcaf1405b6d78dd13f24789 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Mon, 28 Sep 2026 15:23:51 -0700 Subject: [PATCH 20/23] release typify 0.10.0-alpha.1 --- Cargo.lock | 8 ++++---- Cargo.toml | 6 +++--- cargo-typify/Cargo.toml | 2 +- typify-impl/Cargo.toml | 2 +- typify-macro/Cargo.toml | 2 +- typify/Cargo.toml | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4ee20ec..33d80421 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.1" dependencies = [ "assert_cmd", "clap", @@ -1324,7 +1324,7 @@ dependencies = [ [[package]] name = "typify" -version = "0.8.0" +version = "0.10.0-alpha.1" dependencies = [ "chrono", "env_logger", @@ -1344,7 +1344,7 @@ dependencies = [ [[package]] name = "typify-impl" -version = "0.8.0" +version = "0.10.0-alpha.1" dependencies = [ "env_logger", "expectorate", @@ -1369,7 +1369,7 @@ dependencies = [ [[package]] name = "typify-macro" -version = "0.8.0" +version = "0.10.0-alpha.1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b7a97886..498dfc95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,9 @@ members = [ resolver = "2" [workspace.dependencies] -typify = { version = "0.8.0", path = "typify" } -typify-impl = { version = "0.8.0", path = "typify-impl" } -typify-macro = { version = "0.8.0", path = "typify-macro" } +typify = { version = "0.10.0-alpha.1", path = "typify" } +typify-impl = { version = "0.10.0-alpha.1", path = "typify-impl" } +typify-macro = { version = "0.10.0-alpha.1", path = "typify-macro" } typespace = "0.0.1-alpha.2" diff --git a/cargo-typify/Cargo.toml b/cargo-typify/Cargo.toml index ae860a92..6846926c 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.1" edition = "2021" license = "Apache-2.0" description = "cargo command to generate Rust code from a JSON Schema" diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml index 8faf0ca2..12b9c2b7 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.1" edition = "2021" license = "Apache-2.0" description = "typify backend implementation" diff --git a/typify-macro/Cargo.toml b/typify-macro/Cargo.toml index 408a663e..728f9785 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.1" edition = "2021" license = "Apache-2.0" description = "typify macro implementation" diff --git a/typify/Cargo.toml b/typify/Cargo.toml index 7315bbe2..5debac1d 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.1" edition = "2021" license = "Apache-2.0" description = "JSON schema to rust type code generator" From 4401293cb811d1506b7f16252bb0170e01d5b195 Mon Sep 17 00:00:00 2001 From: Adam Leventhal Date: Sun, 4 Oct 2026 22:03:31 -0700 Subject: [PATCH 21/23] use codespace to track crate dependencies (#1089) --- .github/workflows/rust.yml | 4 +- Cargo.lock | 26 +++++++-- Cargo.toml | 2 +- typify-impl/src/convert.rs | 10 ++-- typify-impl/src/lib.rs | 68 +++++++++++------------ typify-impl/src/rust_extension.rs | 33 +++++++++--- typify-impl/tests/test_dependencies.rs | 75 ++++++++++++++++++++++++++ 7 files changed, 160 insertions(+), 58 deletions(-) create mode 100644 typify-impl/tests/test_dependencies.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index da63d037..4a1a9a37 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -5,9 +5,9 @@ name: Build on: push: - branches: [ main ] + branches: [ main, v0_10 ] pull_request: - branches: [ main ] + branches: [ main, v0_10 ] jobs: check-style: diff --git a/Cargo.lock b/Cargo.lock index 33d80421..4b0cc482 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,13 +251,15 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "codespace" -version = "0.0.1-alpha.2" +version = "0.0.1-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61018db61b1cdf32bed05d3addbd3a49d03be38e1a7674e067e6ded193d0cf49" +checksum = "a9b10f68d82f8de01589cd0ab1a21cddaf271b2a936d0bb2518c56a3498efe2f" dependencies = [ "proc-macro2", "quote", + "semver", "syn 3.0.6", + "version-ranges", ] [[package]] @@ -1028,6 +1030,12 @@ dependencies = [ "bstr", ] +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + [[package]] name = "strsim" version = "0.11.1" @@ -1305,9 +1313,9 @@ dependencies = [ [[package]] name = "typespace" -version = "0.0.1-alpha.2" +version = "0.0.1-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e351db219780cb453ee29422f86eaba6fa2538c45febb6043c2424595412fd02" +checksum = "5ac9bd6fcb72ab3ddfb1b9b20e28520f697df23a4b4340562685e7b6a8e8dc44" dependencies = [ "codespace", "heck", @@ -1441,6 +1449,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e9bd4e9c9ff6a2a9b5969462ba26216af3e010df0377dad8320ab515262ef8" +dependencies = [ + "semver", + "smallvec", +] + [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 498dfc95..7e6eace6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ typify = { version = "0.10.0-alpha.1", path = "typify" } typify-impl = { version = "0.10.0-alpha.1", path = "typify-impl" } typify-macro = { version = "0.10.0-alpha.1", path = "typify-macro" } -typespace = "0.0.1-alpha.2" +typespace = "0.0.1-alpha.3" assert_cmd = "2.2.2" chrono = { version = "0.4.45", features = ["serde"] } diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index 03251aa6..f1ee7c8b 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -799,7 +799,7 @@ impl TypeSpace { ) -> Result<(TypeEntry, &'a Option>)> { match format.as_ref().map(String::as_str) { Some("uuid") => { - self.uses_uuid = true; + self.add_dependency(typespace::codespace::Dependency::new("uuid")); // ::uuid::Uuid Default, but we ignore it because there's no // useful value. We assume the uuid feature in schemars. Ok(( @@ -827,7 +827,7 @@ impl TypeSpace { } Some("date") => { - self.uses_chrono = true; + self.add_dependency(typespace::codespace::Dependency::new("chrono")); // ::chrono::naive::NaiveDate implements Default, but there's // no useful value. We assume the chrono feaeture in schemars. Ok(( @@ -854,7 +854,7 @@ impl TypeSpace { )) } Some("date-time") => { - self.uses_chrono = true; + self.add_dependency(typespace::codespace::Dependency::new("chrono")); // ::chrono::DateTime implements Default, but not usefully. // We assume the chrono feaeture in schemars. Ok(( @@ -976,7 +976,6 @@ impl TypeSpace { type_name: type_name.clone().into_option(), reason: format!("invalid pattern '{}' {}", pattern, e), })?; - self.uses_regress = true; } let string = TypeEntry::from(Type::String); @@ -1948,7 +1947,6 @@ impl TypeSpace { unique_items, contains: None, } => { - self.uses_serde_json = true; let type_id = self.assign_type(TypeEntry::from(Type::JsonValue)); // If items are unique, this is a Set; otherwise it's an Array. @@ -1969,7 +1967,6 @@ impl TypeSpace { &mut self, metadata: &'a Option>, ) -> Result<(TypeEntry, &'a Option>)> { - self.uses_serde_json = true; let type_id = self.assign_type(TypeEntry::from(Type::JsonValue)); Ok((Type::Vec(type_id).into(), metadata)) } @@ -1986,7 +1983,6 @@ impl TypeSpace { &mut self, metadata: &'a Option>, ) -> Result<(TypeEntry, &'a Option>)> { - self.uses_serde_json = true; Ok((Type::JsonValue.into(), metadata)) } diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 5e9b0870..12a4d5b0 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -126,10 +126,12 @@ pub struct TypeSpace { name_to_id: BTreeMap, ref_to_id: BTreeMap, - uses_chrono: bool, - uses_uuid: bool, - uses_serde_json: bool, - uses_regress: bool, + /// The crates needed for native types: `chrono` and `uuid` for the string + /// formats it maps to them, and each crate an `x-rust-type` extension + /// names, at the version [`TypeSpaceSettings::with_crate`] declared for + /// it. Recorded on the `TypespaceBuilder` so the finalized typespace + /// reports them with the crates of what typespace renders itself. + dependencies: Vec, settings: TypeSpaceSettings, @@ -144,10 +146,7 @@ impl Default for TypeSpace { id_to_entry: Default::default(), name_to_id: Default::default(), ref_to_id: Default::default(), - uses_chrono: Default::default(), - uses_uuid: Default::default(), - uses_serde_json: Default::default(), - uses_regress: Default::default(), + dependencies: Default::default(), settings: Default::default(), cache: Default::default(), } @@ -745,26 +744,6 @@ impl TypeSpace { } } - /// Whether the generated code needs `chrono` crate. - pub fn uses_chrono(&self) -> bool { - self.uses_chrono - } - - /// Whether the generated code needs [regress] crate. - pub fn uses_regress(&self) -> bool { - self.uses_regress - } - - /// Whether the generated code needs [serde_json] crate. - pub fn uses_serde_json(&self) -> bool { - self.uses_serde_json - } - - /// Whether the generated code needs `uuid` crate. - pub fn uses_uuid(&self) -> bool { - self.uses_uuid - } - /// The type inserted under `type_id`, as it was inserted. /// /// This is the declaration typify handed typespace, available before @@ -791,6 +770,10 @@ impl TypeSpace { pub fn to_typespace(&self) -> Result> { let mut builder = typespace::TypespaceBuilder::new(self.settings.typespace.clone()); + for dependency in &self.dependencies { + builder.add_dependency(dependency.clone()); + } + for (type_id, type_entry) in &self.id_to_entry { match type_entry { TypeEntry::Type(typ) => { @@ -809,17 +792,30 @@ impl TypeSpace { /// All code for processed types. /// - /// Rendering is delegated to typespace: the stored types are - /// inserted into a `TypespaceBuilder`, finalized, and rendered - /// through codespace. Finalization errors (dangling references, - /// name collisions, unsatisfiable trait requirements) surface as + /// Rendering is delegated to typespace: the stored types are inserted into + /// a `TypespaceBuilder`, finalized, and rendered through codespace. The + /// codespace tracks depenencies. Finalization errors result in /// [`Error::Typespace`]. - pub fn to_stream(&self) -> Result { - let typespace = self.to_typespace()?; + pub fn to_codespace(&self) -> Result { + Ok(self.to_typespace()?.to_codespace()) + } - let codespace = typespace.to_codespace(); + /// All code for processed types. + /// + /// [`TypeSpace::to_codespace`] rendered to a `TokenStream`. + pub fn to_stream(&self) -> Result { + Ok(self.to_codespace()?.into_stream()) + } - Ok(codespace.into_stream()) + /// Record a crate behind a native type, once per crate. + fn add_dependency(&mut self, dependency: typespace::codespace::Dependency) { + if !self + .dependencies + .iter() + .any(|known| known.name == dependency.name) + { + self.dependencies.push(dependency); + } } /// Allocated the next TypeId. diff --git a/typify-impl/src/rust_extension.rs b/typify-impl/src/rust_extension.rs index e7a6cf5f..c2ac5708 100644 --- a/typify-impl/src/rust_extension.rs +++ b/typify-impl/src/rust_extension.rs @@ -57,26 +57,42 @@ impl TypeSpace { return None; } - let path = { + let (path, dependency) = { if let Some(crate_spec) = self.settings.crates.get(crate_name.as_str()) { // The version must be non-Never and match the requirements // from the extension. - match &crate_spec.version { - CrateVers::Any => (), - CrateVers::Version(version) if req.matches(version) => (), + let version = match &crate_spec.version { + CrateVers::Any => semver::VersionReq::STAR, + CrateVers::Version(version) if req.matches(version) => { + semver::VersionReq::parse(&version.to_string()) + .expect("a version is a valid requirement") + } _ => return None, - } + }; + let dependency = typespace::codespace::Dependency { + version, + ..typespace::codespace::Dependency::new(&crate_name) + }; // Replace the initial path component with the new crate name. if let Some(new_crate) = &crate_spec.rename { - format!("{}{}", new_crate.replace('-', "_"), &path[path_sep..]) + let new_crate = new_crate.replace('-', "_"); + ( + format!("{}{}", new_crate, &path[path_sep..]), + typespace::codespace::Dependency { + rename: Some(new_crate), + ..dependency + }, + ) } else { - path + (path, dependency) } } else { match self.settings.unknown_crates { crate::UnknownPolicy::Generate => return None, - crate::UnknownPolicy::Allow => path, + crate::UnknownPolicy::Allow => { + (path, typespace::codespace::Dependency::new(&crate_name)) + } // TODO need to bubble up a coherent compiler error via the // generated code. @@ -84,6 +100,7 @@ impl TypeSpace { } } }; + self.add_dependency(dependency); // Convert and collect type parameters. let param_ids = parameters diff --git a/typify-impl/tests/test_dependencies.rs b/typify-impl/tests/test_dependencies.rs new file mode 100644 index 00000000..8b3d465b --- /dev/null +++ b/typify-impl/tests/test_dependencies.rs @@ -0,0 +1,75 @@ +// Copyright 2026 Oxide Computer Company + +//! The crates a generated type space reports. + +use schemars::schema::Schema; +use typify_impl::{CrateVers, TypeSpace, TypeSpaceSettings}; + +fn names(type_space: &TypeSpace) -> Vec { + type_space + .to_codespace() + .unwrap() + .dependencies() + .map(|dep| dep.name.clone()) + .collect() +} + +/// A string format mapped to a native type reports that type's crate, +/// alongside what typespace reports for the rendered code. +#[test] +fn formats_report_their_crates() { + let schema: Schema = serde_json::from_value(serde_json::json!({ + "type": "object", + "properties": { + "when": { "type": "string", "format": "date-time" }, + "id": { "type": "string", "format": "uuid" } + }, + "required": ["when", "id"] + })) + .unwrap(); + let mut type_space = TypeSpace::new(&TypeSpaceSettings::default()); + type_space + .add_type_with_name(&schema, Some("Thing".to_string())) + .unwrap(); + assert_eq!(names(&type_space), ["chrono", "serde", "uuid"]); +} + +/// A type from an `x-rust-type` extension reports its crate at the +/// version the settings declared for it, renamed if the settings +/// renamed it. +#[test] +fn rust_extension_crates_carry_their_version() { + let schema: Schema = serde_json::from_value(serde_json::json!({ + "type": "object", + "properties": { + "value": { + "type": "string", + "x-rust-type": { + "crate": "my-crate", + "version": "1.2.3", + "path": "my_crate::Value" + } + } + }, + "required": ["value"] + })) + .unwrap(); + let mut settings = TypeSpaceSettings::default(); + settings.with_crate( + "my-crate", + CrateVers::Version("1.2.3".parse().unwrap()), + Some(&"their-crate".to_string()), + ); + let mut type_space = TypeSpace::new(&settings); + type_space + .add_type_with_name(&schema, Some("Thing".to_string())) + .unwrap(); + + let codespace = type_space.to_codespace().unwrap(); + let my_crate = codespace + .dependencies() + .find(|dep| dep.name == "my-crate") + .unwrap(); + assert_eq!(my_crate.version.to_string(), "^1.2.3"); + assert_eq!(my_crate.rename.as_deref(), Some("their_crate")); +} From ba5ba82e638a193ff26fe7980874c5ea9c4e2cf1 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 4 Oct 2026 22:15:42 -0700 Subject: [PATCH 22/23] prepare for 0.10.0-alpha.2 release --- release.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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}}" From bceff9aef7e59eb6df845e469dc4741e030ef84f Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Sun, 4 Oct 2026 22:16:29 -0700 Subject: [PATCH 23/23] release typify 0.10.0-alpha.2 --- CHANGELOG.adoc | 6 +++++- Cargo.lock | 8 ++++---- Cargo.toml | 6 +++--- cargo-typify/Cargo.toml | 2 +- typify-impl/Cargo.toml | 2 +- typify-macro/Cargo.toml | 2 +- typify/Cargo.toml | 2 +- 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 52b1b7d4..ec5049e6 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -13,7 +13,11 @@ == 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** diff --git a/Cargo.lock b/Cargo.lock index 4b0cc482..b0614a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,7 +163,7 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "cargo-typify" -version = "0.10.0-alpha.1" +version = "0.10.0-alpha.2" dependencies = [ "assert_cmd", "clap", @@ -1332,7 +1332,7 @@ dependencies = [ [[package]] name = "typify" -version = "0.10.0-alpha.1" +version = "0.10.0-alpha.2" dependencies = [ "chrono", "env_logger", @@ -1352,7 +1352,7 @@ dependencies = [ [[package]] name = "typify-impl" -version = "0.10.0-alpha.1" +version = "0.10.0-alpha.2" dependencies = [ "env_logger", "expectorate", @@ -1377,7 +1377,7 @@ dependencies = [ [[package]] name = "typify-macro" -version = "0.10.0-alpha.1" +version = "0.10.0-alpha.2" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7e6eace6..87de2760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,9 @@ members = [ resolver = "2" [workspace.dependencies] -typify = { version = "0.10.0-alpha.1", path = "typify" } -typify-impl = { version = "0.10.0-alpha.1", path = "typify-impl" } -typify-macro = { version = "0.10.0-alpha.1", path = "typify-macro" } +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" diff --git a/cargo-typify/Cargo.toml b/cargo-typify/Cargo.toml index 6846926c..b33dee9c 100644 --- a/cargo-typify/Cargo.toml +++ b/cargo-typify/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-typify" -version = "0.10.0-alpha.1" +version = "0.10.0-alpha.2" edition = "2021" license = "Apache-2.0" description = "cargo command to generate Rust code from a JSON Schema" diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml index 12b9c2b7..edd86342 100644 --- a/typify-impl/Cargo.toml +++ b/typify-impl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typify-impl" -version = "0.10.0-alpha.1" +version = "0.10.0-alpha.2" edition = "2021" license = "Apache-2.0" description = "typify backend implementation" diff --git a/typify-macro/Cargo.toml b/typify-macro/Cargo.toml index 728f9785..4958ae95 100644 --- a/typify-macro/Cargo.toml +++ b/typify-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typify-macro" -version = "0.10.0-alpha.1" +version = "0.10.0-alpha.2" edition = "2021" license = "Apache-2.0" description = "typify macro implementation" diff --git a/typify/Cargo.toml b/typify/Cargo.toml index 5debac1d..3b2e6a13 100644 --- a/typify/Cargo.toml +++ b/typify/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typify" -version = "0.10.0-alpha.1" +version = "0.10.0-alpha.2" edition = "2021" license = "Apache-2.0" description = "JSON schema to rust type code generator"