Skip to content

Commit 20fa68a

Browse files
authored
fix: close remaining 0.1.0 release gaps (#15)
Signed-off-by: tison <wander4096@gmail.com>
1 parent fba3618 commit 20fa68a

8 files changed

Lines changed: 120 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,19 @@ All notable changes to this project will be documented in this file.
4040

4141
### Bug fixes
4242

43-
* Match Serde's fixed-array coverage and bounds: lengths above 32 no longer claim shape support, while zero-length arrays no longer require their unobserved element type to implement a shape trait.
43+
* Preserve qualified Serde function paths such as `<T as Trait>::function` as parseable Rust token streams.
44+
* Match Serde's fixed-array coverage and bounds: lengths above 32 no longer claim shape support, while zero-length arrays no longer require their unobserved element type to implement a shape trait, including inside derived generic containers.
4445
* Match Serde's deserialization bounds for tree and hash collections so a shape implementation is exposed only when the corresponding collection can actually deserialize.
4546
* Preserve the known string and byte shapes of `#[serde(borrow)]` fields using `Cow<str>` or `Cow<[u8]>` from their source-level metadata, while leaving explicit custom deserializers opaque.
4647
* Distinguish borrowed byte input from owned boxed slices: `&[u8]` reflects bytes while `Box<[u8]>` reflects a sequence, matching Serde.
4748
* Match Serde's serialization bounds for `BinaryHeap`, `RefCell`, `Mutex`, and `RwLock`, including unsized wrapper contents.
4849
* Reflect the proxy type used by Serde `from`, `try_from`, and `into` container attributes.
4950
* Follow Serde's directional bounds for `Cow`: serialization reflects the borrowed type and deserialization reflects the owned type.
5051
* Make IP and socket address shapes available in `no_std` builds through `core::net`.
51-
* Preserve qualified Serde default paths without token-rendering spaces.
5252

5353
### Improvements
5454

55+
* Point README and crate-level installation examples at the upcoming `0.1.0` release.
5556
* State the graph-local `ShapeId` ownership contract accurately instead of claiming that lookups can detect an in-bounds id copied from another graph.
5657
* Add `definition_for` to both graph types so walkers can resolve a `ShapeRef::Definition` without repeating a match and id lookup.
5758
* 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.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ Enable the `derive` feature when you want `#[derive(SerializeShape)]` and `#[der
2626

2727
```toml
2828
[dependencies]
29-
serde-shape = { version = "0.0.1", features = ["derive"] }
29+
serde-shape = { version = "0.1.0", features = ["derive"] }
3030
```
3131

3232
Enable `std` when your reflected types use shapes provided only by the Rust standard library:
3333

3434
```toml
3535
[dependencies]
36-
serde-shape = { version = "0.0.1", features = ["derive", "std"] }
36+
serde-shape = { version = "0.1.0", features = ["derive", "std"] }
3737
```
3838

3939
## Motivation

serde-shape-derive/src/lib.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,15 @@ fn collect_shape_bound_types(
368368
type_params: &BTreeSet<String>,
369369
field_bound_types: &mut Vec<Type>,
370370
) {
371+
// Keep this selective traversal aligned with serde_derive::bound::with_bound. A general
372+
// syn::visit::Visit would also enter macros and const expressions, where mentioning a type
373+
// parameter does not imply that the field needs a Shape bound.
371374
match ty {
372-
Type::Array(ty) => collect_shape_bound_types(&ty.elem, type_params, field_bound_types),
375+
Type::Array(ty) => {
376+
if !is_zero_length(&ty.len) {
377+
collect_shape_bound_types(&ty.elem, type_params, field_bound_types);
378+
}
379+
}
373380
Type::FnPtr(ty) => {
374381
for input in &ty.inputs {
375382
collect_shape_bound_types(&input.ty, type_params, field_bound_types);
@@ -445,6 +452,19 @@ fn collect_shape_bound_types(
445452
}
446453
}
447454

455+
fn is_zero_length(expr: &syn::Expr) -> bool {
456+
match expr {
457+
syn::Expr::Group(expr) => is_zero_length(&expr.expr),
458+
syn::Expr::Lit(expr) => {
459+
matches!(&expr.lit, syn::Lit::Int(value) if value
460+
.base10_parse::<usize>()
461+
.is_ok_and(|value| value == 0))
462+
}
463+
syn::Expr::Paren(expr) => is_zero_length(&expr.expr),
464+
_ => false,
465+
}
466+
}
467+
448468
fn type_uses_params(ty: &Type, type_params: &BTreeSet<String>) -> bool {
449469
let mut bound_types = Vec::new();
450470
collect_shape_bound_types(ty, type_params, &mut bound_types);
@@ -931,7 +951,7 @@ fn default_shape(default: &attr::Default) -> TokenStream2 {
931951
attr::Default::None => quote!(__serde_shape::DefaultShape::None),
932952
attr::Default::Default => quote!(__serde_shape::DefaultShape::Default),
933953
attr::Default::Path(path) => {
934-
let path = lit(path.to_token_stream().to_string().replace(' ', ""));
954+
let path = lit(path.to_token_stream().to_string());
935955
quote!(__serde_shape::DefaultShape::Path(#path))
936956
}
937957
}
@@ -1033,7 +1053,7 @@ fn option_lit(value: Option<&str>) -> TokenStream2 {
10331053
fn option_path(value: Option<&syn::ExprPath>) -> TokenStream2 {
10341054
match value {
10351055
Some(value) => {
1036-
let value = lit(value.to_token_stream().to_string().replace(' ', ""));
1056+
let value = lit(value.to_token_stream().to_string());
10371057
quote!(::core::option::Option::Some(#value))
10381058
}
10391059
None => quote!(::core::option::Option::None),

serde-shape/src/lib.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@
3030
//!
3131
//! ```toml
3232
//! [dependencies]
33-
//! serde-shape = { version = "0.0.1", features = ["derive"] }
33+
//! serde-shape = { version = "0.1.0", features = ["derive"] }
3434
//! ```
3535
//!
3636
//! Enable `std` when the reflected types use shapes provided only by the Rust standard library:
3737
//!
3838
//! ```toml
3939
//! [dependencies]
40-
//! serde-shape = { version = "0.0.1", features = ["derive", "std"] }
40+
//! serde-shape = { version = "0.1.0", features = ["derive", "std"] }
4141
//! ```
4242
//!
4343
//! The crate is `no_std` by default and requires `alloc`.
@@ -293,7 +293,7 @@ pub use serde_shape_derive::DeserializeShape;
293293
/// assert_eq!(definition.type_name.name, "api-response");
294294
/// assert_eq!(shape.fields[0].name, "requestId");
295295
/// assert_eq!(shape.fields[1].name, "nextPage");
296-
/// assert_eq!(shape.fields[1].skip_if, Some("Option::is_none"));
296+
/// assert!(shape.fields[1].skip_if.is_some());
297297
/// ```
298298
pub use serde_shape_derive::SerializeShape;
299299

@@ -987,7 +987,8 @@ pub struct SerializeFieldShape {
987987
pub description: Option<&'static str>,
988988
/// How this field contributes to the serialized wire shape.
989989
pub wire_shape: FieldWireShape,
990-
/// The predicate used to skip this field during serialization.
990+
/// The predicate used to skip this field during serialization, rendered as a parseable Rust
991+
/// path token stream. Whitespace is not normalized.
991992
pub skip_if: Option<&'static str>,
992993
}
993994

@@ -1120,7 +1121,8 @@ pub enum DefaultShape {
11201121
None,
11211122
/// `Default::default()` is used.
11221123
Default,
1123-
/// A custom default function path is used.
1124+
/// A custom default function path is used. The value is a parseable Rust path token stream;
1125+
/// whitespace is not normalized.
11241126
Path(&'static str),
11251127
}
11261128

@@ -1145,9 +1147,11 @@ pub struct OpaqueShape {
11451147
/// Reason a shape cannot be represented precisely.
11461148
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
11471149
pub enum OpaqueReason {
1148-
/// A custom serializer controls the output.
1150+
/// A custom serializer controls the output. Derive-generated `detail` is a parseable Rust path
1151+
/// token stream whose whitespace is not normalized.
11491152
CustomSerializer,
1150-
/// A custom deserializer controls the input.
1153+
/// A custom deserializer controls the input. Derive-generated `detail` is a parseable Rust
1154+
/// path token stream whose whitespace is not normalized.
11511155
CustomDeserializer,
11521156
/// The type has no built-in shape implementation.
11531157
Unsupported,

tests/derive/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ renamed-shape = { package = "serde-shape", path = "../../serde-shape", features
3030
[dev-dependencies]
3131
serde = { workspace = true }
3232
serde_json = { workspace = true }
33+
syn = { workspace = true }
3334

3435
[lints]
3536
workspace = true

tests/derive/tests/derive.rs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,17 @@ struct Marker<T> {
188188
marker: core::marker::PhantomData<T>,
189189
}
190190

191+
#[derive(SerializeShape, DeserializeShape)]
192+
struct EmptyArray<T> {
193+
values: [T; 0],
194+
}
195+
196+
#[derive(DeserializeShape)]
197+
struct QualifiedMetadataPaths {
198+
#[serde(default = "<u8 as Default>::default")]
199+
value: u8,
200+
}
201+
191202
#[derive(serde::Deserialize, DeserializeShape)]
192203
struct BorrowedCow<'a> {
193204
#[serde(borrow)]
@@ -308,6 +319,10 @@ fn deserialize_bool_shape(_context: &mut DeserializeShapeContext) -> ShapeRef {
308319
ShapeRef::Bool
309320
}
310321

322+
fn parse_path(path: &str) -> syn::ExprPath {
323+
syn::parse_str(path).expect("metadata path should remain valid Rust")
324+
}
325+
311326
fn deserialize_borrowed_str<'de, D>(deserializer: D) -> Result<Cow<'de, str>, D::Error>
312327
where
313328
D: serde::Deserializer<'de>,
@@ -335,9 +350,18 @@ fn exposes_deserialize_container_attributes() {
335350
};
336351
assert_eq!(http_port.name, "http-port");
337352
assert_eq!(api_url.aliases, ["api-url", "endpoint"]);
353+
let DefaultShape::Path(path) = retries.default else {
354+
panic!("custom default should be represented by its path");
355+
};
356+
let path = parse_path(path);
357+
assert!(path.qself.is_none());
338358
assert_eq!(
339-
retries.default,
340-
DefaultShape::Path("crate::default_retries")
359+
path.path
360+
.segments
361+
.iter()
362+
.map(|segment| segment.ident.to_string())
363+
.collect::<Vec<_>>(),
364+
["crate", "default_retries"]
341365
);
342366
assert!(matches!(storage.wire_shape, FieldWireShape::Flatten(_)));
343367
assert_eq!(skipped.wire_shape, FieldWireShape::Omitted);
@@ -521,7 +545,7 @@ fn preserves_rust_documentation() {
521545
}
522546

523547
#[test]
524-
fn omits_shape_bounds_for_skipped_and_marker_fields() {
548+
fn omits_unnecessary_shape_bounds() {
525549
assert_eq!(
526550
SkipsGeneric::<NotShape>::deserialize_shape()
527551
.definitions()
@@ -532,6 +556,39 @@ fn omits_shape_bounds_for_skipped_and_marker_fields() {
532556
Marker::<NotShape>::deserialize_shape().definitions().len(),
533557
1
534558
);
559+
assert_eq!(
560+
EmptyArray::<NotShape>::serialize_shape()
561+
.definitions()
562+
.len(),
563+
1
564+
);
565+
assert_eq!(
566+
EmptyArray::<NotShape>::deserialize_shape()
567+
.definitions()
568+
.len(),
569+
1
570+
);
571+
}
572+
573+
#[test]
574+
fn preserves_qualified_metadata_paths() {
575+
let deserialize = deserialize_root_definition::<QualifiedMetadataPaths>();
576+
let DeserializeDefinitionKind::Struct(deserialize) = &deserialize.kind else {
577+
panic!("deserialization definition should be a struct");
578+
};
579+
let DefaultShape::Path(path) = deserialize.fields[0].default else {
580+
panic!("qualified default should be represented by its path");
581+
};
582+
let path = parse_path(path);
583+
assert!(path.qself.is_some());
584+
assert_eq!(
585+
path.path
586+
.segments
587+
.iter()
588+
.map(|segment| segment.ident.to_string())
589+
.collect::<Vec<_>>(),
590+
["Default", "default"]
591+
);
535592
}
536593

537594
#[test]

tests/derive/tests/serde_compat.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,19 @@ fn flat_value_shape() -> ShapeRef {
8585
}
8686
}
8787

88+
fn assert_path(actual: Option<&str>, expected_segments: &[&str]) {
89+
let path = syn::parse_str::<syn::ExprPath>(actual.expect("path metadata should be present"))
90+
.expect("metadata path should remain valid Rust");
91+
assert_eq!(
92+
path.path
93+
.segments
94+
.iter()
95+
.map(|segment| segment.ident.to_string())
96+
.collect::<Vec<_>>(),
97+
expected_segments
98+
);
99+
}
100+
88101
#[test]
89102
fn composes_flatten_with_custom_field_boundaries() {
90103
let value = FlattenedCustom {
@@ -102,14 +115,14 @@ fn composes_flatten_with_custom_field_boundaries() {
102115
panic!("custom serialized flatten field should be flattened and opaque");
103116
};
104117
assert_eq!(opaque.reason, OpaqueReason::CustomSerializer);
105-
assert_eq!(opaque.detail, Some("flat_value::serialize"));
118+
assert_path(opaque.detail, &["flat_value", "serialize"]);
106119

107120
let deserialize_field = first_deserialize_field::<FlattenedCustom>();
108121
let FieldWireShape::Flatten(ShapeRef::Opaque(opaque)) = deserialize_field.wire_shape else {
109122
panic!("custom deserialized flatten field should be flattened and opaque");
110123
};
111124
assert_eq!(opaque.reason, OpaqueReason::CustomDeserializer);
112-
assert_eq!(opaque.detail, Some("flat_value::deserialize"));
125+
assert_path(opaque.detail, &["flat_value", "deserialize"]);
113126
}
114127

115128
#[test]
@@ -147,14 +160,14 @@ fn composes_transparent_with_custom_field_boundaries() {
147160
panic!("custom serialized transparent field should be inline and opaque");
148161
};
149162
assert_eq!(opaque.reason, OpaqueReason::CustomSerializer);
150-
assert_eq!(opaque.detail, Some("stringified::serialize"));
163+
assert_path(opaque.detail, &["stringified", "serialize"]);
151164

152165
let deserialize_field = first_deserialize_field::<TransparentCustom>();
153166
let FieldWireShape::Inline(ShapeRef::Opaque(opaque)) = deserialize_field.wire_shape else {
154167
panic!("custom deserialized transparent field should be inline and opaque");
155168
};
156169
assert_eq!(opaque.reason, OpaqueReason::CustomDeserializer);
157-
assert_eq!(opaque.detail, Some("stringified::deserialize"));
170+
assert_path(opaque.detail, &["stringified", "deserialize"]);
158171
}
159172

160173
#[test]
@@ -172,14 +185,14 @@ fn retains_custom_variant_boundary_details() {
172185
panic!("custom serialized variant should expose opaque content");
173186
};
174187
assert_eq!(opaque.reason, OpaqueReason::CustomSerializer);
175-
assert_eq!(opaque.detail, Some("stringified::serialize"));
188+
assert_path(opaque.detail, &["stringified", "serialize"]);
176189

177190
let deserialize_variant = first_deserialize_variant::<CustomVariant>();
178191
let DeserializeVariantContent::Custom(opaque) = deserialize_variant.content else {
179192
panic!("custom deserialized variant should expose opaque content");
180193
};
181194
assert_eq!(opaque.reason, OpaqueReason::CustomDeserializer);
182-
assert_eq!(opaque.detail, Some("stringified::deserialize"));
195+
assert_path(opaque.detail, &["stringified", "deserialize"]);
183196
}
184197

185198
#[test]

0 commit comments

Comments
 (0)