From 960b4b3df214d2198e91978e387e22e2ce6cab5c Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Wed, 14 Oct 2026 11:32:09 -0700 Subject: [PATCH 1/3] leave a consumer-named type's other traits unknown --- typify-impl/src/conversions.rs | 3 +- typify-impl/src/lib.rs | 3 +- typify-impl/src/rust_extension.rs | 10 +++- typify-impl/src/type_entry.rs | 26 ++++++----- typify-impl/tests/test_consumer_natives.rs | 53 ++++++++++++++++++++++ 5 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 typify-impl/tests/test_consumer_natives.rs diff --git a/typify-impl/src/conversions.rs b/typify-impl/src/conversions.rs index dd37ace2..18decd0b 100644 --- a/typify-impl/src/conversions.rs +++ b/typify-impl/src/conversions.rs @@ -13,7 +13,8 @@ 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, TypeSpaceImpl::native_traits(impls)); + let type_entry = + TypeEntry::new_consumer_native(type_name, TypeSpaceImpl::native_traits(impls), &[]); self.schemas.push(( SchemaObject { metadata: None, diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 12a4d5b0..3b011ca2 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -599,9 +599,10 @@ impl TypeSpace { } Some(replace_type) => { - let type_entry = TypeEntry::new_native( + let type_entry = TypeEntry::new_consumer_native( replace_type.replace_type.clone(), TypeSpaceImpl::native_traits(&replace_type.impls), + &[], ); self.id_to_entry.insert(type_id, type_entry); } diff --git a/typify-impl/src/rust_extension.rs b/typify-impl/src/rust_extension.rs index c2ac5708..30e21083 100644 --- a/typify-impl/src/rust_extension.rs +++ b/typify-impl/src/rust_extension.rs @@ -113,8 +113,16 @@ impl TypeSpace { .collect::>>() .ok()?; - Some(TypeEntry::new_native_params( + Some(TypeEntry::new_consumer_native( format!("::{path}"), + [ + typespace::TypespaceTrait::Clone, + typespace::TypespaceTrait::Debug, + typespace::TypespaceTrait::Serialize, + typespace::TypespaceTrait::Deserialize, + ] + .into_iter() + .collect(), ¶m_ids, )) } diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index 598bda76..fc7ca0dd 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -343,6 +343,7 @@ impl From> for TypeEntry { } impl TypeEntry { + /// A native typify knows: `traits` is everything it implements. pub(crate) fn new_native( type_name: S, traits: typespace::TypespaceTraitSet, @@ -350,17 +351,20 @@ impl TypeEntry { Type::Native(Native::new(&type_name.to_string(), traits, Vec::new())).into() } - 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::(); - - Type::Native(Native::new(&type_name.to_string(), traits, params.to_vec())).into() + /// A native the consumer named, through a replacement, a conversion, + /// or an `x-rust-type` extension: `traits` is what the consumer + /// vouched for, and the declaration says nothing about the rest. + /// A requirement the declaration cannot answer for then passes to + /// the compiler, which checks the real type. + pub(crate) fn new_consumer_native( + type_name: S, + traits: typespace::TypespaceTraitSet, + params: &[TypeId], + ) -> Self { + Type::Native( + Native::new(&type_name.to_string(), traits, params.to_vec()).with_rest_unknown(), + ) + .into() } pub(crate) fn new_boolean() -> Self { diff --git a/typify-impl/tests/test_consumer_natives.rs b/typify-impl/tests/test_consumer_natives.rs new file mode 100644 index 00000000..9a9c3c53 --- /dev/null +++ b/typify-impl/tests/test_consumer_natives.rs @@ -0,0 +1,53 @@ +// Copyright 2026 Oxide Computer Company + +//! A type the consumer names stands in for whatever it implements. + +use schemars::schema::RootSchema; +use typespace::TypespaceTrait; +use typify_impl::{TypeSpace, TypeSpaceSettings}; + +fn schema() -> RootSchema { + serde_json::from_value(serde_json::json!({ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Holder", + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { "$ref": "#/definitions/Id" }, + "uniqueItems": true + } + }, + "required": ["ids"], + "definitions": { + "Id": { "type": "string" } + } + })) + .unwrap() +} + +/// A set of a replaced type generates. typify cannot see what the +/// replacement implements, so what a set asks of it is left to the +/// compiler, which checks the real type; typify's own refusal was a +/// claim it had no basis for. +#[test] +fn a_set_of_a_replaced_type_generates() { + let mut settings = TypeSpaceSettings::default(); + settings.with_replacement("Id", "my_crate::Id", [].into_iter()); + let mut type_space = TypeSpace::new(&settings); + type_space.add_root_schema(schema()).unwrap(); + let rendered = type_space.to_stream().unwrap().to_string(); + assert!(rendered.contains("my_crate :: Id"), "{rendered}"); +} + +/// The same holds under a crate-wide required trait. +#[test] +fn a_replaced_type_satisfies_a_required_trait() { + let mut settings = TypeSpaceSettings::default(); + settings + .with_replacement("Id", "my_crate::Id", [].into_iter()) + .map_typespace_settings(|s| s.with_required_trait(TypespaceTrait::PartialEq)); + let mut type_space = TypeSpace::new(&settings); + type_space.add_root_schema(schema()).unwrap(); + type_space.to_stream().unwrap(); +} From a33618ab9427f12c17e30a1296857bde8ff667df Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Wed, 14 Oct 2026 12:12:33 -0700 Subject: [PATCH 2/3] nit --- typify-impl/src/type_entry.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index fc7ca0dd..c129d86f 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -351,11 +351,10 @@ impl TypeEntry { Type::Native(Native::new(&type_name.to_string(), traits, Vec::new())).into() } - /// A native the consumer named, through a replacement, a conversion, - /// or an `x-rust-type` extension: `traits` is what the consumer - /// vouched for, and the declaration says nothing about the rest. - /// A requirement the declaration cannot answer for then passes to - /// the compiler, which checks the real type. + /// A native type that the consumer named, through a replacement, a + /// conversion, or an `x-rust-type` extension. `traits` is what the + /// consumer vouched for. Ultiamtely the compiler decides whether the + /// generated code is valid... pub(crate) fn new_consumer_native( type_name: S, traits: typespace::TypespaceTraitSet, From 121a1aab5ac5dfd041edce2772cee4b91b2ff205 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Wed, 14 Oct 2026 12:17:25 -0700 Subject: [PATCH 3/3] typo --- typify-impl/src/type_entry.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index c129d86f..1c8e577b 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -353,7 +353,7 @@ impl TypeEntry { /// A native type that the consumer named, through a replacement, a /// conversion, or an `x-rust-type` extension. `traits` is what the - /// consumer vouched for. Ultiamtely the compiler decides whether the + /// consumer vouched for. Ultimately the compiler decides whether the /// generated code is valid... pub(crate) fn new_consumer_native( type_name: S,