diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee6327..5974b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. ### Breaking changes +* Represent Serde field and variant identifier enums with dedicated `Tagging` variants instead of incorrectly describing their input as externally tagged enums. +* Gate atomic shape implementations behind the `std` feature, matching Serde's own atomic implementations instead of advertising them in `no_std` builds where Serde cannot use them. +* Remove `DeserializeShape` from the unsized `str`, `[u8]`, and `Path` types, which do not implement Serde `Deserialize`. Their supported borrowed and owned forms retain explicit shapes. +* Replace the identical `SerializeTypeName` and `DeserializeTypeName` structures with one direction-neutral `TypeName`. Manual named definitions can use `TypeName::of::(serde_name)` instead of repeating `core::any::type_name::()`. * Remove the redundant `transparent` field from container attributes. Transparent containers remain observable through their field's `FieldWireShape::Inline` position. * Remove the blanket `DeserializeShape` implementations for `&T` and `&mut T`, which claimed support that Serde does not provide. Borrowed `&str`, `&[u8]`, and `&Path` inputs retain explicit implementations; custom borrowed types can now provide their own local implementation. * Remove the redundant `tagging` and `has_flatten` fields from container attributes. Read enum tagging from `SerializeEnumShape::repr` or `DeserializeEnumShape::repr`, and identify flattened fields through `FieldWireShape::Flatten`. @@ -17,6 +21,9 @@ All notable changes to this project will be documented in this file. ### New features +* Reflect serialized `core::fmt::Arguments` as a string, matching Serde's formatting implementation. +* Add target-specific `OsStr` and `OsString` enum shapes on Unix and Windows, including owned `Box` input. +* Add `SerializeShapeGraph::from_fn` and `DeserializeShapeGraph::from_fn` so custom shape functions can describe foreign graph roots without a dummy wrapper type. * Add `#[serde_shape(serialize_with = "path", deserialize_with = "path")]` hooks for custom Serde functions and foreign representations. * Allow custom shape hooks on enum variants so known custom variant content does not have to remain opaque. * Reflect Serde's byte-buffer representation for `CStr`, `CString`, and owned `Box` input. @@ -32,7 +39,7 @@ All notable changes to this project will be documented in this file. ### Bug fixes * Match Serde's deserialization bounds for tree and hash collections so a shape implementation is exposed only when the corresponding collection can actually deserialize. -* Preserve the known string and byte shapes of `#[serde(borrow)]` fields using `Cow` or `Cow<[u8]>` instead of treating Serde's generated borrowing helpers as custom opaque deserializers. +* Preserve the known string and byte shapes of `#[serde(borrow)]` fields using `Cow` or `Cow<[u8]>` from their source-level metadata, while leaving explicit custom deserializers opaque. * Distinguish borrowed byte input from owned boxed slices: `&[u8]` reflects bytes while `Box<[u8]>` reflects a sequence, matching Serde. * Match Serde's serialization bounds for `BinaryHeap`, `RefCell`, `Mutex`, and `RwLock`, including unsized wrapper contents. * Reflect the proxy type used by Serde `from`, `try_from`, and `into` container attributes. @@ -42,6 +49,7 @@ All notable changes to this project will be documented in this file. ### Improvements +* Add `definition_for` to both graph types so walkers can resolve a `ShapeRef::Definition` without repeating a match and id lookup. * Verify the packaged main crate against the packaged derive implementation that will be released with it, rather than accidentally compiling the previously published same-version macro crate from crates.io. * Clarify that shape graphs are normalized semantic models rather than exact traces of Serde serializer or deserializer method dispatch. * Add `FieldWireShape::shape()` so graph walkers can follow any present field without repeating a match over every wire position. diff --git a/README.md b/README.md index 633a0c3..0da5ec5 100644 --- a/README.md +++ b/README.md @@ -158,21 +158,25 @@ The built-in implementations follow Serde's semantic representations in each dir | Group | Supported types | | --- | --- | -| Scalars | Rust primitives, `String`, `str`, non-zero integers, and atomics available on the target | +| Scalars | Rust primitives, `String`, serialized `str` and `fmt::Arguments`, and non-zero integers | | Containers | `Option`, `Result`, arrays, slices for serialization, tuples through arity 16, `Vec`, `VecDeque`, `LinkedList`, `BinaryHeap`, `BTreeSet`, and `BTreeMap` | | Wrappers | Serialized references, borrowed string/byte/path inputs, `Box`, `Rc`, `Arc`, their weak pointers, `Cow`, `Cell`, `RefCell`, `Wrapping`, `Saturating`, `Reverse`, and `PhantomData` | -| FFI | `CStr` and `CString` byte representations, including owned `Box` input | +| FFI | `CStr` and `CString` byte representations; on Unix and Windows, serialized `OsStr`, `OsString`, and owned `Box` input | | Ranges | `Range`, `RangeFrom`, `RangeInclusive`, `RangeTo`, and `Bound` | | Time | `core::time::Duration` and, with `std`, `SystemTime` | | Network | `core::net` IP and socket address types | -| `std` feature | `HashMap`, `HashSet`, `Path`, `PathBuf`, `Mutex`, and `RwLock` | +| `std` feature | Atomics available on the target, `HashMap`, `HashSet`, `Path`, `PathBuf`, `Mutex`, and `RwLock` | Network address shapes are unions of their human-readable string representation and their compact Serde representation. A serialized byte slice and an owned `Box<[u8]>` input are sequences, while borrowed byte deserialization uses `ShapeRef::Bytes`. +OS string shapes preserve Serde's target-specific externally tagged representation: `Unix` contains a byte sequence, while `Windows` contains a `u16` sequence. Deserialization advertises only the variant accepted on the current target. + Serde's `rc` feature is still required to serialize or deserialize `Rc`, `Arc`, and their weak pointers; the shape implementations do not enable Serde features. Serialization follows Serde's blanket support for `&T` and `&mut T`. Deserialization only provides reference shapes for Serde's borrowable `&str`, `&[u8]`, and `&Path` inputs; arbitrary shared and mutable references do not have a Serde deserializer. +The unsized `str`, `[u8]`, and `Path` types themselves do not implement `DeserializeShape`, matching Serde. Their borrowed and owned input forms have explicit shape implementations. + For an unsupported foreign type, use a local newtype and implement `SerializeShape` or `DeserializeShape` manually. Custom Serde functions remain opaque by default because their wire behavior cannot be inferred; use a `serde_shape` custom hook when the representation is known. ## `no_std` support diff --git a/serde-shape-derive/src/lib.rs b/serde-shape-derive/src/lib.rs index 728bf9a..cf01f77 100644 --- a/serde-shape-derive/src/lib.rs +++ b/serde-shape-derive/src/lib.rs @@ -29,6 +29,7 @@ use serde_derive_internals::Derive; use serde_derive_internals::ast; use serde_derive_internals::attr; use serde_derive_internals::name::Name; +use serde_derive_internals::ungroup; use syn::DeriveInput; use syn::GenericArgument; use syn::LitStr; @@ -144,6 +145,18 @@ fn parse_container<'a>(input: &'a DeriveInput, derive: Derive) -> syn::Result None, + attr::Identifier::Field => Some("field identifiers cannot be serialized"), + attr::Identifier::Variant => Some("variant identifiers cannot be serialized"), + }; + if let Some(message) = message { + return Err(syn::Error::new_spanned(input, message)); + } + } + Ok(container) } @@ -535,10 +548,7 @@ fn serialize_shape_body( Ok(quote! { context.define_named_type_with_description( - __serde_shape::SerializeTypeName { - rust_name: ::core::any::type_name::(), - name: #name, - }, + __serde_shape::TypeName::of::(#name), #description, |context| { #kind @@ -569,10 +579,7 @@ fn deserialize_shape_body( Ok(quote! { context.define_named_type_with_description( - __serde_shape::DeserializeTypeName { - rust_name: ::core::any::type_name::(), - name: #name, - }, + __serde_shape::TypeName::of::(#name), #description, |context| { #kind @@ -643,7 +650,7 @@ fn deserialize_definition_kind(container: &ast::Container<'_>) -> syn::Result { - let repr = tagging(container.attrs.tag()); + let repr = deserialize_tagging(&container.attrs); let variants = variants .iter() .map(deserialize_variant_shape) @@ -849,6 +856,7 @@ fn serialize_field_shape(field: &ast::Field<'_>) -> syn::Result { fn deserialize_field_shape(field: &ast::Field<'_>) -> syn::Result { let shape_attrs = ShapeAttrs::parse(&field.original.attrs)?; + let borrowed_cow_shape = borrowed_cow_shape(field)?; let member = field_member(&field.member); let name = lit_name(field.attrs.name().deserialize_name()); let aliases = aliases(field.attrs.aliases()); @@ -864,18 +872,16 @@ fn deserialize_field_shape(field: &ast::Field<'_>) -> syn::Result } else { let value_shape = if let Some(function) = shape_attrs.deserialize_with() { quote!(#function(context)) + } else if let Some(shape) = borrowed_cow_shape { + shape } else if let Some(custom_deserializer) = field.attrs.deserialize_with() { - if let Some(shape) = serde_borrowed_cow_shape(custom_deserializer) { - shape - } else { - let detail = option_path(Some(custom_deserializer)); - quote! { - __serde_shape::ShapeRef::Opaque(__serde_shape::OpaqueShape { - type_name: ::core::any::type_name::<#ty>(), - reason: __serde_shape::OpaqueReason::CustomDeserializer, - detail: #detail, - }) - } + let detail = option_path(Some(custom_deserializer)); + quote! { + __serde_shape::ShapeRef::Opaque(__serde_shape::OpaqueShape { + type_name: ::core::any::type_name::<#ty>(), + reason: __serde_shape::OpaqueReason::CustomDeserializer, + detail: #detail, + }) } } else { quote!(<#ty as __serde_shape::DeserializeShape>::deserialize_shape_in(context)) @@ -943,6 +949,14 @@ fn tagging(tag: &attr::TagType) -> TokenStream2 { } } +fn deserialize_tagging(attrs: &attr::Container) -> TokenStream2 { + match attrs.identifier() { + attr::Identifier::No => tagging(attrs.tag()), + attr::Identifier::Field => quote!(__serde_shape::Tagging::FieldIdentifier), + attr::Identifier::Variant => quote!(__serde_shape::Tagging::VariantIdentifier), + } +} + fn default_shape(default: &attr::Default) -> TokenStream2 { match default { attr::Default::None => quote!(__serde_shape::DefaultShape::None), @@ -959,27 +973,80 @@ fn aliases(aliases: &BTreeSet) -> TokenStream2 { quote!(__serde_shape::__private::vec![#(#aliases),*]) } -fn serde_borrowed_cow_shape(path: &syn::ExprPath) -> Option { - if path.qself.is_some() || path.path.leading_colon.is_some() { - return None; +fn borrowed_cow_shape(field: &ast::Field<'_>) -> syn::Result> { + // serde_derive_internals models borrowed Cow fields as custom deserializers internally. Read + // the source-level contract instead, so this derive does not depend on Serde's private helper + // path. An explicit user deserializer still takes precedence over the built-in Cow behavior. + if field.attrs.borrowed_lifetimes().is_empty() + || has_explicit_serde_deserializer(&field.original.attrs)? + { + return Ok(None); } - let mut segments = path.path.segments.iter(); - let serde = segments.next()?; - let _private = segments.next()?; - let de = segments.next()?; - let helper = segments.next()?; - if segments.next().is_some() || serde.ident != "_serde" || de.ident != "de" { - return None; + let Some(element) = cow_element_type(field.ty) else { + return Ok(None); + }; + if is_primitive_type(element, "str") { + Ok(Some(quote!(__serde_shape::ShapeRef::String))) + } else if is_byte_slice(element) { + Ok(Some(quote!(__serde_shape::ShapeRef::Bytes))) + } else { + Ok(None) + } +} + +fn has_explicit_serde_deserializer(attrs: &[syn::Attribute]) -> syn::Result { + for attr in attrs.iter().filter(|attr| attr.path().is_ident("serde")) { + let metas = attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + )?; + if metas + .iter() + .any(|meta| meta.path().is_ident("deserialize_with") || meta.path().is_ident("with")) + { + return Ok(true); + } } + Ok(false) +} - match helper.ident.to_string().as_str() { - "borrow_cow_str" => Some(quote!(__serde_shape::ShapeRef::String)), - "borrow_cow_bytes" => Some(quote!(__serde_shape::ShapeRef::Bytes)), +fn cow_element_type(ty: &Type) -> Option<&Type> { + let Type::Path(ty) = ungroup(ty) else { + return None; + }; + let segment = ty.path.segments.last()?; + let PathArguments::AngleBracketed(arguments) = &segment.arguments else { + return None; + }; + let mut arguments = arguments.args.iter(); + match (arguments.next(), arguments.next(), arguments.next()) { + (Some(GenericArgument::Lifetime(_)), Some(GenericArgument::Type(element)), None) + if segment.ident == "Cow" => + { + Some(element) + } _ => None, } } +fn is_byte_slice(ty: &Type) -> bool { + match ungroup(ty) { + Type::Slice(slice) => is_primitive_type(&slice.elem, "u8"), + _ => false, + } +} + +fn is_primitive_type(ty: &Type, name: &str) -> bool { + let Type::Path(ty) = ungroup(ty) else { + return false; + }; + ty.qself.is_none() + && ty.path.leading_colon.is_none() + && ty.path.segments.len() == 1 + && ty.path.segments[0].ident == name + && ty.path.segments[0].arguments.is_empty() +} + fn lit_name(value: &Name) -> LitStr { LitStr::new(&value.value, value.span) } diff --git a/serde-shape/src/impls/bound.rs b/serde-shape/src/impls/bound.rs index af56391..4f0110a 100644 --- a/serde-shape/src/impls/bound.rs +++ b/serde-shape/src/impls/bound.rs @@ -13,7 +13,6 @@ // limitations under the License. use alloc::vec; -use core::any::type_name; use core::ops::Bound; use crate::DefaultShape; @@ -23,7 +22,6 @@ use crate::DeserializeEnumShape; use crate::DeserializeFieldShape; use crate::DeserializeShape; use crate::DeserializeShapeContext; -use crate::DeserializeTypeName; use crate::DeserializeVariantContent; use crate::DeserializeVariantShape; use crate::FieldMember; @@ -35,34 +33,28 @@ use crate::SerializeEnumShape; use crate::SerializeFieldShape; use crate::SerializeShape; use crate::SerializeShapeContext; -use crate::SerializeTypeName; use crate::SerializeVariantContent; use crate::SerializeVariantShape; use crate::ShapeRef; use crate::Tagging; +use crate::TypeName; impl SerializeShape for Bound where T: SerializeShape, { fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { - context.define_named_type( - SerializeTypeName { - rust_name: type_name::(), - name: "Bound", - }, - |context| { - SerializeDefinitionKind::Enum(SerializeEnumShape { - repr: Tagging::External, - variants: vec![ - serialize_bound_variant("Unbounded", None), - serialize_bound_variant("Included", Some(T::serialize_shape_in(context))), - serialize_bound_variant("Excluded", Some(T::serialize_shape_in(context))), - ], - attributes: SerializeContainerAttributes::default(), - }) - }, - ) + context.define_named_type(TypeName::of::("Bound"), |context| { + SerializeDefinitionKind::Enum(SerializeEnumShape { + repr: Tagging::External, + variants: vec![ + serialize_bound_variant("Unbounded", None), + serialize_bound_variant("Included", Some(T::serialize_shape_in(context))), + serialize_bound_variant("Excluded", Some(T::serialize_shape_in(context))), + ], + attributes: SerializeContainerAttributes::default(), + }) + }) } } @@ -71,29 +63,17 @@ where T: DeserializeShape, { fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { - context.define_named_type( - DeserializeTypeName { - rust_name: type_name::(), - name: "Bound", - }, - |context| { - DeserializeDefinitionKind::Enum(DeserializeEnumShape { - repr: Tagging::External, - variants: vec![ - deserialize_bound_variant("Unbounded", None), - deserialize_bound_variant( - "Included", - Some(T::deserialize_shape_in(context)), - ), - deserialize_bound_variant( - "Excluded", - Some(T::deserialize_shape_in(context)), - ), - ], - attributes: DeserializeContainerAttributes::default(), - }) - }, - ) + context.define_named_type(TypeName::of::("Bound"), |context| { + DeserializeDefinitionKind::Enum(DeserializeEnumShape { + repr: Tagging::External, + variants: vec![ + deserialize_bound_variant("Unbounded", None), + deserialize_bound_variant("Included", Some(T::deserialize_shape_in(context))), + deserialize_bound_variant("Excluded", Some(T::deserialize_shape_in(context))), + ], + attributes: DeserializeContainerAttributes::default(), + }) + }) } } diff --git a/serde-shape/src/impls/mod.rs b/serde-shape/src/impls/mod.rs index 45ac420..04b149a 100644 --- a/serde-shape/src/impls/mod.rs +++ b/serde-shape/src/impls/mod.rs @@ -16,6 +16,8 @@ mod bound; mod container; mod ffi; mod net; +#[cfg(all(feature = "std", any(unix, windows)))] +mod os_string; mod primitive; mod range; mod result; diff --git a/serde-shape/src/impls/net.rs b/serde-shape/src/impls/net.rs index 4c4415e..5fe64e8 100644 --- a/serde-shape/src/impls/net.rs +++ b/serde-shape/src/impls/net.rs @@ -14,7 +14,6 @@ use alloc::boxed::Box; use alloc::vec; -use core::any::type_name; use core::net::IpAddr; use core::net::Ipv4Addr; use core::net::Ipv6Addr; @@ -29,7 +28,6 @@ use crate::DeserializeEnumShape; use crate::DeserializeFieldShape; use crate::DeserializeShape; use crate::DeserializeShapeContext; -use crate::DeserializeTypeName; use crate::DeserializeVariantContent; use crate::DeserializeVariantShape; use crate::FieldMember; @@ -41,11 +39,11 @@ use crate::SerializeEnumShape; use crate::SerializeFieldShape; use crate::SerializeShape; use crate::SerializeShapeContext; -use crate::SerializeTypeName; use crate::SerializeVariantContent; use crate::SerializeVariantShape; use crate::ShapeRef; use crate::Tagging; +use crate::TypeName; macro_rules! union_shape { ($ty:ty => $binary:expr) => { @@ -70,88 +68,64 @@ union_shape!(SocketAddrV6 => socket_v6_binary_shape()); impl SerializeShape for IpAddr { fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { - let binary = context.define_named_type( - SerializeTypeName { - rust_name: type_name::(), - name: "IpAddr", - }, - |_| { - SerializeDefinitionKind::Enum(SerializeEnumShape { - repr: Tagging::External, - variants: vec![ - serialize_newtype_variant("V4", ipv4_binary_shape()), - serialize_newtype_variant("V6", ipv6_binary_shape()), - ], - attributes: serialize_enum_attributes(), - }) - }, - ); + let binary = context.define_named_type(TypeName::of::("IpAddr"), |_| { + SerializeDefinitionKind::Enum(SerializeEnumShape { + repr: Tagging::External, + variants: vec![ + serialize_newtype_variant("V4", ipv4_binary_shape()), + serialize_newtype_variant("V6", ipv6_binary_shape()), + ], + attributes: serialize_enum_attributes(), + }) + }); ShapeRef::union([ShapeRef::String, binary]) } } impl DeserializeShape for IpAddr { fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { - let binary = context.define_named_type( - DeserializeTypeName { - rust_name: type_name::(), - name: "IpAddr", - }, - |_| { - DeserializeDefinitionKind::Enum(DeserializeEnumShape { - repr: Tagging::External, - variants: vec![ - deserialize_newtype_variant("V4", ipv4_binary_shape()), - deserialize_newtype_variant("V6", ipv6_binary_shape()), - ], - attributes: deserialize_enum_attributes(), - }) - }, - ); + let binary = context.define_named_type(TypeName::of::("IpAddr"), |_| { + DeserializeDefinitionKind::Enum(DeserializeEnumShape { + repr: Tagging::External, + variants: vec![ + deserialize_newtype_variant("V4", ipv4_binary_shape()), + deserialize_newtype_variant("V6", ipv6_binary_shape()), + ], + attributes: deserialize_enum_attributes(), + }) + }); ShapeRef::union([ShapeRef::String, binary]) } } impl SerializeShape for SocketAddr { fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { - let binary = context.define_named_type( - SerializeTypeName { - rust_name: type_name::(), - name: "SocketAddr", - }, - |_| { - SerializeDefinitionKind::Enum(SerializeEnumShape { - repr: Tagging::External, - variants: vec![ - serialize_newtype_variant("V4", socket_v4_binary_shape()), - serialize_newtype_variant("V6", socket_v6_binary_shape()), - ], - attributes: serialize_enum_attributes(), - }) - }, - ); + let binary = context.define_named_type(TypeName::of::("SocketAddr"), |_| { + SerializeDefinitionKind::Enum(SerializeEnumShape { + repr: Tagging::External, + variants: vec![ + serialize_newtype_variant("V4", socket_v4_binary_shape()), + serialize_newtype_variant("V6", socket_v6_binary_shape()), + ], + attributes: serialize_enum_attributes(), + }) + }); ShapeRef::union([ShapeRef::String, binary]) } } impl DeserializeShape for SocketAddr { fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { - let binary = context.define_named_type( - DeserializeTypeName { - rust_name: type_name::(), - name: "SocketAddr", - }, - |_| { - DeserializeDefinitionKind::Enum(DeserializeEnumShape { - repr: Tagging::External, - variants: vec![ - deserialize_newtype_variant("V4", socket_v4_binary_shape()), - deserialize_newtype_variant("V6", socket_v6_binary_shape()), - ], - attributes: deserialize_enum_attributes(), - }) - }, - ); + let binary = context.define_named_type(TypeName::of::("SocketAddr"), |_| { + DeserializeDefinitionKind::Enum(DeserializeEnumShape { + repr: Tagging::External, + variants: vec![ + deserialize_newtype_variant("V4", socket_v4_binary_shape()), + deserialize_newtype_variant("V6", socket_v6_binary_shape()), + ], + attributes: deserialize_enum_attributes(), + }) + }); ShapeRef::union([ShapeRef::String, binary]) } } diff --git a/serde-shape/src/impls/os_string.rs b/serde-shape/src/impls/os_string.rs new file mode 100644 index 0000000..e687d0c --- /dev/null +++ b/serde-shape/src/impls/os_string.rs @@ -0,0 +1,135 @@ +// Copyright 2026 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use alloc::boxed::Box; +use alloc::vec; +use std::ffi::OsStr; +use std::ffi::OsString; + +use crate::DefaultShape; +use crate::DeserializeContainerAttributes; +use crate::DeserializeDefinitionKind; +use crate::DeserializeEnumShape; +use crate::DeserializeFieldShape; +use crate::DeserializeShape; +use crate::DeserializeShapeContext; +use crate::DeserializeVariantContent; +use crate::DeserializeVariantShape; +use crate::FieldMember; +use crate::FieldWireShape; +use crate::FieldsStyle; +use crate::SerializeContainerAttributes; +use crate::SerializeDefinitionKind; +use crate::SerializeEnumShape; +use crate::SerializeFieldShape; +use crate::SerializeShape; +use crate::SerializeShapeContext; +use crate::SerializeVariantContent; +use crate::SerializeVariantShape; +use crate::ShapeRef; +use crate::Tagging; +use crate::TypeName; + +impl SerializeShape for OsStr { + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + serialize_os_string(context, TypeName::of::("OsString")) + } +} + +impl SerializeShape for OsString { + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + serialize_os_string(context, TypeName::of::("OsString")) + } +} + +impl DeserializeShape for OsString { + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + deserialize_os_string(context, TypeName::of::("OsString")) + } +} + +impl DeserializeShape for Box { + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + deserialize_os_string(context, TypeName::of::("OsString")) + } +} + +fn serialize_os_string(context: &mut SerializeShapeContext, type_name: TypeName) -> ShapeRef { + context.define_named_type(type_name, |_| { + SerializeDefinitionKind::Enum(SerializeEnumShape { + repr: Tagging::External, + variants: vec![SerializeVariantShape { + rust_name: platform_variant_name(), + name: platform_variant_name(), + description: None, + style: FieldsStyle::Newtype, + content: SerializeVariantContent::Fields(vec![SerializeFieldShape { + member: FieldMember::Unnamed(0), + name: "0", + description: None, + wire_shape: FieldWireShape::Value(platform_value_shape()), + skip_if: None, + }]), + untagged: false, + }], + attributes: SerializeContainerAttributes::default(), + }) + }) +} + +fn deserialize_os_string(context: &mut DeserializeShapeContext, type_name: TypeName) -> ShapeRef { + context.define_named_type(type_name, |_| { + DeserializeDefinitionKind::Enum(DeserializeEnumShape { + repr: Tagging::External, + variants: vec![DeserializeVariantShape { + rust_name: platform_variant_name(), + name: platform_variant_name(), + aliases: vec![platform_variant_name()], + description: None, + style: FieldsStyle::Newtype, + content: DeserializeVariantContent::Fields(vec![DeserializeFieldShape { + member: FieldMember::Unnamed(0), + name: "0", + aliases: vec!["0"], + description: None, + wire_shape: FieldWireShape::Value(platform_value_shape()), + default: DefaultShape::None, + }]), + other: false, + untagged: false, + }], + attributes: DeserializeContainerAttributes::default(), + }) + }) +} + +#[cfg(unix)] +fn platform_variant_name() -> &'static str { + "Unix" +} + +#[cfg(windows)] +fn platform_variant_name() -> &'static str { + "Windows" +} + +#[cfg(unix)] +fn platform_value_shape() -> ShapeRef { + ShapeRef::Seq(Box::new(ShapeRef::U8)) +} + +#[cfg(windows)] +fn platform_value_shape() -> ShapeRef { + ShapeRef::Seq(Box::new(ShapeRef::U16)) +} diff --git a/serde-shape/src/impls/primitive.rs b/serde-shape/src/impls/primitive.rs index 5c34c21..6b03f3c 100644 --- a/serde-shape/src/impls/primitive.rs +++ b/serde-shape/src/impls/primitive.rs @@ -56,7 +56,6 @@ primitive_shape! { usize => ShapeRef::Usize; f32 => ShapeRef::F32; f64 => ShapeRef::F64; - str => ShapeRef::String; String => ShapeRef::String; core::num::NonZeroI8 => ShapeRef::I8; core::num::NonZeroI16 => ShapeRef::I16; @@ -72,44 +71,65 @@ primitive_shape! { core::num::NonZeroUsize => ShapeRef::Usize; } -impl DeserializeShape for [u8] { - fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { - ShapeRef::Bytes +impl SerializeShape for str { + fn serialize_shape_in(_context: &mut SerializeShapeContext) -> ShapeRef { + ShapeRef::String + } +} + +impl SerializeShape for core::fmt::Arguments<'_> { + fn serialize_shape_in(_context: &mut SerializeShapeContext) -> ShapeRef { + ShapeRef::String } } #[cfg(feature = "std")] -primitive_shape! { - std::path::Path => ShapeRef::String; - std::path::PathBuf => ShapeRef::String; +impl SerializeShape for std::path::Path { + fn serialize_shape_in(_context: &mut SerializeShapeContext) -> ShapeRef { + ShapeRef::String + } +} + +#[cfg(feature = "std")] +impl SerializeShape for std::path::PathBuf { + fn serialize_shape_in(_context: &mut SerializeShapeContext) -> ShapeRef { + ShapeRef::String + } +} + +#[cfg(feature = "std")] +impl DeserializeShape for std::path::PathBuf { + fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::String + } } -#[cfg(target_has_atomic = "8")] +#[cfg(all(feature = "std", target_has_atomic = "8"))] primitive_shape! { core::sync::atomic::AtomicBool => ShapeRef::Bool; core::sync::atomic::AtomicI8 => ShapeRef::I8; core::sync::atomic::AtomicU8 => ShapeRef::U8; } -#[cfg(target_has_atomic = "16")] +#[cfg(all(feature = "std", target_has_atomic = "16"))] primitive_shape! { core::sync::atomic::AtomicI16 => ShapeRef::I16; core::sync::atomic::AtomicU16 => ShapeRef::U16; } -#[cfg(target_has_atomic = "32")] +#[cfg(all(feature = "std", target_has_atomic = "32"))] primitive_shape! { core::sync::atomic::AtomicI32 => ShapeRef::I32; core::sync::atomic::AtomicU32 => ShapeRef::U32; } -#[cfg(target_has_atomic = "64")] +#[cfg(all(feature = "std", target_has_atomic = "64"))] primitive_shape! { core::sync::atomic::AtomicI64 => ShapeRef::I64; core::sync::atomic::AtomicU64 => ShapeRef::U64; } -#[cfg(target_has_atomic = "ptr")] +#[cfg(all(feature = "std", target_has_atomic = "ptr"))] primitive_shape! { core::sync::atomic::AtomicIsize => ShapeRef::Isize; core::sync::atomic::AtomicUsize => ShapeRef::Usize; diff --git a/serde-shape/src/impls/range.rs b/serde-shape/src/impls/range.rs index 93ed9f2..cf5d897 100644 --- a/serde-shape/src/impls/range.rs +++ b/serde-shape/src/impls/range.rs @@ -13,7 +13,6 @@ // limitations under the License. use alloc::vec; -use core::any::type_name; use core::ops::Range; use core::ops::RangeFrom; use core::ops::RangeInclusive; @@ -26,7 +25,6 @@ use crate::DeserializeFieldShape; use crate::DeserializeShape; use crate::DeserializeShapeContext; use crate::DeserializeStructShape; -use crate::DeserializeTypeName; use crate::FieldMember; use crate::FieldWireShape; use crate::FieldsStyle; @@ -36,8 +34,8 @@ use crate::SerializeFieldShape; use crate::SerializeShape; use crate::SerializeShapeContext; use crate::SerializeStructShape; -use crate::SerializeTypeName; use crate::ShapeRef; +use crate::TypeName; macro_rules! range_shape { ($($range:ident { $($field:ident),+ $(,)? })+) => { @@ -48,10 +46,7 @@ macro_rules! range_shape { { fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { context.define_named_type( - SerializeTypeName { - rust_name: type_name::(), - name: stringify!($range), - }, + TypeName::of::(stringify!($range)), |context| { SerializeDefinitionKind::Struct(SerializeStructShape { style: FieldsStyle::Struct, @@ -79,10 +74,7 @@ macro_rules! range_shape { { fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { context.define_named_type( - DeserializeTypeName { - rust_name: type_name::(), - name: stringify!($range), - }, + TypeName::of::(stringify!($range)), |context| { DeserializeDefinitionKind::Struct(DeserializeStructShape { style: FieldsStyle::Struct, diff --git a/serde-shape/src/impls/result.rs b/serde-shape/src/impls/result.rs index b0b9768..7be9503 100644 --- a/serde-shape/src/impls/result.rs +++ b/serde-shape/src/impls/result.rs @@ -13,7 +13,6 @@ // limitations under the License. use alloc::vec; -use core::any::type_name; use crate::DefaultShape; use crate::DeserializeContainerAttributes; @@ -22,7 +21,6 @@ use crate::DeserializeEnumShape; use crate::DeserializeFieldShape; use crate::DeserializeShape; use crate::DeserializeShapeContext; -use crate::DeserializeTypeName; use crate::DeserializeVariantContent; use crate::DeserializeVariantShape; use crate::FieldMember; @@ -34,11 +32,11 @@ use crate::SerializeEnumShape; use crate::SerializeFieldShape; use crate::SerializeShape; use crate::SerializeShapeContext; -use crate::SerializeTypeName; use crate::SerializeVariantContent; use crate::SerializeVariantShape; use crate::ShapeRef; use crate::Tagging; +use crate::TypeName; impl SerializeShape for Result where @@ -46,22 +44,16 @@ where E: SerializeShape, { fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { - context.define_named_type( - SerializeTypeName { - rust_name: type_name::(), - name: "Result", - }, - |context| { - SerializeDefinitionKind::Enum(SerializeEnumShape { - repr: Tagging::External, - variants: vec![ - serialize_result_variant("Ok", T::serialize_shape_in(context)), - serialize_result_variant("Err", E::serialize_shape_in(context)), - ], - attributes: SerializeContainerAttributes::default(), - }) - }, - ) + context.define_named_type(TypeName::of::("Result"), |context| { + SerializeDefinitionKind::Enum(SerializeEnumShape { + repr: Tagging::External, + variants: vec![ + serialize_result_variant("Ok", T::serialize_shape_in(context)), + serialize_result_variant("Err", E::serialize_shape_in(context)), + ], + attributes: SerializeContainerAttributes::default(), + }) + }) } } @@ -71,22 +63,16 @@ where E: DeserializeShape, { fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { - context.define_named_type( - DeserializeTypeName { - rust_name: type_name::(), - name: "Result", - }, - |context| { - DeserializeDefinitionKind::Enum(DeserializeEnumShape { - repr: Tagging::External, - variants: vec![ - deserialize_result_variant("Ok", T::deserialize_shape_in(context)), - deserialize_result_variant("Err", E::deserialize_shape_in(context)), - ], - attributes: DeserializeContainerAttributes::default(), - }) - }, - ) + context.define_named_type(TypeName::of::("Result"), |context| { + DeserializeDefinitionKind::Enum(DeserializeEnumShape { + repr: Tagging::External, + variants: vec![ + deserialize_result_variant("Ok", T::deserialize_shape_in(context)), + deserialize_result_variant("Err", E::deserialize_shape_in(context)), + ], + attributes: DeserializeContainerAttributes::default(), + }) + }) } } diff --git a/serde-shape/src/impls/time.rs b/serde-shape/src/impls/time.rs index c05cf44..9c3e4f6 100644 --- a/serde-shape/src/impls/time.rs +++ b/serde-shape/src/impls/time.rs @@ -26,7 +26,6 @@ use crate::DeserializeFieldShape; use crate::DeserializeShape; use crate::DeserializeShapeContext; use crate::DeserializeStructShape; -use crate::DeserializeTypeName; use crate::FieldMember; use crate::FieldWireShape; use crate::FieldsStyle; @@ -36,8 +35,8 @@ use crate::SerializeFieldShape; use crate::SerializeShape; use crate::SerializeShapeContext; use crate::SerializeStructShape; -use crate::SerializeTypeName; use crate::ShapeRef; +use crate::TypeName; macro_rules! time_shape { ($ty:ty, $name:literal, $($field:literal => $shape:expr),+ $(,)?) => { @@ -91,7 +90,7 @@ fn serialize_time_shape( skip_if: None, }) .collect(); - context.define_named_type(SerializeTypeName { rust_name, name }, move |_| { + context.define_named_type(TypeName { rust_name, name }, move |_| { SerializeDefinitionKind::Struct(SerializeStructShape { style: FieldsStyle::Struct, fields, @@ -117,7 +116,7 @@ fn deserialize_time_shape( default: DefaultShape::None, }) .collect(); - context.define_named_type(DeserializeTypeName { rust_name, name }, move |_| { + context.define_named_type(TypeName { rust_name, name }, move |_| { DeserializeDefinitionKind::Struct(DeserializeStructShape { style: FieldsStyle::Struct, fields, diff --git a/serde-shape/src/impls/wrapper.rs b/serde-shape/src/impls/wrapper.rs index 1d9cad9..e464e24 100644 --- a/serde-shape/src/impls/wrapper.rs +++ b/serde-shape/src/impls/wrapper.rs @@ -117,14 +117,14 @@ where } impl DeserializeShape for &str { - fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { - str::deserialize_shape_in(context) + fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::String } } impl DeserializeShape for &[u8] { - fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { - <[u8]>::deserialize_shape_in(context) + fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::Bytes } } @@ -223,8 +223,8 @@ impl DeserializeShape for Box { #[cfg(feature = "std")] impl DeserializeShape for &std::path::Path { - fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { - std::path::Path::deserialize_shape_in(context) + fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::String } } diff --git a/serde-shape/src/lib.rs b/serde-shape/src/lib.rs index 6324d48..e1fc221 100644 --- a/serde-shape/src/lib.rs +++ b/serde-shape/src/lib.rs @@ -331,19 +331,30 @@ pub struct SerializeShapeGraph { } impl SerializeShapeGraph { - /// Build a complete serialization shape graph rooted at `T`. - pub fn for_type() -> Self + /// Build a serialization graph from a function that returns its root shape. + /// + /// This is useful when the root type is foreign or when no Rust type corresponds to the + /// complete wire shape. Use [`Self::for_type`] when the root implements [`SerializeShape`]. + pub fn from_fn(build_root: F) -> Self where - T: SerializeShape + ?Sized, + F: FnOnce(&mut SerializeShapeContext) -> ShapeRef, { let mut context = SerializeShapeContext::default(); - let root = T::serialize_shape_in(&mut context); + let root = build_root(&mut context); Self { root, definitions: context.finish(), } } + /// Build a complete serialization shape graph rooted at `T`. + pub fn for_type() -> Self + where + T: SerializeShape + ?Sized, + { + Self::from_fn(T::serialize_shape_in) + } + /// Return the root shape reference. pub fn root(&self) -> &ShapeRef { &self.root @@ -351,10 +362,7 @@ impl SerializeShapeGraph { /// Return the root definition when the graph root is a named type. pub fn root_definition(&self) -> Option<&SerializeDefinitionShape> { - let ShapeRef::Definition(id) = self.root() else { - return None; - }; - self.definition(*id) + self.definition_for(self.root()) } /// Return the named definitions reachable from the root. @@ -366,6 +374,16 @@ impl SerializeShapeGraph { pub fn definition(&self, id: ShapeId) -> Option<&SerializeDefinitionShape> { self.definitions.get(id.0) } + + /// Return the definition directly referenced by `shape`. + /// + /// Returns `None` for non-definition shapes and for ids that do not belong to this graph. + pub fn definition_for(&self, shape: &ShapeRef) -> Option<&SerializeDefinitionShape> { + let ShapeRef::Definition(id) = shape else { + return None; + }; + self.definition(*id) + } } /// A complete deserialization shape graph rooted at one type. @@ -378,19 +396,46 @@ pub struct DeserializeShapeGraph { } impl DeserializeShapeGraph { - /// Build a complete deserialization shape graph rooted at `T`. - pub fn for_type() -> Self + /// Build a deserialization graph from a function that returns its root shape. + /// + /// This lets a custom shape function describe a foreign root without introducing a wrapper + /// type solely to implement [`DeserializeShape`]. + /// + /// ```rust + /// use serde_shape::DeserializeShapeContext; + /// use serde_shape::DeserializeShapeGraph; + /// use serde_shape::ShapeRef; + /// + /// fn duration_input(_context: &mut DeserializeShapeContext) -> ShapeRef { + /// ShapeRef::union([ShapeRef::String, ShapeRef::U64]) + /// } + /// + /// let graph = DeserializeShapeGraph::from_fn(duration_input); + /// assert_eq!( + /// graph.root(), + /// &ShapeRef::union([ShapeRef::String, ShapeRef::U64]), + /// ); + /// ``` + pub fn from_fn(build_root: F) -> Self where - T: DeserializeShape + ?Sized, + F: FnOnce(&mut DeserializeShapeContext) -> ShapeRef, { let mut context = DeserializeShapeContext::default(); - let root = T::deserialize_shape_in(&mut context); + let root = build_root(&mut context); Self { root, definitions: context.finish(), } } + /// Build a complete deserialization shape graph rooted at `T`. + pub fn for_type() -> Self + where + T: DeserializeShape + ?Sized, + { + Self::from_fn(T::deserialize_shape_in) + } + /// Return the root shape reference. pub fn root(&self) -> &ShapeRef { &self.root @@ -398,10 +443,7 @@ impl DeserializeShapeGraph { /// Return the root definition when the graph root is a named type. pub fn root_definition(&self) -> Option<&DeserializeDefinitionShape> { - let ShapeRef::Definition(id) = self.root() else { - return None; - }; - self.definition(*id) + self.definition_for(self.root()) } /// Return the named definitions reachable from the root. @@ -413,6 +455,16 @@ impl DeserializeShapeGraph { pub fn definition(&self, id: ShapeId) -> Option<&DeserializeDefinitionShape> { self.definitions.get(id.0) } + + /// Return the definition directly referenced by `shape`. + /// + /// Returns `None` for non-definition shapes and for ids that do not belong to this graph. + pub fn definition_for(&self, shape: &ShapeRef) -> Option<&DeserializeDefinitionShape> { + let ShapeRef::Definition(id) = shape else { + return None; + }; + self.definition(*id) + } } /// Accumulates named serialization definitions while a shape graph is built. @@ -427,7 +479,7 @@ impl SerializeShapeContext { /// /// The concrete builder type and diagnostic Rust name form the graph-local identity. Call this /// method from one stable closure expression for every occurrence of the same named type. - pub fn define_named_type(&mut self, type_name: SerializeTypeName, build: F) -> ShapeRef + pub fn define_named_type(&mut self, type_name: TypeName, build: F) -> ShapeRef where F: FnOnce(&mut Self) -> SerializeDefinitionKind + 'static, { @@ -440,7 +492,7 @@ impl SerializeShapeContext { /// definition. pub fn define_named_type_with_description( &mut self, - type_name: SerializeTypeName, + type_name: TypeName, description: Option<&'static str>, build: F, ) -> ShapeRef @@ -486,7 +538,7 @@ impl DeserializeShapeContext { /// /// The concrete builder type and diagnostic Rust name form the graph-local identity. Call this /// method from one stable closure expression for every occurrence of the same named type. - pub fn define_named_type(&mut self, type_name: DeserializeTypeName, build: F) -> ShapeRef + pub fn define_named_type(&mut self, type_name: TypeName, build: F) -> ShapeRef where F: FnOnce(&mut Self) -> DeserializeDefinitionKind + 'static, { @@ -499,7 +551,7 @@ impl DeserializeShapeContext { /// definition. pub fn define_named_type_with_description( &mut self, - type_name: DeserializeTypeName, + type_name: TypeName, description: Option<&'static str>, build: F, ) -> ShapeRef @@ -544,22 +596,26 @@ impl ShapeId { } } -/// Names associated with a Rust type and its Serde serializer. +/// Names associated with a Rust type and one direction of its Serde representation. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct SerializeTypeName { +pub struct TypeName { /// The fully qualified Rust type name, including generic arguments. pub rust_name: &'static str, - /// The Serde serialize name after container rename rules are applied. + /// The direction-specific Serde name after container rename rules are applied. pub name: &'static str, } -/// Names associated with a Rust type and its Serde deserializer. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DeserializeTypeName { - /// The fully qualified Rust type name, including generic arguments. - pub rust_name: &'static str, - /// The Serde deserialize name after container rename rules are applied. - pub name: &'static str, +impl TypeName { + /// Build names for `T` and one direction-specific Serde container name. + pub fn of(name: &'static str) -> Self + where + T: ?Sized, + { + Self { + rust_name: core::any::type_name::(), + name, + } + } } /// A reference to a shape node. @@ -751,7 +807,7 @@ pub struct SerializeDefinitionShape { /// The stable id of this definition inside its graph. pub id: ShapeId, /// The Rust and Serde names for this definition. - pub type_name: SerializeTypeName, + pub type_name: TypeName, /// User-facing documentation for this definition, if available. pub description: Option<&'static str>, /// The definition body. @@ -764,7 +820,7 @@ pub struct DeserializeDefinitionShape { /// The stable id of this definition inside its graph. pub id: ShapeId, /// The Rust and Serde names for this definition. - pub type_name: DeserializeTypeName, + pub type_name: TypeName, /// User-facing documentation for this definition, if available. pub description: Option<&'static str>, /// The definition body. @@ -843,6 +899,10 @@ pub enum Tagging { }, /// `#[serde(untagged)]`. Untagged, + /// `#[serde(field_identifier)]`, accepted only during deserialization. + FieldIdentifier, + /// `#[serde(variant_identifier)]`, accepted only during deserialization. + VariantIdentifier, } /// Struct-like serialization metadata. diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index 25368d4..383715b 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -44,7 +44,8 @@ use crate::DeserializeDefinitionKind; use crate::DeserializeShape; use crate::DeserializeShapeContext; use crate::DeserializeShapeGraph; -use crate::DeserializeTypeName; +#[cfg(all(feature = "std", any(unix, windows)))] +use crate::DeserializeVariantContent; use crate::FieldWireShape; use crate::FieldsStyle; use crate::OpaqueReason; @@ -53,9 +54,9 @@ use crate::SerializeDefinitionKind; use crate::SerializeShape; use crate::SerializeShapeContext; use crate::SerializeShapeGraph; -use crate::SerializeTypeName; use crate::ShapeRef; use crate::Tagging; +use crate::TypeName; struct BorrowedShape; @@ -134,7 +135,7 @@ fn normalizes_union_shapes() { fn keeps_distinct_definition_builders_with_the_same_type_name() { let mut serialize = SerializeShapeContext::default(); let first = serialize.define_named_type( - SerializeTypeName { + TypeName { rust_name: "duplicate::Type", name: "First", }, @@ -147,7 +148,7 @@ fn keeps_distinct_definition_builders_with_the_same_type_name() { }, ); let second = serialize.define_named_type( - SerializeTypeName { + TypeName { rust_name: "duplicate::Type", name: "Second", }, @@ -165,7 +166,7 @@ fn keeps_distinct_definition_builders_with_the_same_type_name() { let mut deserialize = DeserializeShapeContext::default(); let first = deserialize.define_named_type( - DeserializeTypeName { + TypeName { rust_name: "duplicate::Type", name: "First", }, @@ -178,7 +179,7 @@ fn keeps_distinct_definition_builders_with_the_same_type_name() { }, ); let second = deserialize.define_named_type( - DeserializeTypeName { + TypeName { rust_name: "duplicate::Type", name: "Second", }, @@ -195,7 +196,7 @@ fn keeps_distinct_definition_builders_with_the_same_type_name() { assert_eq!(deserialize.finish().len(), 2); } -#[cfg(target_has_atomic = "ptr")] +#[cfg(all(feature = "std", target_has_atomic = "ptr"))] #[test] fn maps_atomic_shapes() { assert_eq!( @@ -235,10 +236,6 @@ fn distinguishes_byte_sequences_from_borrowed_byte_input() { SerializeShapeGraph::for_type::>().root(), &ShapeRef::Seq(Box::new(ShapeRef::U8)) ); - assert_eq!( - <[u8] as DeserializeShape>::deserialize_shape().root(), - &ShapeRef::Bytes - ); assert_eq!( <&[u8] as DeserializeShape>::deserialize_shape().root(), &ShapeRef::Bytes @@ -411,6 +408,10 @@ fn supports_serde_tuple_arity() { #[test] fn maps_common_core_and_alloc_shapes() { + assert_eq!( + SerializeShapeGraph::for_type::>().root(), + &ShapeRef::String + ); assert_eq!( DeserializeShapeGraph::for_type::>().root(), &ShapeRef::String @@ -514,11 +515,11 @@ fn maps_common_std_shapes() { &ShapeRef::String ); assert_eq!( - DeserializeShapeGraph::for_type::().root(), + SerializeShapeGraph::for_type::().root(), &ShapeRef::String ); assert_eq!( - SerializeShapeGraph::for_type::().root(), + DeserializeShapeGraph::for_type::().root(), &ShapeRef::String ); assert_eq!( @@ -548,6 +549,41 @@ fn maps_common_std_shapes() { assert!(shape.attributes.deny_unknown_fields); } +#[cfg(all(feature = "std", any(unix, windows)))] +#[test] +fn maps_os_strings_as_platform_enums() { + let serialize = SerializeShapeGraph::for_type::(); + let serialize = serialize + .root_definition() + .expect("OsString should have a named serialization definition"); + let SerializeDefinitionKind::Enum(serialize) = &serialize.kind else { + panic!("OsString should serialize as an enum"); + }; + + let deserialize = DeserializeShapeGraph::for_type::>(); + let deserialize = deserialize + .root_definition() + .expect("Box should have a named deserialization definition"); + let DeserializeDefinitionKind::Enum(deserialize) = &deserialize.kind else { + panic!("Box should deserialize from an enum"); + }; + + #[cfg(unix)] + let (variant, item) = ("Unix", ShapeRef::U8); + #[cfg(windows)] + let (variant, item) = ("Windows", ShapeRef::U16); + + assert_eq!(serialize.variants[0].name, variant); + assert_eq!(deserialize.variants[0].name, variant); + let DeserializeVariantContent::Fields(fields) = &deserialize.variants[0].content else { + panic!("platform variant should contain one value"); + }; + assert_eq!( + fields[0].wire_shape, + FieldWireShape::Value(ShapeRef::Seq(Box::new(item))) + ); +} + #[test] fn maps_network_shapes_without_std() { let ipv4_binary = ShapeRef::Array { diff --git a/tests/derive/tests/derive.rs b/tests/derive/tests/derive.rs index fb55fd5..05d2b18 100644 --- a/tests/derive/tests/derive.rs +++ b/tests/derive/tests/derive.rs @@ -75,6 +75,24 @@ enum Storage { Other, } +#[derive(DeserializeShape)] +#[serde(field_identifier, rename_all = "snake_case")] +enum FieldIdentifier { + KnownField, + #[serde(alias = "old_name")] + RenamedField, + #[serde(other)] + Other, +} + +#[derive(DeserializeShape)] +#[serde(variant_identifier)] +enum VariantIdentifier { + First, + #[serde(alias = "legacy")] + Second, +} + #[derive(DeserializeShape)] #[serde(transparent)] struct UserId(u64); @@ -154,12 +172,14 @@ struct Marker { marker: core::marker::PhantomData, } -#[derive(DeserializeShape)] +#[derive(serde::Deserialize, DeserializeShape)] struct BorrowedCow<'a> { #[serde(borrow)] text: Cow<'a, str>, #[serde(borrow)] bytes: Cow<'a, [u8]>, + #[serde(borrow, deserialize_with = "deserialize_borrowed_str")] + custom: Cow<'a, str>, } #[derive(DeserializeShape)] @@ -272,6 +292,13 @@ fn deserialize_bool_shape(_context: &mut DeserializeShapeContext) -> ShapeRef { ShapeRef::Bool } +fn deserialize_borrowed_str<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + <&'de str as serde::Deserialize>::deserialize(deserializer).map(Cow::Borrowed) +} + fn default_retries() -> u8 { 3 } @@ -317,6 +344,25 @@ fn exposes_deserialize_enum_attributes() { assert!(shape.variants[2].other); } +#[test] +fn reflects_serde_identifier_enums() { + let field = deserialize_root_definition::(); + let DeserializeDefinitionKind::Enum(field) = &field.kind else { + panic!("field identifier should be an enum"); + }; + assert_eq!(field.repr, Tagging::FieldIdentifier); + assert_eq!(field.variants[0].name, "known_field"); + assert_eq!(field.variants[1].aliases, ["old_name", "renamed_field"]); + assert!(field.variants[2].other); + + let variant = deserialize_root_definition::(); + let DeserializeDefinitionKind::Enum(variant) = &variant.kind else { + panic!("variant identifier should be an enum"); + }; + assert_eq!(variant.repr, Tagging::VariantIdentifier); + assert_eq!(variant.variants[1].aliases, ["Second", "legacy"]); +} + #[test] fn exposes_transparent_shape() { let definition = deserialize_root_definition::(); @@ -522,12 +568,17 @@ fn preserves_serde_borrowed_cow_shapes() { let DeserializeDefinitionKind::Struct(shape) = &definition.kind else { panic!("definition should be a struct"); }; - let [text, bytes] = shape.fields.as_slice() else { + let [text, bytes, custom] = shape.fields.as_slice() else { panic!("borrowed Cow fields should be reflected"); }; assert_eq!(text.wire_shape, FieldWireShape::Value(ShapeRef::String)); assert_eq!(bytes.wire_shape, FieldWireShape::Value(ShapeRef::Bytes)); + let FieldWireShape::Value(ShapeRef::Opaque(opaque)) = &custom.wire_shape else { + panic!("explicit custom deserializer should remain opaque"); + }; + assert_eq!(opaque.reason, OpaqueReason::CustomDeserializer); + assert_eq!(opaque.detail, Some("deserialize_borrowed_str")); } #[test] diff --git a/tests/integration/tests/configenv.rs b/tests/integration/tests/configenv.rs index 8cd53bc..a864adf 100644 --- a/tests/integration/tests/configenv.rs +++ b/tests/integration/tests/configenv.rs @@ -15,13 +15,10 @@ #![allow(dead_code)] use serde_shape::DeserializeDefinitionKind; -use serde_shape::DeserializeDefinitionShape; use serde_shape::DeserializeShape; -use serde_shape::DeserializeShapeGraph; use serde_shape::DeserializeVariantContent; use serde_shape::FieldWireShape; use serde_shape::FieldsStyle; -use serde_shape::ShapeRef; use serde_shape::Tagging; #[derive(DeserializeShape)] @@ -64,13 +61,22 @@ fn exposes_the_structure_needed_by_config_consumers() { }; assert!(matches!(common.wire_shape, FieldWireShape::Flatten(_))); - let common = definition_for_wire_shape(&graph, &common.wire_shape); + let common = graph + .definition_for(common.wire_shape.shape().expect("field should be present")) + .expect("field should reference a named definition"); let DeserializeDefinitionKind::Struct(common) = &common.kind else { panic!("flattened config should be a struct"); }; assert_eq!(common.fields[0].name, "retries"); - let transport = definition_for_wire_shape(&graph, &transport.wire_shape); + let transport = graph + .definition_for( + transport + .wire_shape + .shape() + .expect("field should be present"), + ) + .expect("field should reference a named definition"); let DeserializeDefinitionKind::Enum(transport) = &transport.kind else { panic!("transport should be an enum"); }; @@ -86,7 +92,9 @@ fn exposes_the_structure_needed_by_config_consumers() { panic!("newtype variant should expose its payload field"); }; - let tcp = definition_for_wire_shape(&graph, &payload.wire_shape); + let tcp = graph + .definition_for(payload.wire_shape.shape().expect("field should be present")) + .expect("field should reference a named definition"); let DeserializeDefinitionKind::Struct(tcp) = &tcp.kind else { panic!("TCP payload should be a struct"); }; @@ -98,14 +106,3 @@ fn exposes_the_structure_needed_by_config_consumers() { ["host", "port", "tls.version"] ); } - -fn definition_for_wire_shape<'a>( - graph: &'a DeserializeShapeGraph, - wire_shape: &FieldWireShape, -) -> &'a DeserializeDefinitionShape { - let shape = wire_shape.shape().expect("field should be present"); - let ShapeRef::Definition(id) = shape else { - panic!("field should reference a named definition"); - }; - graph.definition(*id).expect("definition should exist") -}