From 97972d4525feebbf0f1ace3cc2ae67baf73c279d Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:46:43 +0800 Subject: [PATCH 01/14] fix: match Serde's serialization bounds BinaryHeap does not require Ord to serialize, and RefCell, Mutex, and RwLock can serialize unsized contents. Mirror those directional bounds in the shape implementations. Why: callers should not lose shape reflection for a type that Serde itself can serialize, especially when orphan rules prevent downstream crates from repairing standard-library implementations. --- CHANGELOG.md | 1 + serde-shape/src/impls/container.rs | 2 +- serde-shape/src/impls/wrapper.rs | 6 +++--- serde-shape/src/tests.rs | 21 +++++++++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cecd5ad..b9a0c4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file. ### Bug fixes +* 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. * Follow Serde's directional bounds for `Cow`: serialization reflects the borrowed type and deserialization reflects the owned type. * Make IP and socket address shapes available in `no_std` builds through `core::net`. diff --git a/serde-shape/src/impls/container.rs b/serde-shape/src/impls/container.rs index f3e4921..f8869f9 100644 --- a/serde-shape/src/impls/container.rs +++ b/serde-shape/src/impls/container.rs @@ -97,7 +97,7 @@ seq_shape! { (T) BinaryHeap where - serialize { T: Ord + SerializeShape } + serialize { T: SerializeShape } deserialize { T: Ord + DeserializeShape } => T; diff --git a/serde-shape/src/impls/wrapper.rs b/serde-shape/src/impls/wrapper.rs index 74a24c5..e7bcda8 100644 --- a/serde-shape/src/impls/wrapper.rs +++ b/serde-shape/src/impls/wrapper.rs @@ -86,7 +86,7 @@ transparent_shape! { (T) RefCell where - serialize { T: SerializeShape } + serialize { T: SerializeShape + ?Sized } deserialize { T: DeserializeShape } => T; @@ -107,13 +107,13 @@ transparent_shape! { transparent_shape! { (T) std::sync::Mutex where - serialize { T: SerializeShape } + serialize { T: SerializeShape + ?Sized } deserialize { T: DeserializeShape } => T; (T) std::sync::RwLock where - serialize { T: SerializeShape } + serialize { T: SerializeShape + ?Sized } deserialize { T: DeserializeShape } => T; } diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index 6a070fb..b5e528f 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -24,6 +24,7 @@ use alloc::vec; use alloc::vec::Vec; use core::borrow::Borrow; use core::cell::Cell; +use core::cell::RefCell; use core::cmp::Reverse; use core::num::Wrapping; @@ -348,6 +349,18 @@ fn maps_common_core_and_alloc_shapes() { ); } +#[test] +fn accepts_serde_serializable_collection_and_wrapper_bounds() { + assert_eq!( + SerializeShapeGraph::for_type::>().root(), + &ShapeRef::Seq(Box::new(ShapeRef::U8)) + ); + assert_eq!( + as SerializeShape>::serialize_shape().root(), + &ShapeRef::String + ); +} + #[test] fn follows_cow_directional_serde_bounds() { assert_eq!( @@ -375,6 +388,14 @@ fn maps_common_std_shapes() { SerializeShapeGraph::for_type::().root(), &ShapeRef::String ); + assert_eq!( + as SerializeShape>::serialize_shape().root(), + &ShapeRef::String + ); + assert_eq!( + as SerializeShape>::serialize_shape().root(), + &ShapeRef::String + ); } #[test] From bd82a83dad4f4637d478c2d92b20e4340732e751 Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:47:25 +0800 Subject: [PATCH 02/14] feat: default ordinary container metadata Implement Default for the two container attribute records and their Tagging and DefaultShape fields. The defaults represent Serde's ordinary externally tagged container with no optional behavior enabled. Why: manual Shape implementations currently repeat every false and None field, which makes straightforward implementations noisy and makes the intended baseline harder to discover. --- CHANGELOG.md | 1 + serde-shape/src/lib.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9a0c4e..10644ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ All notable changes to this project will be documented in this file. ### Improvements +* Implement `Default` for container attributes, `Tagging`, and `DefaultShape` so manual shape implementations can initialize ordinary Serde metadata concisely. * Allow unsized types such as `str` and slices to use the `SerializeShape::serialize_shape` and `DeserializeShape::deserialize_shape` convenience methods directly. * Document runtime graph construction, format-dependent unions, graph-local identifiers, built-in coverage, and custom representation boundaries. * Replace broad debug snapshots with focused behavior assertions and remove the snapshot-testing dependency. diff --git a/serde-shape/src/lib.rs b/serde-shape/src/lib.rs index ab9f5ce..d1aa027 100644 --- a/serde-shape/src/lib.rs +++ b/serde-shape/src/lib.rs @@ -790,7 +790,7 @@ pub enum DeserializeDefinitionKind { } /// Serde attributes that apply to a whole serialized container. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SerializeContainerAttributes { /// The container tagging representation. pub tagging: Tagging, @@ -803,7 +803,7 @@ pub struct SerializeContainerAttributes { } /// Serde attributes that apply to a whole deserialized container. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct DeserializeContainerAttributes { /// The container tagging representation. pub tagging: Tagging, @@ -822,9 +822,10 @@ pub struct DeserializeContainerAttributes { } /// Serde container or enum tagging representation. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub enum Tagging { /// The default externally tagged representation. + #[default] External, /// `#[serde(tag = "...")]`. Internal { @@ -1017,9 +1018,10 @@ pub enum DeserializeVariantContent { } /// A Serde default marker. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub enum DefaultShape { /// No default is configured. + #[default] None, /// `Default::default()` is used. Default, From 3c7de197c59fc22bd3d556e5b525ee1053559b42 Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:48:52 +0800 Subject: [PATCH 03/14] feat: let variants declare custom content shapes Accept directional serde_shape hooks on enum variants and expose their result as VariantContent::Shape. Keep variant-level Serde custom functions opaque when no shape hook is present, and avoid inferring field bounds when the hook replaces those fields. Why: Serde supports custom variant representations, but callers had no way to describe a known representation and were forced to expose an opaque boundary even when they could describe it precisely. --- CHANGELOG.md | 1 + README.md | 2 ++ serde-shape-derive/src/lib.rs | 36 +++++++++++++++------ serde-shape-derive/src/shape_attr.rs | 4 --- serde-shape/src/lib.rs | 24 ++++++++------ tests/derive/tests/serde_compat.rs | 48 ++++++++++++++++++++++++++++ 6 files changed, 92 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10644ab..d2a90aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. ### New features * 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. * Add directional `#[serde_shape(bound(...))]` overrides for generic custom shape hooks. * Add `SerializeShapeGraph::root_definition` and `DeserializeShapeGraph::root_definition` for directly inspecting named root types. * Preserve Rust doc comments on derived containers, variants, and fields as user-facing descriptions. diff --git a/README.md b/README.md index 1fc057d..4b1cc4e 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,8 @@ struct Config { Each function receives the current graph context and returns a `ShapeRef`. It may delegate to another type's shape implementation or construct a custom shape directly. A custom shape function is an assertion about the Serde behavior; `serde-shape` cannot verify that the declared shape matches the serializer or deserializer implementation. +The same `serde_shape` hooks can be placed on enum variants whose content is controlled by a variant-level Serde custom function. Without an explicit hook, custom variant content remains opaque. + For a generic custom hook, container-level `#[serde_shape(bound(serialize = "...", deserialize = "..."))]` replaces the automatically inferred bounds in the corresponding direction, following Serde's bound-override convention. ## Model boundaries diff --git a/serde-shape-derive/src/lib.rs b/serde-shape-derive/src/lib.rs index f66bb27..8ac243e 100644 --- a/serde-shape-derive/src/lib.rs +++ b/serde-shape-derive/src/lib.rs @@ -149,13 +149,7 @@ fn validate_shape_attrs(container: &ast::Container<'_>) -> syn::Result<()> { match &container.data { ast::Data::Enum(variants) => { for variant in variants { - let attrs = ShapeAttrs::parse(&variant.original.attrs)?; - if !attrs.is_empty() { - return Err(syn::Error::new_spanned( - variant.original, - "serde_shape attributes are not supported on variants", - )); - } + validate_variant_shape_attrs(variant)?; for field in &variant.fields { validate_field_shape_attrs(field)?; } @@ -170,6 +164,17 @@ fn validate_shape_attrs(container: &ast::Container<'_>) -> syn::Result<()> { Ok(()) } +fn validate_variant_shape_attrs(variant: &ast::Variant<'_>) -> syn::Result<()> { + let attrs = ShapeAttrs::parse(&variant.original.attrs)?; + if attrs.has_bound() { + return Err(syn::Error::new_spanned( + variant.original, + "serde_shape bounds are supported on containers, not variants", + )); + } + Ok(()) +} + fn validate_field_shape_attrs(field: &ast::Field<'_>) -> syn::Result<()> { let attrs = ShapeAttrs::parse(&field.original.attrs)?; if attrs.has_bound() { @@ -221,7 +226,11 @@ fn add_serialize_shape_bounds( } ast::Data::Enum(variants) => { for variant in variants { - if variant.attrs.skip_serializing() || variant.attrs.serialize_with().is_some() { + let variant_shape_attrs = ShapeAttrs::parse(&variant.original.attrs)?; + if variant.attrs.skip_serializing() + || variant.attrs.serialize_with().is_some() + || variant_shape_attrs.serialize_with().is_some() + { continue; } collect_serialize_field_bound_types( @@ -286,7 +295,10 @@ fn add_deserialize_shape_bounds( } ast::Data::Enum(variants) => { for variant in variants { - if variant.attrs.skip_deserializing() || variant.attrs.deserialize_with().is_some() + let variant_shape_attrs = ShapeAttrs::parse(&variant.original.attrs)?; + if variant.attrs.skip_deserializing() + || variant.attrs.deserialize_with().is_some() + || variant_shape_attrs.deserialize_with().is_some() { continue; } @@ -699,6 +711,7 @@ fn deserialize_container_attributes(attrs: &attr::Container) -> TokenStream2 { } fn serialize_variant_shape(variant: &ast::Variant<'_>) -> syn::Result { + let shape_attrs = ShapeAttrs::parse(&variant.original.attrs)?; let rust_name = lit(variant.ident.to_string()); let name = lit(variant.attrs.name().serialize_name()); let description = description(&variant.original.attrs); @@ -708,6 +721,8 @@ fn serialize_variant_shape(variant: &ast::Variant<'_>) -> syn::Result) -> syn::Result) -> syn::Result { + let shape_attrs = ShapeAttrs::parse(&variant.original.attrs)?; let rust_name = lit(variant.ident.to_string()); let name = lit(variant.attrs.name().deserialize_name()); let aliases = aliases(variant.attrs.aliases()); @@ -754,6 +770,8 @@ fn deserialize_variant_shape(variant: &ast::Variant<'_>) -> syn::Result bool { self.serialize_bound.is_some() || self.deserialize_bound.is_some() } - - pub fn is_empty(&self) -> bool { - self.serialize_with.is_none() && self.deserialize_with.is_none() && !self.has_bound() - } } pub fn description(attrs: &[Attribute]) -> Option { diff --git a/serde-shape/src/lib.rs b/serde-shape/src/lib.rs index d1aa027..aa4c438 100644 --- a/serde-shape/src/lib.rs +++ b/serde-shape/src/lib.rs @@ -152,9 +152,9 @@ //! [`ShapeRef::Opaque`] and remain composable with those field positions. //! //! Use `#[serde_shape(serialize_with = "path")]` or -//! `#[serde_shape(deserialize_with = "path")]` to declare the representation of a container or -//! field that cannot be inferred. Each function receives the current graph context and returns a -//! [`ShapeRef`], so it can delegate to another type or build a custom shape directly. +//! `#[serde_shape(deserialize_with = "path")]` to declare the representation of a container, +//! variant, or field that cannot be inferred. Each function receives the current graph context and +//! returns a [`ShapeRef`], so it can delegate to another type or build a custom shape directly. //! //! Rust doc comments on derived containers, variants, and fields are preserved in their //! `description` fields for documentation and diagnostic consumers. @@ -218,10 +218,10 @@ pub mod __private { /// metadata that Serde uses for deserialization. The generated implementation records the /// deserialization-side names, shape graph, and Serde field/container metadata. /// -/// Use `#[serde_shape(deserialize_with = "path")]` on a container or field to override an -/// opaque or foreign representation. The function must accept `&mut DeserializeShapeContext` -/// and return a [`ShapeRef`]. Generic hooks can replace inferred bounds with -/// `#[serde_shape(bound(deserialize = "T: DeserializeShape"))]` on the container. +/// Use `#[serde_shape(deserialize_with = "path")]` on a container, variant, or field to +/// override an opaque or foreign representation. The function must accept `&mut +/// DeserializeShapeContext` and return a [`ShapeRef`]. Generic hooks can replace inferred +/// bounds with `#[serde_shape(bound(deserialize = "T: DeserializeShape"))]` on the container. /// /// # Example /// @@ -258,9 +258,9 @@ pub use serde_shape_derive::DeserializeShape; /// metadata that Serde uses for serialization. The generated implementation records the /// serialization-side names, shape graph, and Serde field/container metadata. /// -/// Use `#[serde_shape(serialize_with = "path")]` on a container or field to override an opaque -/// or foreign representation. The function must accept `&mut SerializeShapeContext` and return -/// a [`ShapeRef`]. Generic hooks can replace inferred bounds with +/// Use `#[serde_shape(serialize_with = "path")]` on a container, variant, or field to override +/// an opaque or foreign representation. The function must accept `&mut SerializeShapeContext` +/// and return a [`ShapeRef`]. Generic hooks can replace inferred bounds with /// `#[serde_shape(bound(serialize = "T: SerializeShape"))]` on the container. /// /// # Example @@ -980,6 +980,8 @@ pub enum SerializeVariantContent { Omitted, /// Serde derives the variant content from these fields. Fields(Vec), + /// A `serde_shape` hook supplies the variant content shape. + Shape(ShapeRef), /// A custom serializer controls the variant content. Custom(OpaqueShape), } @@ -1013,6 +1015,8 @@ pub enum DeserializeVariantContent { Omitted, /// Serde derives the variant content from these fields. Fields(Vec), + /// A `serde_shape` hook supplies the variant content shape. + Shape(ShapeRef), /// A custom deserializer controls the variant content. Custom(OpaqueShape), } diff --git a/tests/derive/tests/serde_compat.rs b/tests/derive/tests/serde_compat.rs index ec7d79a..0ed26ff 100644 --- a/tests/derive/tests/serde_compat.rs +++ b/tests/derive/tests/serde_compat.rs @@ -60,6 +60,31 @@ enum CustomVariant { Value(u64), } +#[derive(Debug, PartialEq, Serialize, Deserialize, SerializeShape, DeserializeShape)] +enum DeclaredCustomVariant { + #[serde(with = "flat_value")] + #[serde_shape( + serialize_with = "serialize_flat_value_shape", + deserialize_with = "deserialize_flat_value_shape" + )] + Value(FlatValue), +} + +fn serialize_flat_value_shape(_context: &mut renamed_shape::SerializeShapeContext) -> ShapeRef { + flat_value_shape() +} + +fn deserialize_flat_value_shape(_context: &mut renamed_shape::DeserializeShapeContext) -> ShapeRef { + flat_value_shape() +} + +fn flat_value_shape() -> ShapeRef { + ShapeRef::Map { + key: Box::new(ShapeRef::String), + value: Box::new(ShapeRef::U64), + } +} + #[test] fn composes_flatten_with_custom_field_boundaries() { let value = FlattenedCustom { @@ -157,6 +182,29 @@ fn retains_custom_variant_boundary_details() { assert_eq!(opaque.detail, Some("stringified::deserialize")); } +#[test] +fn declares_known_custom_variant_content() { + let value = DeclaredCustomVariant::Value(FlatValue(19)); + let json = serde_json::to_value(&value).expect("value should serialize"); + assert_eq!(json, serde_json::json!({ "Value": { "custom": 19 } })); + assert_eq!( + serde_json::from_value::(json).expect("value should deserialize"), + value + ); + + let expected = flat_value_shape(); + let serialize_variant = first_serialize_variant::(); + assert_eq!( + serialize_variant.content, + SerializeVariantContent::Shape(expected.clone()) + ); + let deserialize_variant = first_deserialize_variant::(); + assert_eq!( + deserialize_variant.content, + DeserializeVariantContent::Shape(expected) + ); +} + fn first_serialize_field() -> SerializeFieldShape where T: SerializeShape, From 486369c14ee04e5f562a99f04e3858571e501882 Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:49:55 +0800 Subject: [PATCH 04/14] fix: preserve owned boxed slice input shapes Keep Box transparent for sized inputs, but reflect Box<[T]> through Serde's owned sequence path. Add explicit unsized implementations for Box and Box so their existing behavior remains available. Why: Serde deserializes borrowed &[u8] through the bytes data-model call but deserializes Box<[u8]> through a sequence; forwarding every Box to its inner unsized type incorrectly collapsed those distinct contracts. --- CHANGELOG.md | 1 + serde-shape/src/impls/wrapper.rs | 24 +++++++++++++++++++++++- serde-shape/src/tests.rs | 8 ++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2a90aa..4c46b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file. ### Bug fixes +* 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. * Follow Serde's directional bounds for `Cow`: serialization reflects the borrowed type and deserialization reflects the owned type. diff --git a/serde-shape/src/impls/wrapper.rs b/serde-shape/src/impls/wrapper.rs index e7bcda8..df5ea85 100644 --- a/serde-shape/src/impls/wrapper.rs +++ b/serde-shape/src/impls/wrapper.rs @@ -75,7 +75,7 @@ transparent_shape! { (T) Box where serialize { T: SerializeShape + ?Sized } - deserialize { T: DeserializeShape + ?Sized } + deserialize { T: DeserializeShape } => T; (T) Cell @@ -103,6 +103,28 @@ transparent_shape! { => T; } +impl DeserializeShape for Box<[T]> +where + T: DeserializeShape, +{ + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::Seq(Box::new(T::deserialize_shape_in(context))) + } +} + +impl DeserializeShape for Box { + fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::String + } +} + +#[cfg(feature = "std")] +impl DeserializeShape for Box { + fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::String + } +} + #[cfg(feature = "std")] transparent_shape! { (T) std::sync::Mutex diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index b5e528f..b6c4dc6 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -227,6 +227,10 @@ fn distinguishes_byte_sequences_from_borrowed_byte_input() { <[u8] as DeserializeShape>::deserialize_shape().root(), &ShapeRef::Bytes ); + assert_eq!( + as DeserializeShape>::deserialize_shape().root(), + &ShapeRef::Seq(Box::new(ShapeRef::U8)) + ); } #[test] @@ -388,6 +392,10 @@ fn maps_common_std_shapes() { SerializeShapeGraph::for_type::().root(), &ShapeRef::String ); + assert_eq!( + as DeserializeShape>::deserialize_shape().root(), + &ShapeRef::String + ); assert_eq!( as SerializeShape>::serialize_shape().root(), &ShapeRef::String From 75fd66ecfdf6cd699979c8777c88326bdcf5d376 Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:50:35 +0800 Subject: [PATCH 05/14] feat: reflect Serde's C string representations Add no_std shape implementations for CStr serialization, CString in both directions, and Box deserialization. These types use Serde's byte-buffer data-model call rather than a sequence. Why: C strings are native Serde-supported alloc types, and downstream crates cannot add the missing Shape implementations themselves because both the trait and types are foreign. --- CHANGELOG.md | 1 + README.md | 1 + serde-shape/src/impls/ffi.rs | 47 ++++++++++++++++++++++++++++++++++++ serde-shape/src/impls/mod.rs | 1 + serde-shape/src/lib.rs | 2 +- serde-shape/src/tests.rs | 14 +++++++++++ 6 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 serde-shape/src/impls/ffi.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c46b59..e9de981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. * 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. * Add directional `#[serde_shape(bound(...))]` overrides for generic custom shape hooks. * Add `SerializeShapeGraph::root_definition` and `DeserializeShapeGraph::root_definition` for directly inspecting named root types. * Preserve Rust doc comments on derived containers, variants, and fields as user-facing descriptions. diff --git a/README.md b/README.md index 4b1cc4e..475fdab 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ The built-in implementations follow Serde's own data-model calls in each directi | Scalars | Rust primitives, `String`, `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 | References, `Box`, `Cow`, `Cell`, `RefCell`, `Wrapping`, `Reverse`, and `PhantomData` | +| FFI | `CStr` and `CString` byte representations, including owned `Box` input | | Time | `core::time::Duration` | | Network | `core::net` IP and socket address types | | `std` feature | `HashMap`, `HashSet`, `Path`, `PathBuf`, `Mutex`, and `RwLock` | diff --git a/serde-shape/src/impls/ffi.rs b/serde-shape/src/impls/ffi.rs new file mode 100644 index 0000000..5eef066 --- /dev/null +++ b/serde-shape/src/impls/ffi.rs @@ -0,0 +1,47 @@ +// 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::ffi::CString; +use core::ffi::CStr; + +use crate::DeserializeShape; +use crate::DeserializeShapeContext; +use crate::SerializeShape; +use crate::SerializeShapeContext; +use crate::ShapeRef; + +impl SerializeShape for CStr { + fn serialize_shape_in(_context: &mut SerializeShapeContext) -> ShapeRef { + ShapeRef::Bytes + } +} + +impl SerializeShape for CString { + fn serialize_shape_in(_context: &mut SerializeShapeContext) -> ShapeRef { + ShapeRef::Bytes + } +} + +impl DeserializeShape for CString { + fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::Bytes + } +} + +impl DeserializeShape for Box { + fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::Bytes + } +} diff --git a/serde-shape/src/impls/mod.rs b/serde-shape/src/impls/mod.rs index 3ef13e9..036c64f 100644 --- a/serde-shape/src/impls/mod.rs +++ b/serde-shape/src/impls/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. mod container; +mod ffi; mod net; mod primitive; mod result; diff --git a/serde-shape/src/lib.rs b/serde-shape/src/lib.rs index aa4c438..73a265d 100644 --- a/serde-shape/src/lib.rs +++ b/serde-shape/src/lib.rs @@ -598,7 +598,7 @@ pub enum ShapeRef { F64, /// UTF-8 string shape. String, - /// Byte buffer shape. + /// Serde byte-buffer data-model shape. Bytes, /// Optional value shape. Option(Box), diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index b6c4dc6..50a075c 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -19,6 +19,7 @@ use alloc::collections::BTreeMap; use alloc::collections::BinaryHeap; use alloc::collections::LinkedList; use alloc::collections::VecDeque; +use alloc::ffi::CString; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; @@ -26,6 +27,7 @@ use core::borrow::Borrow; use core::cell::Cell; use core::cell::RefCell; use core::cmp::Reverse; +use core::ffi::CStr; use core::num::Wrapping; use crate::DeserializeDefinitionKind; @@ -351,6 +353,18 @@ fn maps_common_core_and_alloc_shapes() { DeserializeShapeGraph::for_type::>().root(), &ShapeRef::Seq(Box::new(ShapeRef::U16)) ); + assert_eq!( + SerializeShapeGraph::for_type::().root(), + &ShapeRef::Bytes + ); + assert_eq!( + DeserializeShapeGraph::for_type::().root(), + &ShapeRef::Bytes + ); + assert_eq!( + as DeserializeShape>::deserialize_shape().root(), + &ShapeRef::Bytes + ); } #[test] From 09e02f55a84f015cd42c0496a728737f6fa8da01 Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:51:12 +0800 Subject: [PATCH 06/14] feat: reflect Serde's shared pointer shapes Add Rc and Arc shape implementations that serialize through their contents and deserialize through the same owned Box representation Serde uses. Reflect weak pointers as optional contents and retain target atomic gating for Arc. Why: shared pointers are common Serde-supported alloc wrappers, but orphan rules left users unable to reflect them; delegating deserialization through Box also preserves the owned-slice distinction fixed separately. --- CHANGELOG.md | 1 + README.md | 4 ++- serde-shape/src/impls/wrapper.rs | 52 ++++++++++++++++++++++++++++++++ serde-shape/src/tests.rs | 25 +++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9de981..fd68c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. * 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. +* Add `Rc`, `Arc`, and weak-pointer shapes, preserving Serde's owned input and optional weak-pointer representations. * Add directional `#[serde_shape(bound(...))]` overrides for generic custom shape hooks. * Add `SerializeShapeGraph::root_definition` and `DeserializeShapeGraph::root_definition` for directly inspecting named root types. * Preserve Rust doc comments on derived containers, variants, and fields as user-facing descriptions. diff --git a/README.md b/README.md index 475fdab..d382e82 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ The built-in implementations follow Serde's own data-model calls in each directi | --- | --- | | Scalars | Rust primitives, `String`, `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 | References, `Box`, `Cow`, `Cell`, `RefCell`, `Wrapping`, `Reverse`, and `PhantomData` | +| Wrappers | References, `Box`, `Rc`, `Arc`, their weak pointers, `Cow`, `Cell`, `RefCell`, `Wrapping`, `Reverse`, and `PhantomData` | | FFI | `CStr` and `CString` byte representations, including owned `Box` input | | Time | `core::time::Duration` | | Network | `core::net` IP and socket address types | @@ -166,6 +166,8 @@ The built-in implementations follow Serde's own data-model calls in each directi Network address shapes are unions of their human-readable string representation and their compact Serde representation. A serialized byte slice is a sequence, while borrowed byte deserialization uses `ShapeRef::Bytes`. +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. + 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/wrapper.rs b/serde-shape/src/impls/wrapper.rs index df5ea85..8965a66 100644 --- a/serde-shape/src/impls/wrapper.rs +++ b/serde-shape/src/impls/wrapper.rs @@ -15,6 +15,12 @@ use alloc::borrow::Cow; use alloc::borrow::ToOwned; use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::rc::Weak as RcWeak; +#[cfg(target_has_atomic = "ptr")] +use alloc::sync::Arc; +#[cfg(target_has_atomic = "ptr")] +use alloc::sync::Weak as ArcWeak; use core::cell::Cell; use core::cell::RefCell; use core::cmp::Reverse; @@ -118,6 +124,52 @@ impl DeserializeShape for Box { } } +macro_rules! shared_pointer_shape { + ($strong:ident, $weak:ident) => { + impl SerializeShape for $strong + where + T: SerializeShape + ?Sized, + { + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + T::serialize_shape_in(context) + } + } + + impl DeserializeShape for $strong + where + T: ?Sized, + Box: DeserializeShape, + { + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + as DeserializeShape>::deserialize_shape_in(context) + } + } + + impl SerializeShape for $weak + where + T: SerializeShape + ?Sized, + { + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + ShapeRef::Option(Box::new(T::serialize_shape_in(context))) + } + } + + impl DeserializeShape for $weak + where + T: DeserializeShape, + { + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::Option(Box::new(T::deserialize_shape_in(context))) + } + } + }; +} + +shared_pointer_shape!(Rc, RcWeak); + +#[cfg(target_has_atomic = "ptr")] +shared_pointer_shape!(Arc, ArcWeak); + #[cfg(feature = "std")] impl DeserializeShape for Box { fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index 50a075c..91263ac 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -20,7 +20,11 @@ use alloc::collections::BinaryHeap; use alloc::collections::LinkedList; use alloc::collections::VecDeque; use alloc::ffi::CString; +use alloc::rc::Rc; +use alloc::rc::Weak as RcWeak; use alloc::string::String; +#[cfg(target_has_atomic = "ptr")] +use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; use core::borrow::Borrow; @@ -367,6 +371,27 @@ fn maps_common_core_and_alloc_shapes() { ); } +#[test] +fn follows_shared_pointer_serde_shapes() { + assert_eq!( + as SerializeShape>::serialize_shape().root(), + &ShapeRef::String + ); + assert_eq!( + as DeserializeShape>::deserialize_shape().root(), + &ShapeRef::Seq(Box::new(ShapeRef::U8)) + ); + assert_eq!( + SerializeShapeGraph::for_type::>().root(), + &ShapeRef::Option(Box::new(ShapeRef::U16)) + ); + #[cfg(target_has_atomic = "ptr")] + assert_eq!( + as DeserializeShape>::deserialize_shape().root(), + &ShapeRef::Seq(Box::new(ShapeRef::U8)) + ); +} + #[test] fn accepts_serde_serializable_collection_and_wrapper_bounds() { assert_eq!( From cdb682a78b05af89e6c7d81a545fdac1962450ab Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:51:35 +0800 Subject: [PATCH 07/14] feat: mirror Saturating's directional Serde support Reflect Saturating transparently for generic serialization while providing deserialization shapes only for the integer primitives implemented by Serde. Why: a blanket bidirectional implementation would overstate Serde's API, while omitting the wrapper entirely prevents valid numeric configuration fields from deriving Shape and cannot be repaired downstream. --- CHANGELOG.md | 1 + README.md | 2 +- serde-shape/src/impls/wrapper.rs | 26 ++++++++++++++++++++++++++ serde-shape/src/tests.rs | 9 +++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd68c87..7c44cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file. * 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. * Add `Rc`, `Arc`, and weak-pointer shapes, preserving Serde's owned input and optional weak-pointer representations. +* Reflect `Saturating` with Serde's generic serialization and primitive-only deserialization support. * Add directional `#[serde_shape(bound(...))]` overrides for generic custom shape hooks. * Add `SerializeShapeGraph::root_definition` and `DeserializeShapeGraph::root_definition` for directly inspecting named root types. * Preserve Rust doc comments on derived containers, variants, and fields as user-facing descriptions. diff --git a/README.md b/README.md index d382e82..8b822f8 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ The built-in implementations follow Serde's own data-model calls in each directi | --- | --- | | Scalars | Rust primitives, `String`, `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 | References, `Box`, `Rc`, `Arc`, their weak pointers, `Cow`, `Cell`, `RefCell`, `Wrapping`, `Reverse`, and `PhantomData` | +| Wrappers | References, `Box`, `Rc`, `Arc`, their weak pointers, `Cow`, `Cell`, `RefCell`, `Wrapping`, `Saturating`, `Reverse`, and `PhantomData` | | FFI | `CStr` and `CString` byte representations, including owned `Box` input | | Time | `core::time::Duration` | | Network | `core::net` IP and socket address types | diff --git a/serde-shape/src/impls/wrapper.rs b/serde-shape/src/impls/wrapper.rs index 8965a66..d56672e 100644 --- a/serde-shape/src/impls/wrapper.rs +++ b/serde-shape/src/impls/wrapper.rs @@ -25,6 +25,7 @@ use core::cell::Cell; use core::cell::RefCell; use core::cmp::Reverse; use core::marker::PhantomData; +use core::num::Saturating; use core::num::Wrapping; use crate::DeserializeShape; @@ -109,6 +110,31 @@ transparent_shape! { => T; } +impl SerializeShape for Saturating +where + T: SerializeShape, +{ + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + T::serialize_shape_in(context) + } +} + +macro_rules! saturating_deserialize_shape { + ($($ty:ty),+ $(,)?) => { + $( + impl DeserializeShape for Saturating<$ty> { + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + <$ty as DeserializeShape>::deserialize_shape_in(context) + } + } + )+ + }; +} + +saturating_deserialize_shape!( + i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, +); + impl DeserializeShape for Box<[T]> where T: DeserializeShape, diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index 91263ac..01902d3 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -32,6 +32,7 @@ use core::cell::Cell; use core::cell::RefCell; use core::cmp::Reverse; use core::ffi::CStr; +use core::num::Saturating; use core::num::Wrapping; use crate::DeserializeDefinitionKind; @@ -341,6 +342,14 @@ fn maps_common_core_and_alloc_shapes() { DeserializeShapeGraph::for_type::>().root(), &ShapeRef::I16 ); + assert_eq!( + SerializeShapeGraph::for_type::>().root(), + &ShapeRef::String + ); + assert_eq!( + DeserializeShapeGraph::for_type::>().root(), + &ShapeRef::U16 + ); assert_eq!( SerializeShapeGraph::for_type::>().root(), &ShapeRef::U32 From 7f4a6717b137dd0cfee77e7c89600dcab35845ae Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:53:06 +0800 Subject: [PATCH 08/14] feat: add Serde's range struct shapes Reflect Range, RangeFrom, RangeInclusive, and RangeTo as their named Serde structs with the appropriate start and end fields. Deserialization records Serde's unknown-field rejection. Why: ranges are native Serde-supported core types, but missing foreign-type implementations forced every downstream range field to use a custom hook; a local macro keeps the four contracts consistent without copied implementations. --- CHANGELOG.md | 1 + README.md | 1 + serde-shape/src/impls/mod.rs | 1 + serde-shape/src/impls/range.rs | 119 +++++++++++++++++++++++++++++++++ serde-shape/src/tests.rs | 48 +++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 serde-shape/src/impls/range.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c44cc5..75c056b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file. * Reflect Serde's byte-buffer representation for `CStr`, `CString`, and owned `Box` input. * Add `Rc`, `Arc`, and weak-pointer shapes, preserving Serde's owned input and optional weak-pointer representations. * Reflect `Saturating` with Serde's generic serialization and primitive-only deserialization support. +* Add named struct shapes for Serde's `Range`, `RangeFrom`, `RangeInclusive`, and `RangeTo` representations. * Add directional `#[serde_shape(bound(...))]` overrides for generic custom shape hooks. * Add `SerializeShapeGraph::root_definition` and `DeserializeShapeGraph::root_definition` for directly inspecting named root types. * Preserve Rust doc comments on derived containers, variants, and fields as user-facing descriptions. diff --git a/README.md b/README.md index 8b822f8..19b2f24 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ The built-in implementations follow Serde's own data-model calls in each directi | Containers | `Option`, `Result`, arrays, slices for serialization, tuples through arity 16, `Vec`, `VecDeque`, `LinkedList`, `BinaryHeap`, `BTreeSet`, and `BTreeMap` | | Wrappers | References, `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`, and `RangeTo` | | Time | `core::time::Duration` | | Network | `core::net` IP and socket address types | | `std` feature | `HashMap`, `HashSet`, `Path`, `PathBuf`, `Mutex`, and `RwLock` | diff --git a/serde-shape/src/impls/mod.rs b/serde-shape/src/impls/mod.rs index 036c64f..d5dcfe5 100644 --- a/serde-shape/src/impls/mod.rs +++ b/serde-shape/src/impls/mod.rs @@ -16,6 +16,7 @@ mod container; mod ffi; mod net; mod primitive; +mod range; mod result; mod time; mod tuple; diff --git a/serde-shape/src/impls/range.rs b/serde-shape/src/impls/range.rs new file mode 100644 index 0000000..93ed9f2 --- /dev/null +++ b/serde-shape/src/impls/range.rs @@ -0,0 +1,119 @@ +// 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::vec; +use core::any::type_name; +use core::ops::Range; +use core::ops::RangeFrom; +use core::ops::RangeInclusive; +use core::ops::RangeTo; + +use crate::DefaultShape; +use crate::DeserializeContainerAttributes; +use crate::DeserializeDefinitionKind; +use crate::DeserializeFieldShape; +use crate::DeserializeShape; +use crate::DeserializeShapeContext; +use crate::DeserializeStructShape; +use crate::DeserializeTypeName; +use crate::FieldMember; +use crate::FieldWireShape; +use crate::FieldsStyle; +use crate::SerializeContainerAttributes; +use crate::SerializeDefinitionKind; +use crate::SerializeFieldShape; +use crate::SerializeShape; +use crate::SerializeShapeContext; +use crate::SerializeStructShape; +use crate::SerializeTypeName; +use crate::ShapeRef; + +macro_rules! range_shape { + ($($range:ident { $($field:ident),+ $(,)? })+) => { + $( + impl SerializeShape for $range + where + T: SerializeShape, + { + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + context.define_named_type( + SerializeTypeName { + rust_name: type_name::(), + name: stringify!($range), + }, + |context| { + SerializeDefinitionKind::Struct(SerializeStructShape { + style: FieldsStyle::Struct, + fields: vec![ + $(SerializeFieldShape { + member: FieldMember::Named(stringify!($field)), + name: stringify!($field), + description: None, + wire_shape: FieldWireShape::Value( + T::serialize_shape_in(context), + ), + skip_if: None, + }),+ + ], + attributes: SerializeContainerAttributes::default(), + }) + }, + ) + } + } + + impl DeserializeShape for $range + where + T: DeserializeShape, + { + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + context.define_named_type( + DeserializeTypeName { + rust_name: type_name::(), + name: stringify!($range), + }, + |context| { + DeserializeDefinitionKind::Struct(DeserializeStructShape { + style: FieldsStyle::Struct, + fields: vec![ + $(DeserializeFieldShape { + member: FieldMember::Named(stringify!($field)), + name: stringify!($field), + aliases: vec![stringify!($field)], + description: None, + wire_shape: FieldWireShape::Value( + T::deserialize_shape_in(context), + ), + default: DefaultShape::None, + }),+ + ], + attributes: DeserializeContainerAttributes { + deny_unknown_fields: true, + ..DeserializeContainerAttributes::default() + }, + }) + }, + ) + } + } + )+ + }; +} + +range_shape! { + Range { start, end } + RangeFrom { start } + RangeInclusive { start, end } + RangeTo { end } +} diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index 01902d3..4ea7ba8 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -34,6 +34,10 @@ use core::cmp::Reverse; use core::ffi::CStr; use core::num::Saturating; use core::num::Wrapping; +use core::ops::Range; +use core::ops::RangeFrom; +use core::ops::RangeInclusive; +use core::ops::RangeTo; use crate::DeserializeDefinitionKind; use crate::DeserializeShape; @@ -300,6 +304,50 @@ fn maps_duration_as_serde_struct_fields() { ); } +#[test] +fn maps_range_struct_shapes() { + assert_range_shapes::>("Range", &["start", "end"]); + assert_range_shapes::>("RangeFrom", &["start"]); + assert_range_shapes::>("RangeInclusive", &["start", "end"]); + assert_range_shapes::>("RangeTo", &["end"]); +} + +fn assert_range_shapes(type_name: &str, field_names: &[&str]) +where + T: SerializeShape + DeserializeShape, +{ + let serialize = T::serialize_shape(); + let serialize_definition = serialize.root_definition().unwrap(); + assert_eq!(serialize_definition.type_name.name, type_name); + let SerializeDefinitionKind::Struct(shape) = &serialize_definition.kind else { + panic!("range serialization shape should be a struct"); + }; + assert_eq!( + shape + .fields + .iter() + .map(|field| field.name) + .collect::>(), + field_names + ); + + let deserialize = T::deserialize_shape(); + let deserialize_definition = deserialize.root_definition().unwrap(); + assert_eq!(deserialize_definition.type_name.name, type_name); + let DeserializeDefinitionKind::Struct(shape) = &deserialize_definition.kind else { + panic!("range deserialization shape should be a struct"); + }; + assert!(shape.attributes.deny_unknown_fields); + assert_eq!( + shape + .fields + .iter() + .map(|field| field.name) + .collect::>(), + field_names + ); +} + #[test] fn supports_serde_tuple_arity() { type Tuple16 = ( From b9287a44e728cd5e3d9ef4ea5d81f9468e349160 Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:53:51 +0800 Subject: [PATCH 09/14] feat: add Serde's Bound enum shape Reflect Bound as the externally tagged Unbounded unit variant plus Included and Excluded newtype variants in both directions. Why: Bound is part of Serde's native core-type surface and cannot receive a downstream Shape implementation; modeling its tags and variant styles is necessary for consumers that document or traverse the wire contract. --- CHANGELOG.md | 1 + README.md | 2 +- serde-shape/src/impls/bound.rs | 157 +++++++++++++++++++++++++++++++++ serde-shape/src/impls/mod.rs | 1 + serde-shape/src/tests.rs | 25 ++++++ 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 serde-shape/src/impls/bound.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 75c056b..7c4c81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. * Add `Rc`, `Arc`, and weak-pointer shapes, preserving Serde's owned input and optional weak-pointer representations. * Reflect `Saturating` with Serde's generic serialization and primitive-only deserialization support. * Add named struct shapes for Serde's `Range`, `RangeFrom`, `RangeInclusive`, and `RangeTo` representations. +* Reflect `Bound` as Serde's externally tagged `Unbounded`, `Included`, and `Excluded` enum. * Add directional `#[serde_shape(bound(...))]` overrides for generic custom shape hooks. * Add `SerializeShapeGraph::root_definition` and `DeserializeShapeGraph::root_definition` for directly inspecting named root types. * Preserve Rust doc comments on derived containers, variants, and fields as user-facing descriptions. diff --git a/README.md b/README.md index 19b2f24..31d1cae 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ The built-in implementations follow Serde's own data-model calls in each directi | Containers | `Option`, `Result`, arrays, slices for serialization, tuples through arity 16, `Vec`, `VecDeque`, `LinkedList`, `BinaryHeap`, `BTreeSet`, and `BTreeMap` | | Wrappers | References, `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`, and `RangeTo` | +| Ranges | `Range`, `RangeFrom`, `RangeInclusive`, `RangeTo`, and `Bound` | | Time | `core::time::Duration` | | Network | `core::net` IP and socket address types | | `std` feature | `HashMap`, `HashSet`, `Path`, `PathBuf`, `Mutex`, and `RwLock` | diff --git a/serde-shape/src/impls/bound.rs b/serde-shape/src/impls/bound.rs new file mode 100644 index 0000000..af56391 --- /dev/null +++ b/serde-shape/src/impls/bound.rs @@ -0,0 +1,157 @@ +// 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::vec; +use core::any::type_name; +use core::ops::Bound; + +use crate::DefaultShape; +use crate::DeserializeContainerAttributes; +use crate::DeserializeDefinitionKind; +use crate::DeserializeEnumShape; +use crate::DeserializeFieldShape; +use crate::DeserializeShape; +use crate::DeserializeShapeContext; +use crate::DeserializeTypeName; +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::SerializeTypeName; +use crate::SerializeVariantContent; +use crate::SerializeVariantShape; +use crate::ShapeRef; +use crate::Tagging; + +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(), + }) + }, + ) + } +} + +impl DeserializeShape for Bound +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(), + }) + }, + ) + } +} + +fn serialize_bound_variant( + name: &'static str, + value_shape: Option, +) -> SerializeVariantShape { + let (style, fields) = match value_shape { + Some(value_shape) => ( + FieldsStyle::Newtype, + vec![SerializeFieldShape { + member: FieldMember::Unnamed(0), + name: "0", + description: None, + wire_shape: FieldWireShape::Value(value_shape), + skip_if: None, + }], + ), + None => (FieldsStyle::Unit, vec![]), + }; + + SerializeVariantShape { + rust_name: name, + name, + description: None, + style, + content: SerializeVariantContent::Fields(fields), + untagged: false, + } +} + +fn deserialize_bound_variant( + name: &'static str, + value_shape: Option, +) -> DeserializeVariantShape { + let (style, fields) = match value_shape { + Some(value_shape) => ( + FieldsStyle::Newtype, + vec![DeserializeFieldShape { + member: FieldMember::Unnamed(0), + name: "0", + aliases: vec!["0"], + description: None, + wire_shape: FieldWireShape::Value(value_shape), + default: DefaultShape::None, + }], + ), + None => (FieldsStyle::Unit, vec![]), + }; + + DeserializeVariantShape { + rust_name: name, + name, + aliases: vec![name], + description: None, + style, + content: DeserializeVariantContent::Fields(fields), + other: false, + untagged: false, + } +} diff --git a/serde-shape/src/impls/mod.rs b/serde-shape/src/impls/mod.rs index d5dcfe5..45ac420 100644 --- a/serde-shape/src/impls/mod.rs +++ b/serde-shape/src/impls/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod bound; mod container; mod ffi; mod net; diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index 4ea7ba8..69db569 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -34,6 +34,7 @@ use core::cmp::Reverse; use core::ffi::CStr; use core::num::Saturating; use core::num::Wrapping; +use core::ops::Bound; use core::ops::Range; use core::ops::RangeFrom; use core::ops::RangeInclusive; @@ -312,6 +313,30 @@ fn maps_range_struct_shapes() { assert_range_shapes::>("RangeTo", &["end"]); } +#[test] +fn maps_bound_as_an_externally_tagged_enum() { + let serialize = SerializeShapeGraph::for_type::>(); + let SerializeDefinitionKind::Enum(shape) = &serialize.root_definition().unwrap().kind else { + panic!("bound serialization shape should be an enum"); + }; + assert_eq!(shape.repr, Tagging::External); + assert_eq!(shape.variants[0].name, "Unbounded"); + assert_eq!(shape.variants[0].style, FieldsStyle::Unit); + assert_eq!(shape.variants[1].name, "Included"); + assert_eq!(shape.variants[1].style, FieldsStyle::Newtype); + + let deserialize = DeserializeShapeGraph::for_type::>(); + let DeserializeDefinitionKind::Enum(shape) = &deserialize.root_definition().unwrap().kind + else { + panic!("bound deserialization shape should be an enum"); + }; + assert_eq!(shape.variants[2].name, "Excluded"); + let crate::DeserializeVariantContent::Fields(fields) = &shape.variants[2].content else { + panic!("Excluded should contain one reflected field"); + }; + assert_eq!(fields[0].wire_shape, FieldWireShape::Value(ShapeRef::U8)); +} + fn assert_range_shapes(type_name: &str, field_names: &[&str]) where T: SerializeShape + DeserializeShape, From aaa88f349c4446d8296f10f83bf6861a58d99fb6 Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:55:10 +0800 Subject: [PATCH 10/14] feat: add Serde's SystemTime struct shape Reflect SystemTime behind the std feature as the secs_since_epoch and nanos_since_epoch struct accepted and emitted by Serde. Share the fixed-field struct construction with Duration instead of duplicating two full manual graph definitions. Why: SystemTime is a common native Serde type that downstream crates cannot implement Shape for, and configuration or wire types containing it should derive without a field-level escape hatch. --- CHANGELOG.md | 1 + README.md | 2 +- serde-shape/src/impls/time.rs | 165 ++++++++++++++++++---------------- serde-shape/src/tests.rs | 9 ++ 4 files changed, 99 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4c81a..2fbbb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to this project will be documented in this file. * Reflect `Saturating` with Serde's generic serialization and primitive-only deserialization support. * Add named struct shapes for Serde's `Range`, `RangeFrom`, `RangeInclusive`, and `RangeTo` representations. * Reflect `Bound` as Serde's externally tagged `Unbounded`, `Included`, and `Excluded` enum. +* Add the `std`-only `SystemTime` struct shape with Serde's epoch field names. * Add directional `#[serde_shape(bound(...))]` overrides for generic custom shape hooks. * Add `SerializeShapeGraph::root_definition` and `DeserializeShapeGraph::root_definition` for directly inspecting named root types. * Preserve Rust doc comments on derived containers, variants, and fields as user-facing descriptions. diff --git a/README.md b/README.md index 31d1cae..e093c10 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ The built-in implementations follow Serde's own data-model calls in each directi | Wrappers | References, `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` | +| 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` | diff --git a/serde-shape/src/impls/time.rs b/serde-shape/src/impls/time.rs index aafc639..c05cf44 100644 --- a/serde-shape/src/impls/time.rs +++ b/serde-shape/src/impls/time.rs @@ -13,8 +13,11 @@ // limitations under the License. use alloc::vec; +use alloc::vec::Vec; use core::any::type_name; use core::time::Duration; +#[cfg(feature = "std")] +use std::time::SystemTime; use crate::DefaultShape; use crate::DeserializeContainerAttributes; @@ -35,85 +38,93 @@ use crate::SerializeShapeContext; use crate::SerializeStructShape; use crate::SerializeTypeName; use crate::ShapeRef; -use crate::Tagging; -impl SerializeShape for Duration { - fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { - context.define_named_type( - SerializeTypeName { - rust_name: type_name::(), - name: "Duration", - }, - |_| { - SerializeDefinitionKind::Struct(SerializeStructShape { - style: FieldsStyle::Struct, - fields: vec![ - SerializeFieldShape { - member: FieldMember::Named("secs"), - name: "secs", - description: None, - wire_shape: FieldWireShape::Value(ShapeRef::U64), - skip_if: None, - }, - SerializeFieldShape { - member: FieldMember::Named("nanos"), - name: "nanos", - description: None, - wire_shape: FieldWireShape::Value(ShapeRef::U32), - skip_if: None, - }, - ], - attributes: SerializeContainerAttributes { - tagging: Tagging::External, - has_flatten: false, - transparent: false, - non_exhaustive: false, - }, - }) - }, - ) - } +macro_rules! time_shape { + ($ty:ty, $name:literal, $($field:literal => $shape:expr),+ $(,)?) => { + impl SerializeShape for $ty { + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + serialize_time_shape( + context, + type_name::(), + $name, + [$(($field, $shape)),+], + ) + } + } + + impl DeserializeShape for $ty { + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + deserialize_time_shape( + context, + type_name::(), + $name, + [$(($field, $shape)),+], + ) + } + } + }; } -impl DeserializeShape for Duration { - fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { - context.define_named_type( - DeserializeTypeName { - rust_name: type_name::(), - name: "Duration", - }, - |_| { - DeserializeDefinitionKind::Struct(DeserializeStructShape { - style: FieldsStyle::Struct, - fields: vec![ - DeserializeFieldShape { - member: FieldMember::Named("secs"), - name: "secs", - aliases: vec!["secs"], - description: None, - wire_shape: FieldWireShape::Value(ShapeRef::U64), - default: DefaultShape::None, - }, - DeserializeFieldShape { - member: FieldMember::Named("nanos"), - name: "nanos", - aliases: vec!["nanos"], - description: None, - wire_shape: FieldWireShape::Value(ShapeRef::U32), - default: DefaultShape::None, - }, - ], - attributes: DeserializeContainerAttributes { - tagging: Tagging::External, - deny_unknown_fields: true, - default: DefaultShape::None, - has_flatten: false, - transparent: false, - expecting: None, - non_exhaustive: false, - }, - }) +time_shape!(Duration, "Duration", "secs" => ShapeRef::U64, "nanos" => ShapeRef::U32); + +#[cfg(feature = "std")] +time_shape!( + SystemTime, + "SystemTime", + "secs_since_epoch" => ShapeRef::U64, + "nanos_since_epoch" => ShapeRef::U32, +); + +fn serialize_time_shape( + context: &mut SerializeShapeContext, + rust_name: &'static str, + name: &'static str, + fields: [(&'static str, ShapeRef); N], +) -> ShapeRef { + let fields: Vec<_> = fields + .into_iter() + .map(|(name, shape)| SerializeFieldShape { + member: FieldMember::Named(name), + name, + description: None, + wire_shape: FieldWireShape::Value(shape), + skip_if: None, + }) + .collect(); + context.define_named_type(SerializeTypeName { rust_name, name }, move |_| { + SerializeDefinitionKind::Struct(SerializeStructShape { + style: FieldsStyle::Struct, + fields, + attributes: SerializeContainerAttributes::default(), + }) + }) +} + +fn deserialize_time_shape( + context: &mut DeserializeShapeContext, + rust_name: &'static str, + name: &'static str, + fields: [(&'static str, ShapeRef); N], +) -> ShapeRef { + let fields: Vec<_> = fields + .into_iter() + .map(|(name, shape)| DeserializeFieldShape { + member: FieldMember::Named(name), + name, + aliases: vec![name], + description: None, + wire_shape: FieldWireShape::Value(shape), + default: DefaultShape::None, + }) + .collect(); + context.define_named_type(DeserializeTypeName { rust_name, name }, move |_| { + DeserializeDefinitionKind::Struct(DeserializeStructShape { + style: FieldsStyle::Struct, + fields, + attributes: DeserializeContainerAttributes { + deny_unknown_fields: true, + ..DeserializeContainerAttributes::default() }, - ) - } + }) + }) } diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index 69db569..39aaef2 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -525,6 +525,15 @@ fn maps_common_std_shapes() { as SerializeShape>::serialize_shape().root(), &ShapeRef::String ); + + let system_time = DeserializeShapeGraph::for_type::(); + let DeserializeDefinitionKind::Struct(shape) = &system_time.root_definition().unwrap().kind + else { + panic!("system time shape should be a struct"); + }; + assert_eq!(shape.fields[0].name, "secs_since_epoch"); + assert_eq!(shape.fields[1].name, "nanos_since_epoch"); + assert!(shape.attributes.deny_unknown_fields); } #[test] From 661ab29c75d4f159a523d008fd8de8a09ed8436b Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:56:09 +0800 Subject: [PATCH 11/14] refactor: use canonical metadata defaults Build ordinary Result and network enum attributes through their Default implementations, removing repeated false and None literals without changing their reflected contracts. Why: keeping one definition of the ordinary Serde container baseline prevents built-in shapes from drifting as metadata evolves and makes manual implementations demonstrate the shorter public API. --- serde-shape/src/impls/net.rs | 17 ++--------------- serde-shape/src/impls/result.rs | 17 ++--------------- 2 files changed, 4 insertions(+), 30 deletions(-) diff --git a/serde-shape/src/impls/net.rs b/serde-shape/src/impls/net.rs index d2cb7f9..4c4415e 100644 --- a/serde-shape/src/impls/net.rs +++ b/serde-shape/src/impls/net.rs @@ -216,22 +216,9 @@ fn deserialize_newtype_variant(name: &'static str, shape: ShapeRef) -> Deseriali } fn serialize_enum_attributes() -> SerializeContainerAttributes { - SerializeContainerAttributes { - tagging: Tagging::External, - has_flatten: false, - transparent: false, - non_exhaustive: false, - } + SerializeContainerAttributes::default() } fn deserialize_enum_attributes() -> DeserializeContainerAttributes { - DeserializeContainerAttributes { - tagging: Tagging::External, - deny_unknown_fields: false, - default: DefaultShape::None, - has_flatten: false, - transparent: false, - expecting: None, - non_exhaustive: false, - } + DeserializeContainerAttributes::default() } diff --git a/serde-shape/src/impls/result.rs b/serde-shape/src/impls/result.rs index 8499934..b0b9768 100644 --- a/serde-shape/src/impls/result.rs +++ b/serde-shape/src/impls/result.rs @@ -58,12 +58,7 @@ where serialize_result_variant("Ok", T::serialize_shape_in(context)), serialize_result_variant("Err", E::serialize_shape_in(context)), ], - attributes: SerializeContainerAttributes { - tagging: Tagging::External, - has_flatten: false, - transparent: false, - non_exhaustive: false, - }, + attributes: SerializeContainerAttributes::default(), }) }, ) @@ -88,15 +83,7 @@ where deserialize_result_variant("Ok", T::deserialize_shape_in(context)), deserialize_result_variant("Err", E::deserialize_shape_in(context)), ], - attributes: DeserializeContainerAttributes { - tagging: Tagging::External, - deny_unknown_fields: false, - default: DefaultShape::None, - has_flatten: false, - transparent: false, - expecting: None, - non_exhaustive: false, - }, + attributes: DeserializeContainerAttributes::default(), }) }, ) From 64d429f95e34f888bc1ecd0a00f0a27c501083f4 Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:56:46 +0800 Subject: [PATCH 12/14] docs: define custom content and metadata defaults Document the exact Default values for public container metadata and clarify that a variant shape hook describes content inside, rather than replacing, the enum tagging representation. Why: both APIs are concise only if callers can tell what is implicit; leaving those semantics to source inspection makes manual shapes easy to initialize incorrectly. --- README.md | 2 +- serde-shape/src/lib.rs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e093c10..789b453 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ struct Config { Each function receives the current graph context and returns a `ShapeRef`. It may delegate to another type's shape implementation or construct a custom shape directly. A custom shape function is an assertion about the Serde behavior; `serde-shape` cannot verify that the declared shape matches the serializer or deserializer implementation. -The same `serde_shape` hooks can be placed on enum variants whose content is controlled by a variant-level Serde custom function. Without an explicit hook, custom variant content remains opaque. +The same `serde_shape` hooks can be placed on enum variants whose content is controlled by a variant-level Serde custom function. A variant hook describes the content inside the enum's tagging representation; the enum's `repr` still describes the tag. Without an explicit hook, custom variant content remains opaque. For a generic custom hook, container-level `#[serde_shape(bound(serialize = "...", deserialize = "..."))]` replaces the automatically inferred bounds in the corresponding direction, following Serde's bound-override convention. diff --git a/serde-shape/src/lib.rs b/serde-shape/src/lib.rs index 73a265d..11caa55 100644 --- a/serde-shape/src/lib.rs +++ b/serde-shape/src/lib.rs @@ -790,6 +790,9 @@ pub enum DeserializeDefinitionKind { } /// Serde attributes that apply to a whole serialized container. +/// +/// [`Default`] represents an ordinary externally tagged container with every optional behavior +/// disabled. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SerializeContainerAttributes { /// The container tagging representation. @@ -803,6 +806,9 @@ pub struct SerializeContainerAttributes { } /// Serde attributes that apply to a whole deserialized container. +/// +/// [`Default`] represents an ordinary externally tagged container with no default, expectation, or +/// optional behavior configured. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct DeserializeContainerAttributes { /// The container tagging representation. @@ -822,6 +828,8 @@ pub struct DeserializeContainerAttributes { } /// Serde container or enum tagging representation. +/// +/// [`Default`] is [`Tagging::External`]. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub enum Tagging { /// The default externally tagged representation. @@ -980,7 +988,7 @@ pub enum SerializeVariantContent { Omitted, /// Serde derives the variant content from these fields. Fields(Vec), - /// A `serde_shape` hook supplies the variant content shape. + /// A `serde_shape` hook supplies the content shape inside the enum's tagging representation. Shape(ShapeRef), /// A custom serializer controls the variant content. Custom(OpaqueShape), @@ -1015,13 +1023,15 @@ pub enum DeserializeVariantContent { Omitted, /// Serde derives the variant content from these fields. Fields(Vec), - /// A `serde_shape` hook supplies the variant content shape. + /// A `serde_shape` hook supplies the content shape inside the enum's tagging representation. Shape(ShapeRef), /// A custom deserializer controls the variant content. Custom(OpaqueShape), } /// A Serde default marker. +/// +/// [`Default`] is [`DefaultShape::None`]. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub enum DefaultShape { /// No default is configured. From d0502f3c1037cb424d701a824cfec656e09a03fc Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 29 Aug 2026 23:57:54 +0800 Subject: [PATCH 13/14] docs: call out owned slice compatibility Document that Box<[u8]> follows Serde's owned sequence path and record the narrowed Box blanket implementation in the breaking-change section. Why: correcting the borrowed-versus-owned shape changes trait availability for shape-only custom DSTs, and a release-quality changelog must make that migration cost explicit rather than hiding it behind a bug-fix label. --- CHANGELOG.md | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fbbb2a..2098b55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. * Make the `ShapeId` tuple field private. Use `ShapeId::index()` when the graph-local numeric index is needed. * Add `description` to definition, field, and variant metadata. Manual struct literals must initialize the new field. * Remove `OpaqueReason::{FromType, TryFromType, IntoType}` because Serde conversion attributes now use the conversion type's shape instead of an opaque boundary. +* Restrict the blanket `Box` deserialization shape to sized `T`. Serde-supported owned DSTs have explicit implementations; shape-only custom DSTs now need a local newtype. ### New features diff --git a/README.md b/README.md index 789b453..53798aa 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ The built-in implementations follow Serde's own data-model calls in each directi | Network | `core::net` IP and socket address types | | `std` feature | `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 is a sequence, while borrowed byte deserialization uses `ShapeRef::Bytes`. +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`. 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. From 817a616777acf8fcb8c4440764b61136ea90cd64 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 30 Aug 2026 00:02:52 +0800 Subject: [PATCH 14/14] ci: verify publishable crate archives Add cargo x package to assemble both crates together, enable every feature, and build the normalized archives through Cargo's temporary registry. Run the locked workflow in CI and document it for contributors. Why: workspace builds can hide missing packaged files and path-dependency mistakes; validating both interdependent archives catches failures that users would otherwise discover only after a release attempt. --- .github/workflows/ci.yml | 1 + CHANGELOG.md | 1 + CONTRIBUTING.md | 1 + xtask/src/main.rs | 31 +++++++++++++++++++++++++++++++ 4 files changed, 34 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 641ca1b..0bd61c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: with: tool: typos-cli,taplo-cli,hawkeye - run: cargo x lint + - run: cargo x package --locked msrv: name: Resolve MSRV diff --git a/CHANGELOG.md b/CHANGELOG.md index 2098b55..8e0fa5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ All notable changes to this project will be documented in this file. ### Improvements +* Verify both publishable crate archives from their normalized manifests in CI. * Implement `Default` for container attributes, `Tagging`, and `DefaultShape` so manual shape implementations can initialize ordinary Serde metadata concisely. * Allow unsized types such as `str` and slices to use the `SerializeShape::serialize_shape` and `DeserializeShape::deserialize_shape` convenience methods directly. * Document runtime graph construction, format-dependent unions, graph-local identifiers, built-in coverage, and custom representation boundaries. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c4a2b2..adc11d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,7 @@ Repository tasks are defined by `cargo x`: cargo x build --locked cargo x test cargo x lint +cargo x package --locked ``` Run `cargo x --help` or `cargo x --help` for command-specific options. `cargo x lint --fix` applies supported formatting and lint fixes. diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 6c93e7c..275e731 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -34,6 +34,7 @@ impl Command { match self.sub { SubCommand::Build(cmd) => cmd.run(), SubCommand::Lint(cmd) => cmd.run(), + SubCommand::Package(cmd) => cmd.run(), SubCommand::Test(cmd) => cmd.run(), } } @@ -45,6 +46,8 @@ enum SubCommand { Build(CommandBuild), #[clap(about = "Run workspace quality checks.")] Lint(CommandLint), + #[clap(about = "Build the publishable crate archives.")] + Package(CommandPackage), #[clap(about = "Run workspace unit tests.")] Test(CommandTest), } @@ -61,6 +64,18 @@ impl CommandBuild { } } +#[derive(Parser)] +struct CommandPackage { + #[arg(long, help = "Assert that `Cargo.lock` will remain unchanged.")] + locked: bool, +} + +impl CommandPackage { + fn run(self) { + run_command(make_package_cmd(self.locked)); + } +} + #[derive(Parser)] struct CommandTest { #[arg(long, help = "Run tests serially and do not capture output.")] @@ -157,6 +172,22 @@ fn make_build_cmd(locked: bool) -> StdCommand { cmd } +fn make_package_cmd(locked: bool) -> StdCommand { + let mut cmd = find_command("cargo"); + cmd.args([ + "package", + "--package", + "serde-shape-derive", + "--package", + "serde-shape", + "--all-features", + ]); + if locked { + cmd.arg("--locked"); + } + cmd +} + fn make_test_cmd(no_capture: bool, package: &str, features: &[&str]) -> StdCommand { let mut cmd = find_command("cargo"); cmd.args(["test", "-p", package, "--no-default-features"]);