diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs index 5f3b4897..45aa6815 100644 --- a/typify-impl/src/type_entry.rs +++ b/typify-impl/src/type_entry.rs @@ -1133,6 +1133,7 @@ impl TypeEntry { let mut prop_error = Vec::new(); let mut prop_type = Vec::new(); let mut prop_type_scoped = Vec::new(); + let mut prop_is_unit = Vec::new(); properties.iter().for_each(|prop| { prop_doc.push(prop.description.as_ref().map(|d| quote! { #[doc = #d] })); @@ -1143,6 +1144,7 @@ impl TypeEntry { )); let prop_type_entry = type_space.id_to_entry.get(&prop.type_id).unwrap(); + prop_is_unit.push(matches!(prop_type_entry.details, TypeEntryDetails::Unit)); 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()))); @@ -1264,10 +1266,38 @@ impl TypeEntry { 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) }, + // The source value is not read when every property is unit-typed. + let from_value_ident = if prop_is_unit.iter().all(|is_unit| *is_unit) { + quote! { _value } + } else { + quote! { value } + }; + + // For unit-typed properties (produced by null schemas) the value is + // always `()`, so we emit `Ok(())` rather than passing the unit value + // to `Ok(..)`, which would trip clippy's `unit_arg` lint. + let prop_default = + prop_default + .iter() + .zip(&prop_is_unit) + .map(|(pd, is_unit)| match pd { + PropDefault::None(err_msg) => quote! { Err(#err_msg.to_string()) }, + PropDefault::Default(_) | PropDefault::Custom(_) if *is_unit => { + quote! { Ok(()) } + } + PropDefault::Default(default_fn) => quote! { Ok(#default_fn) }, + PropDefault::Custom(custom_fn) => quote! { Ok(super::#custom_fn) }, + }); + + // Per-property initializers for the `From` impl. Reading a + // unit field yields `()`, so we emit `Ok(())` directly to avoid + // passing a unit value to `Ok(..)` (clippy's `unit_arg` lint). + let prop_from = prop_name.iter().zip(&prop_is_unit).map(|(name, is_unit)| { + if *is_unit { + quote! { #name: Ok(()) } + } else { + quote! { #name: Ok(value.#name) } + } }); output.add_item( @@ -1324,10 +1354,10 @@ impl TypeEntry { // Construct a builder from the item. impl ::std::convert::From for #type_name { - fn from(#value_ident: super::#type_name) -> Self { + fn from(#from_value_ident: super::#type_name) -> Self { Self { #( - #prop_name: Ok(value.#prop_name), + #prop_from, )* } } diff --git a/typify/tests/schemas/builder-unit-property.json b/typify/tests/schemas/builder-unit-property.json new file mode 100644 index 00000000..7a329a62 --- /dev/null +++ b/typify/tests/schemas/builder-unit-property.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UnitProperty", + "type": "object", + "required": ["unit"], + "properties": { + "unit": { + "type": "null", + "default": null + } + }, + "additionalProperties": false +} diff --git a/typify/tests/schemas/builder-unit-property.rs b/typify/tests/schemas/builder-unit-property.rs new file mode 100644 index 00000000..98e873ce --- /dev/null +++ b/typify/tests/schemas/builder-unit-property.rs @@ -0,0 +1,98 @@ +#![deny(warnings)] +#[doc = r" Error types."] +pub mod error { + #[doc = r" 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()) + } + } +} +#[doc = "`UnitProperty`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"title\": \"UnitProperty\","] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"unit\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"unit\": {"] +#[doc = " \"default\": null,"] +#[doc = " \"type\": \"null\""] +#[doc = " }"] +#[doc = " },"] +#[doc = " \"additionalProperties\": false"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] +pub struct UnitProperty { + pub unit: (), +} +impl UnitProperty { + pub fn builder() -> builder::UnitProperty { + Default::default() + } +} +#[doc = r" Types for composing complex structures."] +pub mod builder { + #[derive(Clone, Debug)] + pub struct UnitProperty { + unit: ::std::result::Result<(), ::std::string::String>, + } + impl ::std::default::Default for UnitProperty { + fn default() -> Self { + Self { + unit: Err("no value supplied for unit".to_string()), + } + } + } + impl UnitProperty { + pub fn unit(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<()>, + T::Error: ::std::fmt::Display, + { + self.unit = value + .try_into() + .map_err(|e| format!("error converting supplied value for unit: {e}")); + self + } + } + impl ::std::convert::TryFrom for super::UnitProperty { + type Error = super::error::ConversionError; + fn try_from( + value: UnitProperty, + ) -> ::std::result::Result { + Ok(Self { unit: value.unit? }) + } + } + impl ::std::convert::From for UnitProperty { + fn from(_value: super::UnitProperty) -> Self { + Self { unit: Ok(()) } + } + } +} +fn main() {}