Skip to content
Open
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file.

### Breaking changes

* Represent Serde field and variant identifier enums with dedicated `Tagging` variants instead of incorrectly describing their input as externally tagged enums.
* Gate atomic shape implementations behind the `std` feature, matching Serde's own atomic implementations instead of advertising them in `no_std` builds where Serde cannot use them.
* Remove `DeserializeShape` from the unsized `str`, `[u8]`, and `Path` types, which do not implement Serde `Deserialize`. Their supported borrowed and owned forms retain explicit shapes.
* Replace the identical `SerializeTypeName` and `DeserializeTypeName` structures with one direction-neutral `TypeName`. Manual named definitions can use `TypeName::of::<T>(serde_name)` instead of repeating `core::any::type_name::<T>()`.
* 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`.
Expand All @@ -17,6 +21,9 @@ All notable changes to this project will be documented in this file.

### New features

* Reflect serialized `core::fmt::Arguments` as a string, matching Serde's formatting implementation.
* Add target-specific `OsStr` and `OsString` enum shapes on Unix and Windows, including owned `Box<OsStr>` input.
* Add `SerializeShapeGraph::from_fn` and `DeserializeShapeGraph::from_fn` so custom shape functions can describe foreign graph roots without a dummy wrapper type.
* Add `#[serde_shape(serialize_with = "path", deserialize_with = "path")]` hooks for custom Serde functions and foreign representations.
* Allow custom shape hooks on enum variants so known custom variant content does not have to remain opaque.
* Reflect Serde's byte-buffer representation for `CStr`, `CString`, and owned `Box<CStr>` input.
Expand All @@ -31,6 +38,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<str>` 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<str>` 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.
Expand All @@ -42,6 +50,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.
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,21 +158,25 @@ The built-in implementations follow Serde's semantic representations in each dir

| Group | Supported types |
| --- | --- |
| Scalars | Rust primitives, `String`, `str`, non-zero integers, and atomics available on the target |
| Scalars | Rust primitives, `String`, serialized `str` and `fmt::Arguments`, and non-zero integers |
| Containers | `Option`, `Result`, arrays, slices for serialization, tuples through arity 16, `Vec`, `VecDeque`, `LinkedList`, `BinaryHeap`, `BTreeSet`, and `BTreeMap` |
| Wrappers | Serialized references, borrowed string/byte/path inputs, `Box`, `Rc`, `Arc`, their weak pointers, `Cow`, `Cell`, `RefCell`, `Wrapping`, `Saturating`, `Reverse`, and `PhantomData` |
| FFI | `CStr` and `CString` byte representations, including owned `Box<CStr>` input |
| FFI | `CStr` and `CString` byte representations; on Unix and Windows, serialized `OsStr`, `OsString`, and owned `Box<OsStr>` input |
| Ranges | `Range`, `RangeFrom`, `RangeInclusive`, `RangeTo`, and `Bound` |
| Time | `core::time::Duration` and, with `std`, `SystemTime` |
| Network | `core::net` IP and socket address types |
| `std` feature | `HashMap`, `HashSet`, `Path`, `PathBuf`, `Mutex`, and `RwLock` |
| `std` feature | Atomics available on the target, `HashMap`, `HashSet`, `Path`, `PathBuf`, `Mutex`, and `RwLock` |

Network address shapes are unions of their human-readable string representation and their compact Serde representation. A serialized byte slice and an owned `Box<[u8]>` input are sequences, while borrowed byte deserialization uses `ShapeRef::Bytes`.

OS string shapes preserve Serde's target-specific externally tagged representation: `Unix` contains a byte sequence, while `Windows` contains a `u16` sequence. Deserialization advertises only the variant accepted on the current target.

Serde's `rc` feature is still required to serialize or deserialize `Rc`, `Arc`, and their weak pointers; the shape implementations do not enable Serde features.

Serialization follows Serde's blanket support for `&T` and `&mut T`. Deserialization only provides reference shapes for Serde's borrowable `&str`, `&[u8]`, and `&Path` inputs; arbitrary shared and mutable references do not have a Serde deserializer.

The unsized `str`, `[u8]`, and `Path` types themselves do not implement `DeserializeShape`, matching Serde. Their borrowed and owned input forms have explicit shape implementations.

For an unsupported foreign type, use a local newtype and implement `SerializeShape` or `DeserializeShape` manually. Custom Serde functions remain opaque by default because their wire behavior cannot be inferred; use a `serde_shape` custom hook when the representation is known.

## `no_std` support
Expand Down
40 changes: 29 additions & 11 deletions serde-shape-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@ fn parse_container<'a>(input: &'a DeriveInput, derive: Derive) -> syn::Result<as
));
};
cx.check()?;

if matches!(derive, Derive::Serialize) {
let message = match container.attrs.identifier() {
attr::Identifier::No => 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)
}

Expand Down Expand Up @@ -535,10 +547,7 @@ fn serialize_shape_body(

Ok(quote! {
context.define_named_type_with_description(
__serde_shape::SerializeTypeName {
rust_name: ::core::any::type_name::<Self>(),
name: #name,
},
__serde_shape::TypeName::of::<Self>(#name),
#description,
|context| {
#kind
Expand Down Expand Up @@ -569,10 +578,7 @@ fn deserialize_shape_body(

Ok(quote! {
context.define_named_type_with_description(
__serde_shape::DeserializeTypeName {
rust_name: ::core::any::type_name::<Self>(),
name: #name,
},
__serde_shape::TypeName::of::<Self>(#name),
#description,
|context| {
#kind
Expand Down Expand Up @@ -643,7 +649,7 @@ fn deserialize_definition_kind(container: &ast::Container<'_>) -> syn::Result<To
}
}
ast::Data::Enum(variants) => {
let repr = tagging(container.attrs.tag());
let repr = deserialize_tagging(&container.attrs);
let variants = variants
.iter()
.map(deserialize_variant_shape)
Expand Down Expand Up @@ -943,6 +949,14 @@ fn tagging(tag: &attr::TagType) -> TokenStream2 {
}
}

fn deserialize_tagging(attrs: &attr::Container) -> TokenStream2 {
match attrs.identifier() {
attr::Identifier::No => tagging(attrs.tag()),
attr::Identifier::Field => quote!(__serde_shape::Tagging::FieldIdentifier),
attr::Identifier::Variant => quote!(__serde_shape::Tagging::VariantIdentifier),
}
}

fn default_shape(default: &attr::Default) -> TokenStream2 {
match default {
attr::Default::None => quote!(__serde_shape::DefaultShape::None),
Expand All @@ -966,10 +980,14 @@ fn serde_borrowed_cow_shape(path: &syn::ExprPath) -> Option<TokenStream2> {

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;
}

Expand Down
66 changes: 23 additions & 43 deletions serde-shape/src/impls/bound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
// limitations under the License.

use alloc::vec;
use core::any::type_name;
use core::ops::Bound;

use crate::DefaultShape;
Expand All @@ -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;
Expand All @@ -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<T> SerializeShape for Bound<T>
where
T: SerializeShape,
{
fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef {
context.define_named_type(
SerializeTypeName {
rust_name: type_name::<Self>(),
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::<Self>("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(),
})
})
}
}

Expand All @@ -71,29 +63,17 @@ where
T: DeserializeShape,
{
fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef {
context.define_named_type(
DeserializeTypeName {
rust_name: type_name::<Self>(),
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::<Self>("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(),
})
})
}
}

Expand Down
2 changes: 2 additions & 0 deletions serde-shape/src/impls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading