Skip to content

feat: harden serde-shape for downstream use - #11

Merged
tisonkun merged 14 commits into
mainfrom
codex/public-api-hardening
Aug 29, 2026
Merged

feat: harden serde-shape for downstream use#11
tisonkun merged 14 commits into
mainfrom
codex/public-api-hardening

Conversation

@tisonkun

Copy link
Copy Markdown
Contributor

Summary

  • fix the remaining Serde parity gaps, including directional wrapper bounds, owned byte slices, and known custom variant content
  • cover common native Serde types that downstream crates cannot implement because of orphan rules
  • clarify the public contracts and verify both normalized publish archives in CI

The 14 commits are intentionally separated by concern. Each commit message includes the concrete rationale under Why: so the history can be reviewed in order without relying on a long PR narrative.

Validation

  • cargo x build --locked
  • cargo x test
  • cargo x lint
  • cargo x package --locked
  • cargo +1.85.0 x test
  • cargo +1.85.0 check -p serde-shape --lib --no-default-features --target thumbv7em-none-eabihf
  • cargo +1.85.0 check -p serde-shape-test-no-std --lib --target thumbv7em-none-eabihf
  • cargo semver-checks check-release -p serde-shape --baseline-rev main --all-features --release-type patch

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.
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.
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.
Keep Box<T> transparent for sized inputs, but reflect Box<[T]> through Serde's owned sequence path. Add explicit unsized implementations for Box<str> and Box<Path> 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.
Add no_std shape implementations for CStr serialization, CString in both directions, and Box<CStr> 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.
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.
Reflect Saturating<T> 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.
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.
Reflect Bound<T> 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.
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.
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.
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.
Document that Box<[u8]> follows Serde's owned sequence path and record the narrowed Box<T> 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.
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.
@tisonkun
tisonkun merged commit 8369886 into main Aug 29, 2026
12 checks passed
@tisonkun
tisonkun deleted the codex/public-api-hardening branch August 29, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant