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..1c8e577b 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,19 @@ 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 type that the consumer named, through a replacement, a + /// conversion, or an `x-rust-type` extension. `traits` is what the + /// consumer vouched for. Ultimately the compiler decides whether the + /// generated code is valid... + 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(); +}