From 77e725ac11284975082f6c3dfbaf2086656eddee Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 01:17:09 +0800 Subject: [PATCH 01/10] refactor: share type-name metadata across directions Replace the identical serialization and deserialization name records with TypeName, and add TypeName::of for the common type_name plus Serde-name construction path. Why: direction is already expressed by the graph containing the value. Two structurally identical public types made manual implementations and consumers duplicate imports and conversion code without preventing any invalid state. --- CHANGELOG.md | 1 + serde-shape-derive/src/lib.rs | 10 +-- serde-shape/src/impls/bound.rs | 66 +++++++------------ serde-shape/src/impls/net.rs | 108 ++++++++++++-------------------- serde-shape/src/impls/range.rs | 14 +---- serde-shape/src/impls/result.rs | 56 +++++++---------- serde-shape/src/impls/time.rs | 7 +-- serde-shape/src/lib.rs | 36 ++++++----- serde-shape/src/tests.rs | 11 ++-- 9 files changed, 119 insertions(+), 190 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee6327..6c3e35a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Breaking changes +* 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`. diff --git a/serde-shape-derive/src/lib.rs b/serde-shape-derive/src/lib.rs index 728bf9a..370c030 100644 --- a/serde-shape-derive/src/lib.rs +++ b/serde-shape-derive/src/lib.rs @@ -535,10 +535,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 +566,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 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/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/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/lib.rs b/serde-shape/src/lib.rs index 6324d48..444998c 100644 --- a/serde-shape/src/lib.rs +++ b/serde-shape/src/lib.rs @@ -427,7 +427,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 +440,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 +486,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 +499,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 +544,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 +755,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 +768,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. diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index 25368d4..0518e76 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -44,7 +44,6 @@ use crate::DeserializeDefinitionKind; use crate::DeserializeShape; use crate::DeserializeShapeContext; use crate::DeserializeShapeGraph; -use crate::DeserializeTypeName; use crate::FieldWireShape; use crate::FieldsStyle; use crate::OpaqueReason; @@ -53,9 +52,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 +133,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 +146,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 +164,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 +177,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", }, From 70c961df1a84dc4be383a1dfc3403e386a955646 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 01:17:47 +0800 Subject: [PATCH 02/10] feat: build graphs from custom root functions Add from_fn constructors to both graph directions and route for_type through them. The builder receives the normal graph context, so named and recursive definitions remain available. Why: custom shape functions already work at derive boundaries, but a foreign root still required a throwaway local wrapper. The graph API should accept the same customization mechanism directly. --- CHANGELOG.md | 1 + serde-shape/src/lib.rs | 54 +++++++++++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3e35a..a6cdc0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file. ### New features +* 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. diff --git a/serde-shape/src/lib.rs b/serde-shape/src/lib.rs index 444998c..62c76d8 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 @@ -378,19 +389,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 From c19ac631da400f87d5351073ca243b94944cad25 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 01:18:25 +0800 Subject: [PATCH 03/10] feat: resolve definitions directly from shape refs Add definition_for to both graph directions, use it for root_definition, and remove the downstream helper that repeated ShapeRef matching plus id lookup. Why: every graph walker reaches definitions through ShapeRef values. Requiring each consumer to reproduce this plumbing adds noise and creates inconsistent handling of non-definition or foreign ids. --- CHANGELOG.md | 1 + serde-shape/src/lib.rs | 30 ++++++++++++++++++++------- tests/integration/tests/configenv.rs | 31 +++++++++++++--------------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6cdc0a..30ab17c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,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/serde-shape/src/lib.rs b/serde-shape/src/lib.rs index 62c76d8..5589016 100644 --- a/serde-shape/src/lib.rs +++ b/serde-shape/src/lib.rs @@ -362,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. @@ -377,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. @@ -436,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. @@ -451,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. 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") -} From 7415d9c3149f91064360e8686bad72e0d6990c32 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 01:19:10 +0800 Subject: [PATCH 04/10] fix: expose only real borrowed input shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove DeserializeShape from unsized str, byte slices, and Path while keeping explicit implementations for Serde-supported borrowed and owned forms. Why: the trait describes a type’s deserialization contract, but those unsized types do not implement Serde Deserialize themselves. Advertising them made compile-time reflection claim capabilities that callers could not use. --- CHANGELOG.md | 1 + README.md | 4 +++- serde-shape/src/impls/primitive.rs | 28 +++++++++++++++++++++------- serde-shape/src/impls/wrapper.rs | 12 ++++++------ serde-shape/src/tests.rs | 8 ++------ 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30ab17c..a23b96a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Breaking changes +* 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. diff --git a/README.md b/README.md index 633a0c3..d906c80 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ 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`, non-zero integers, and atomics available on the target | | 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 | @@ -173,6 +173,8 @@ Serde's `rc` feature is still required to serialize or deserialize `Rc`, `Arc`, 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/src/impls/primitive.rs b/serde-shape/src/impls/primitive.rs index 5c34c21..fa74e8d 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,16 +71,31 @@ 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 } } #[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")] 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/tests.rs b/serde-shape/src/tests.rs index 0518e76..557c53e 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -234,10 +234,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 @@ -513,11 +509,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!( From 43af1d2b581e509f41ba640e217c1ce1013dce01 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 01:19:40 +0800 Subject: [PATCH 05/10] fix: gate atomic shapes with std MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move atomic SerializeShape and DeserializeShape implementations, their tests, and their support documentation behind the crate’s std feature. Why: Serde only implements its atomic traits with std enabled. Exposing atomic shapes in a no_std graph claimed a contract that the matching Serde configuration cannot provide. --- CHANGELOG.md | 1 + README.md | 4 ++-- serde-shape/src/impls/primitive.rs | 10 +++++----- serde-shape/src/tests.rs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a23b96a..0eb78d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Breaking changes +* 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. diff --git a/README.md b/README.md index d906c80..28909bc 100644 --- a/README.md +++ b/README.md @@ -158,14 +158,14 @@ The built-in implementations follow Serde's semantic representations in each dir | Group | Supported types | | --- | --- | -| Scalars | Rust primitives, `String`, serialized `str`, non-zero integers, and atomics available on the target | +| Scalars | Rust primitives, `String`, serialized `str`, 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 | | 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`. diff --git a/serde-shape/src/impls/primitive.rs b/serde-shape/src/impls/primitive.rs index fa74e8d..ee8ecd2 100644 --- a/serde-shape/src/impls/primitive.rs +++ b/serde-shape/src/impls/primitive.rs @@ -98,32 +98,32 @@ impl DeserializeShape for std::path::PathBuf { } } -#[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/tests.rs b/serde-shape/src/tests.rs index 557c53e..45f91ee 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -194,7 +194,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!( From 075469a0a6584eaca6e6ba9e065ccbe96f9b0080 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 01:22:04 +0800 Subject: [PATCH 06/10] fix: model Serde identifier enums explicitly Why: field_identifier and variant_identifier enums deserialize through Serde's identifier visitor rather than the externally tagged enum protocol. Calling them externally tagged misleads graph consumers, while allowing a serialization shape contradicts Serde's own derive rejection. --- CHANGELOG.md | 1 + serde-shape-derive/src/lib.rs | 22 ++++++++++++++++++++- serde-shape/src/lib.rs | 4 ++++ tests/derive/tests/derive.rs | 37 +++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eb78d6..574e63a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ 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::()`. diff --git a/serde-shape-derive/src/lib.rs b/serde-shape-derive/src/lib.rs index 370c030..c78095a 100644 --- a/serde-shape-derive/src/lib.rs +++ b/serde-shape-derive/src/lib.rs @@ -144,6 +144,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) } @@ -637,7 +649,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) @@ -937,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), diff --git a/serde-shape/src/lib.rs b/serde-shape/src/lib.rs index 5589016..e1fc221 100644 --- a/serde-shape/src/lib.rs +++ b/serde-shape/src/lib.rs @@ -899,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/tests/derive/tests/derive.rs b/tests/derive/tests/derive.rs index fb55fd5..a063d5e 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); @@ -317,6 +335,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::(); From ea780f66b100a572d2fc5b5dba9c2905b0a351b2 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 01:22:48 +0800 Subject: [PATCH 07/10] fix: recognize only Serde's private Cow helpers Why: matching only the helper function name caused unrelated custom deserializers such as a user-defined borrow_cow_str path to be reported as String. The recovery is valid only for the exact private path synthesized by serde_derive_internals. --- CHANGELOG.md | 1 + serde-shape-derive/src/lib.rs | 8 ++++++-- tests/derive/tests/derive.rs | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 574e63a..f9ba763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ All notable changes to this project will be documented in this file. ### Bug fixes +* Recognize only Serde's private borrowing helpers when recovering `Cow` and `Cow<[u8]>` shapes, leaving similarly named user deserializers opaque. * 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. * Distinguish borrowed byte input from owned boxed slices: `&[u8]` reflects bytes while `Box<[u8]>` reflects a sequence, matching Serde. diff --git a/serde-shape-derive/src/lib.rs b/serde-shape-derive/src/lib.rs index c78095a..01b14de 100644 --- a/serde-shape-derive/src/lib.rs +++ b/serde-shape-derive/src/lib.rs @@ -980,10 +980,14 @@ fn serde_borrowed_cow_shape(path: &syn::ExprPath) -> Option { let mut segments = path.path.segments.iter(); let serde = segments.next()?; - let _private = 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" { + if segments.next().is_some() + || serde.ident != "_serde" + || private.ident != "__private" + || de.ident != "de" + { return None; } diff --git a/tests/derive/tests/derive.rs b/tests/derive/tests/derive.rs index a063d5e..2c4e54c 100644 --- a/tests/derive/tests/derive.rs +++ b/tests/derive/tests/derive.rs @@ -180,6 +180,12 @@ struct BorrowedCow<'a> { bytes: Cow<'a, [u8]>, } +#[derive(DeserializeShape)] +struct CustomCowNamedHelper { + #[serde(deserialize_with = "_serde::custom::de::borrow_cow_str")] + value: NotShape, +} + #[derive(DeserializeShape)] struct Recursive { child: Option>, @@ -567,6 +573,20 @@ fn preserves_serde_borrowed_cow_shapes() { assert_eq!(bytes.wire_shape, FieldWireShape::Value(ShapeRef::Bytes)); } +#[test] +fn keeps_custom_cow_named_helpers_opaque() { + let definition = deserialize_root_definition::(); + let DeserializeDefinitionKind::Struct(shape) = &definition.kind else { + panic!("definition should be a struct"); + }; + let FieldWireShape::Value(ShapeRef::Opaque(opaque)) = &shape.fields[0].wire_shape else { + panic!("custom helper should remain opaque"); + }; + + assert_eq!(opaque.reason, OpaqueReason::CustomDeserializer); + assert_eq!(opaque.detail, Some("_serde::custom::de::borrow_cow_str")); +} + #[test] fn exposes_serialize_field_metadata() { let definition = serialize_root_definition::(); From e08fbea1f8fb9726e09b72af1f8e1e54d517aaf5 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 01:23:31 +0800 Subject: [PATCH 08/10] feat: reflect formatted arguments as strings Why: Serde serializes core::fmt::Arguments through its string representation, but downstream crates cannot add SerializeShape for this foreign standard-library type themselves. --- CHANGELOG.md | 1 + README.md | 2 +- serde-shape/src/impls/primitive.rs | 6 ++++++ serde-shape/src/tests.rs | 4 ++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ba763..0be1b74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ 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 `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. diff --git a/README.md b/README.md index 28909bc..78d5b97 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ The built-in implementations follow Serde's semantic representations in each dir | Group | Supported types | | --- | --- | -| Scalars | Rust primitives, `String`, serialized `str`, and non-zero integers | +| 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 | diff --git a/serde-shape/src/impls/primitive.rs b/serde-shape/src/impls/primitive.rs index ee8ecd2..6b03f3c 100644 --- a/serde-shape/src/impls/primitive.rs +++ b/serde-shape/src/impls/primitive.rs @@ -77,6 +77,12 @@ impl SerializeShape for str { } } +impl SerializeShape for core::fmt::Arguments<'_> { + fn serialize_shape_in(_context: &mut SerializeShapeContext) -> ShapeRef { + ShapeRef::String + } +} + #[cfg(feature = "std")] impl SerializeShape for std::path::Path { fn serialize_shape_in(_context: &mut SerializeShapeContext) -> ShapeRef { diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index 45f91ee..faae283 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -406,6 +406,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 From fccb5725bff8846ac36c209105d455ac77baf7f2 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 01:25:47 +0800 Subject: [PATCH 09/10] feat: add platform-aware OS string shapes Why: Serde supports OsStr and OsString on Unix and Windows, but downstream crates cannot implement shape traits for these foreign standard-library types. Their wire form is platform-specific, so a plain String shape would hide the tagged Unix byte or Windows wide-unit representation. --- CHANGELOG.md | 1 + README.md | 4 +- serde-shape/src/impls/mod.rs | 2 + serde-shape/src/impls/os_string.rs | 135 +++++++++++++++++++++++++++++ serde-shape/src/tests.rs | 37 ++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 serde-shape/src/impls/os_string.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be1b74..70c072e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ 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. diff --git a/README.md b/README.md index 78d5b97..0da5ec5 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ The built-in implementations follow Serde's semantic representations in each dir | 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 | @@ -169,6 +169,8 @@ The built-in implementations follow Serde's semantic representations in each dir 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. 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/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/tests.rs b/serde-shape/src/tests.rs index faae283..383715b 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -44,6 +44,8 @@ use crate::DeserializeDefinitionKind; use crate::DeserializeShape; use crate::DeserializeShapeContext; use crate::DeserializeShapeGraph; +#[cfg(all(feature = "std", any(unix, windows)))] +use crate::DeserializeVariantContent; use crate::FieldWireShape; use crate::FieldsStyle; use crate::OpaqueReason; @@ -547,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 { From 77681a0516fc54ab99970ca635d9b2977ad62c96 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 18:26:04 +0800 Subject: [PATCH 10/10] refactor: derive borrowed Cow shapes from source metadata Replace matching against serde_derive_internals' synthesized _serde::__private helper paths with the field's borrowing metadata, Cow element type, and explicit source attributes. The regression fixture now also derives real Serde Deserialize and keeps an explicit custom deserializer opaque.\n\nWhy: Serde's generated private path is neither a serde-shape contract nor an understandable maintenance boundary. Expressing the source-level behavior removes an undocumented coupling and makes custom-deserializer precedence visible in the code. --- CHANGELOG.md | 3 +- serde-shape-derive/src/lib.rs | 105 +++++++++++++++++++++++++--------- tests/derive/tests/derive.rs | 34 +++++------ 3 files changed, 92 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70c072e..5974b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,9 +38,8 @@ All notable changes to this project will be documented in this file. ### Bug fixes -* Recognize only Serde's private borrowing helpers when recovering `Cow` and `Cow<[u8]>` shapes, leaving similarly named user deserializers opaque. * 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. diff --git a/serde-shape-derive/src/lib.rs b/serde-shape-derive/src/lib.rs index 01b14de..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; @@ -855,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()); @@ -870,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)) @@ -973,31 +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" - || private.ident != "__private" - || 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) } +} - 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 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) +} + +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/tests/derive/tests/derive.rs b/tests/derive/tests/derive.rs index 2c4e54c..05d2b18 100644 --- a/tests/derive/tests/derive.rs +++ b/tests/derive/tests/derive.rs @@ -172,18 +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]>, -} - -#[derive(DeserializeShape)] -struct CustomCowNamedHelper { - #[serde(deserialize_with = "_serde::custom::de::borrow_cow_str")] - value: NotShape, + #[serde(borrow, deserialize_with = "deserialize_borrowed_str")] + custom: Cow<'a, str>, } #[derive(DeserializeShape)] @@ -296,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 } @@ -565,26 +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)); -} - -#[test] -fn keeps_custom_cow_named_helpers_opaque() { - let definition = deserialize_root_definition::(); - let DeserializeDefinitionKind::Struct(shape) = &definition.kind else { - panic!("definition should be a struct"); - }; - let FieldWireShape::Value(ShapeRef::Opaque(opaque)) = &shape.fields[0].wire_shape else { - panic!("custom helper should remain opaque"); + 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("_serde::custom::de::borrow_cow_str")); + assert_eq!(opaque.detail, Some("deserialize_borrowed_str")); } #[test]