Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Changelog

All notable changes to this project will be documented in this file.

## Unreleased

### Breaking changes

* Make shape graph roots and definition lists read-only. Use `root()`, `definitions()`, and `definition(id)` instead of accessing fields directly.
* 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.

### New features

* Add `#[serde_shape(serialize_with = "path", deserialize_with = "path")]` hooks for custom Serde functions and foreign representations.
* Preserve Rust doc comments on derived containers, variants, and fields as user-facing descriptions.

### Bug fixes

* 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.

### Improvements

* 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.
36 changes: 36 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Contributing

Thank you for helping improve `serde-shape`.

## Development workflow

Repository tasks are defined by `cargo x`:

```console
cargo x build --locked
cargo x test
cargo x lint
```

Run `cargo x --help` or `cargo x <command> --help` for command-specific options. `cargo x lint --fix` applies supported formatting and lint fixes.

The main crate is `no_std` by default. Changes to core shape construction must continue to compile without the `std` feature; the test task exercises the supported feature combinations.

## Tests

Add tests at the observable behavior boundary:

* Put shape context, built-in type, and graph behavior tests in `serde-shape/src/tests.rs`.
* Put derive behavior and Serde attribute compatibility tests in `tests/derive`.
* Put end-to-end consumer scenarios and comparisons with actual Serde calls in `tests/integration`.
* Put regressions that specifically exercise `no_std` derive output in `tests/no_std`.

Prefer focused assertions for the contract under test. Avoid snapshots of entire debug graphs: they obscure the behavior being protected and make unrelated metadata additions expensive to review.

## Public API changes

Serialization and deserialization may have different shapes, bounds, and names. Check both directions when changing a shared implementation, and compare built-in types with Serde's actual data-model calls when behavior depends on the format or human-readable mode.

Document user-visible changes in `CHANGELOG.md`. Update the README or crate-level documentation when a change affects setup, feature flags, supported representations, model boundaries, or migration steps.

Keep pull requests and commits focused enough to review independently. Explain the concrete user problem and avoid adding generic abstractions without a current consumer.
121 changes: 0 additions & 121 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ serde-shape-derive = { version = "=0.0.1", path = "serde-shape-derive" }

# crates.io dependencies
clap = { version = "4.6.1" }
insta = { version = "1.48.0" }
proc-macro-crate = { version = "3.5.0" }
proc-macro2 = { version = "1.0.95" }
quote = { version = "1.0.40" }
Expand Down
58 changes: 53 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
[docs-url]: https://docs.rs/serde-shape
[msrv-badge]: https://img.shields.io/badge/MSRV-1.85-green?logo=rust
[license-badge]: https://img.shields.io/crates/l/serde-shape
[license-url]: LICENSE
[license-url]: https://www.apache.org/licenses/LICENSE-2.0
[actions-badge]: https://github.com/fast/serde-shape/workflows/CI/badge.svg
[actions-url]: https://github.com/fast/serde-shape/actions?query=workflow%3ACI

`serde-shape` reflects the shape of Serde serialization and deserialization at compile time.
`serde-shape` builds inspectable graphs of Serde serialization and deserialization shapes. Derive macros generate the metadata code at compile time; calling `serialize_shape()` or `deserialize_shape()` constructs the graph at runtime without serializing or deserializing a value.

It gives libraries and tools a lightweight graph of the Rust types, Serde names, field metadata, enum tagging, defaults, aliases, union value alternatives, skips, and custom serializer/deserializer boundaries that make up a type's wire shape.

Expand Down Expand Up @@ -65,7 +65,9 @@ use serde_shape::{

#[derive(DeserializeShape)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
/// Application configuration.
struct Config {
/// Port used by the HTTP server.
http_port: u16,
peers: Vec<String>,
tls: Option<TlsConfig>,
Expand All @@ -79,25 +81,67 @@ struct TlsConfig {
}

let graph = Config::deserialize_shape();
let ShapeRef::Definition(config_id) = graph.root else {
let ShapeRef::Definition(config_id) = graph.root() else {
panic!("Config should produce a named definition");
};
let definition = graph.definition(config_id).unwrap();
let definition = graph.definition(*config_id).unwrap();

let DeserializeDefinitionKind::Struct(shape) = &definition.kind else {
panic!("Config should produce a struct shape");
};

assert_eq!(definition.type_name.name, "Config");
assert_eq!(definition.description, Some("Application configuration."));
assert_eq!(shape.style, FieldsStyle::Struct);
assert!(shape.attributes.deny_unknown_fields);
assert_eq!(shape.fields[0].name, "http-port");
assert_eq!(shape.fields[0].description, Some("Port used by the HTTP server."));
assert_eq!(shape.fields[1].name, "peers");
assert_eq!(shape.fields[2].name, "tls");
```

See the [crate documentation][docs-url] for the full shape graph model, derive behavior, and manual implementation examples.

Rust doc comments on derived containers, variants, and fields are preserved as descriptions. Consumers can use the same comments for generated configuration references, CLI help, or diagnostics.

## Custom representations

Custom Serde functions and foreign types do not expose enough information for `serde-shape` to infer their wire representation. Provide functions that build the serialization and deserialization shapes explicitly:

```rust
use serde_shape::{
DeserializeShape, DeserializeShapeContext, SerializeShape, SerializeShapeContext, ShapeRef,
};

struct ForeignDuration;

fn serialize_duration(context: &mut SerializeShapeContext) -> ShapeRef {
String::serialize_shape_in(context)
}

fn deserialize_duration(_context: &mut DeserializeShapeContext) -> ShapeRef {
ShapeRef::union([ShapeRef::String, ShapeRef::U64])
}

#[derive(SerializeShape, DeserializeShape)]
struct Config {
#[serde(with = "duration_format")]
#[serde_shape(
serialize_with = "serialize_duration",
deserialize_with = "deserialize_duration"
)]
timeout: ForeignDuration,
}
```

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.

## Model boundaries

Shape graphs are an inspection API, not a stable interchange format. `ShapeId` values are local to one graph, and definition ordering and `Debug` output are not persistence contracts.

Types that branch on `Serializer::is_human_readable()` or `Deserializer::is_human_readable()` may expose a union of their known representations. The graph describes the possible Serde calls across formats; it is not specialized for one serializer format.

## Feature flags

`serde-shape` enables no features by default.
Expand Down Expand Up @@ -133,6 +177,10 @@ This crate's minimum supported `rustc` version is `1.85.0`.

The current policy is that the minimum Rust version required to use this crate can be increased in minor version updates. For example, if `crate 1.0` requires Rust 1.85.0, then `crate 1.0.z` for all values of `z` will also require Rust 1.85.0 or newer. However, `crate 1.y` for `y > 0` may require a newer minimum version of Rust.

## Contributing

See the [contributor guide](CONTRIBUTING.md) for the development workflow and test conventions.

## License

This project is licensed under [Apache License, Version 2.0](LICENSE).
This project is licensed under the [Apache License, Version 2.0](https://github.com/fast/serde-shape/blob/main/LICENSE).
Loading