diff --git a/Cargo.lock b/Cargo.lock index ba116e3..a471cf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,7 +95,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -178,6 +178,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "libc" version = "0.2.186" @@ -190,6 +196,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "once_cell" version = "1.21.4" @@ -239,6 +251,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + [[package]] name = "serde-shape" version = "0.0.1" @@ -253,7 +275,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.118", ] [[package]] @@ -261,7 +283,9 @@ name = "serde-shape-test-derive" version = "0.0.0" dependencies = [ "insta", + "serde", "serde-shape", + "serde_json", ] [[package]] @@ -280,6 +304,26 @@ dependencies = [ "serde-shape", ] +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "serde_derive_internals" version = "0.29.1" @@ -288,7 +332,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", ] [[package]] @@ -314,6 +371,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -370,3 +438,9 @@ dependencies = [ "clap", "which", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 1caacc7..c65c2df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,9 @@ clap = { version = "4.6.1" } insta = { version = "1.48.0" } proc-macro2 = { version = "1.0.95" } quote = { version = "1.0.40" } +serde = { version = "1.0.229", features = ["derive"] } serde_derive_internals = { version = "0.29.1" } +serde_json = { version = "1.0.151" } syn = { version = "2.0.104" } which = { version = "8.0.4" } diff --git a/README.md b/README.md index 2503a21..8e9693a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ `serde-shape` reflects the shape of Serde serialization and deserialization at compile time. -It gives libraries and tools a lightweight graph of the Rust types, Serde names, field metadata, enum tagging, defaults, aliases, skips, and custom serializer/deserializer boundaries that make up a type's wire shape. +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. ## Install @@ -48,7 +48,9 @@ Typical use cases: - checking how a serialized or deserialized shape changes across releases; - building schema exporters that start from Serde metadata. -`serde-shape` is intentionally not a full validation schema. It reflects the Serde data model shape and relevant Serde attributes; it does not infer value ranges, regexes, business rules, or runtime behavior hidden inside custom serializer/deserializer functions. +`serde-shape` is intentionally not a full validation schema. It reflects the Serde data model shape and relevant Serde attributes; it does not infer value ranges, regexes, business rules, or runtime behavior hidden inside custom serializer/deserializer functions. Use `ShapeRef::union` for format-native alternatives that do not fit one Rust shape. Union alternatives may overlap; they are flattened, deduplicated, and stored in canonical order. + +Field shapes expose `wire_shape` as the source of truth for regular values, flattened fields, inline transparent fields, and omitted fields. Custom serializer/deserializer boundaries are represented by `ShapeRef::Opaque`, including when they are flattened or inline. You may use [`schemars`](https://docs.rs/schemars) for JSON Schema generation and validation. But `schemars` is not a general-purpose Serde shape reflection library, and it does not support all Serde attributes. `serde-shape` is designed to be a more complete and general-purpose reflection of Serde shapes. diff --git a/serde-shape-derive/src/lib.rs b/serde-shape-derive/src/lib.rs index a091182..f7d7231 100644 --- a/serde-shape-derive/src/lib.rs +++ b/serde-shape-derive/src/lib.rs @@ -513,12 +513,25 @@ fn serialize_variant_shape(variant: &ast::Variant<'_>) -> TokenStream2 { let name = lit(variant.attrs.name().serialize_name()); let style = fields_style(variant.style); let skip = variant.attrs.skip_serializing(); - let custom_serializer = variant.attrs.serialize_with().is_some(); let untagged = variant.attrs.untagged(); - let fields: Vec<_> = if skip || custom_serializer { - Vec::new() + let content = if skip { + quote!(::serde_shape::SerializeVariantContent::Omitted) + } else if let Some(custom_serializer) = variant.attrs.serialize_with() { + let detail = option_path(Some(custom_serializer)); + quote! { + ::serde_shape::SerializeVariantContent::Custom(::serde_shape::OpaqueShape { + type_name: ::core::any::type_name::(), + reason: ::serde_shape::OpaqueReason::CustomSerializer, + detail: #detail, + }) + } } else { - variant.fields.iter().map(serialize_field_shape).collect() + let fields = variant.fields.iter().map(serialize_field_shape); + quote! { + ::serde_shape::SerializeVariantContent::Fields( + ::serde_shape::__private::vec![#(#fields),*], + ) + } }; quote! { @@ -526,9 +539,7 @@ fn serialize_variant_shape(variant: &ast::Variant<'_>) -> TokenStream2 { rust_name: #rust_name, name: #name, style: #style, - fields: ::serde_shape::__private::vec![#(#fields),*], - skip: #skip, - custom_serializer: #custom_serializer, + content: #content, untagged: #untagged, } } @@ -540,13 +551,26 @@ fn deserialize_variant_shape(variant: &ast::Variant<'_>) -> TokenStream2 { let aliases = aliases(variant.attrs.aliases()); let style = fields_style(variant.style); let skip = variant.attrs.skip_deserializing(); - let custom_deserializer = variant.attrs.deserialize_with().is_some(); let other = variant.attrs.other(); let untagged = variant.attrs.untagged(); - let fields: Vec<_> = if skip || custom_deserializer { - Vec::new() + let content = if skip { + quote!(::serde_shape::DeserializeVariantContent::Omitted) + } else if let Some(custom_deserializer) = variant.attrs.deserialize_with() { + let detail = option_path(Some(custom_deserializer)); + quote! { + ::serde_shape::DeserializeVariantContent::Custom(::serde_shape::OpaqueShape { + type_name: ::core::any::type_name::(), + reason: ::serde_shape::OpaqueReason::CustomDeserializer, + detail: #detail, + }) + } } else { - variant.fields.iter().map(deserialize_field_shape).collect() + let fields = variant.fields.iter().map(deserialize_field_shape); + quote! { + ::serde_shape::DeserializeVariantContent::Fields( + ::serde_shape::__private::vec![#(#fields),*], + ) + } }; quote! { @@ -555,9 +579,7 @@ fn deserialize_variant_shape(variant: &ast::Variant<'_>) -> TokenStream2 { name: #name, aliases: #aliases, style: #style, - fields: ::serde_shape::__private::vec![#(#fields),*], - skip: #skip, - custom_deserializer: #custom_deserializer, + content: #content, other: #other, untagged: #untagged, } @@ -569,26 +591,40 @@ fn serialize_field_shape(field: &ast::Field<'_>) -> TokenStream2 { let name = lit(field.attrs.name().serialize_name()); let skip = field.attrs.skip_serializing(); let skip_if = option_path(field.attrs.skip_serializing_if()); - let custom_serializer = field.attrs.serialize_with().is_some(); let flatten = field.attrs.flatten(); let transparent = field.attrs.transparent(); let ty = field.ty; - let value_shape = if skip || custom_serializer { - quote!(::core::option::Option::None) + let wire_shape = if skip { + quote!(::serde_shape::FieldWireShape::Omitted) } else { - quote!(::core::option::Option::Some(<#ty as ::serde_shape::SerializeShape>::serialize_shape_in(context))) + let value_shape = if let Some(custom_serializer) = field.attrs.serialize_with() { + let detail = option_path(Some(custom_serializer)); + quote! { + ::serde_shape::ShapeRef::Opaque(::serde_shape::OpaqueShape { + type_name: ::core::any::type_name::<#ty>(), + reason: ::serde_shape::OpaqueReason::CustomSerializer, + detail: #detail, + }) + } + } else { + quote!(<#ty as ::serde_shape::SerializeShape>::serialize_shape_in(context)) + }; + + if transparent { + quote!(::serde_shape::FieldWireShape::Inline(#value_shape)) + } else if flatten { + quote!(::serde_shape::FieldWireShape::Flatten(#value_shape)) + } else { + quote!(::serde_shape::FieldWireShape::Value(#value_shape)) + } }; quote! { ::serde_shape::SerializeFieldShape { member: #member, name: #name, - value_shape: #value_shape, - flatten: #flatten, - skip: #skip, + wire_shape: #wire_shape, skip_if: #skip_if, - custom_serializer: #custom_serializer, - transparent: #transparent, } } } @@ -598,15 +634,33 @@ fn deserialize_field_shape(field: &ast::Field<'_>) -> TokenStream2 { let name = lit(field.attrs.name().deserialize_name()); let aliases = aliases(field.attrs.aliases()); let skip = field.attrs.skip_deserializing(); - let custom_deserializer = field.attrs.deserialize_with().is_some(); let default = default_shape(field.attrs.default()); let flatten = field.attrs.flatten(); let transparent = field.attrs.transparent(); let ty = field.ty; - let value_shape = if skip || custom_deserializer { - quote!(::core::option::Option::None) + let wire_shape = if skip { + quote!(::serde_shape::FieldWireShape::Omitted) } else { - quote!(::core::option::Option::Some(<#ty as ::serde_shape::DeserializeShape>::deserialize_shape_in(context))) + let value_shape = if let Some(custom_deserializer) = field.attrs.deserialize_with() { + let detail = option_path(Some(custom_deserializer)); + quote! { + ::serde_shape::ShapeRef::Opaque(::serde_shape::OpaqueShape { + type_name: ::core::any::type_name::<#ty>(), + reason: ::serde_shape::OpaqueReason::CustomDeserializer, + detail: #detail, + }) + } + } else { + quote!(<#ty as ::serde_shape::DeserializeShape>::deserialize_shape_in(context)) + }; + + if transparent { + quote!(::serde_shape::FieldWireShape::Inline(#value_shape)) + } else if flatten { + quote!(::serde_shape::FieldWireShape::Flatten(#value_shape)) + } else { + quote!(::serde_shape::FieldWireShape::Value(#value_shape)) + } }; quote! { @@ -614,12 +668,8 @@ fn deserialize_field_shape(field: &ast::Field<'_>) -> TokenStream2 { member: #member, name: #name, aliases: #aliases, - value_shape: #value_shape, + wire_shape: #wire_shape, default: #default, - flatten: #flatten, - skip: #skip, - custom_deserializer: #custom_deserializer, - transparent: #transparent, } } } diff --git a/serde-shape/src/lib.rs b/serde-shape/src/lib.rs index 71abd42..579bc73 100644 --- a/serde-shape/src/lib.rs +++ b/serde-shape/src/lib.rs @@ -17,7 +17,7 @@ //! `serde-shape` builds a lightweight graph that describes what a Rust type emits through Serde //! serialization and accepts through Serde deserialization. It does not run Serde, and it is not a //! full validation schema. Instead, it gives tools access to the same structural information that -//! Serde derives from Rust types and `#[serde(...)]` attributes. +//! Serde derives from Rust types and `#[serde(...)]` attributes, including union value shapes. //! //! Common uses are generating configuration reference docs, deriving environment-variable maps //! from config structs, documenting wire formats, and checking whether two versions of a type @@ -151,8 +151,11 @@ //! follows the metadata Serde derives for each direction. //! //! A custom serializer or deserializer has no inferable inner shape, so the affected field or -//! variant is marked as custom and its nested shape is omitted. Whole-container conversion and +//! variant content is represented by an opaque boundary. Whole-container conversion and //! remote-derive attributes are represented as opaque definitions. +//! Field-level [`FieldWireShape`] distinguishes ordinary values from flattened fields, inline +//! transparent fields, and omitted fields. Custom serializer/deserializer boundaries use +//! [`ShapeRef::Opaque`] and remain composable with those field positions. //! //! # Manual implementations //! @@ -169,11 +172,14 @@ //! //! impl DeserializeShape for ByteSize { //! fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { -//! ShapeRef::String +//! ShapeRef::union([ShapeRef::String, ShapeRef::U64]) //! } //! } //! -//! assert_eq!(ByteSize::deserialize_shape().root, ShapeRef::String); +//! assert_eq!( +//! ByteSize::deserialize_shape().root, +//! ShapeRef::union([ShapeRef::String, ShapeRef::U64]) +//! ); //! ``` //! //! For recursive or shared named types, use [`SerializeShapeContext::define_named_type`] or @@ -191,6 +197,7 @@ extern crate std; use alloc::boxed::Box; use alloc::collections::BTreeMap; use alloc::vec::Vec; +use core::fmt; /// Private exports used by generated derive code. #[doc(hidden)] @@ -469,7 +476,8 @@ pub struct DeserializeTypeName { } /// A reference to a shape node. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[non_exhaustive] pub enum ShapeRef { /// Unit shape. Unit, @@ -529,6 +537,10 @@ pub enum ShapeRef { }, /// Tuple shape. Tuple(Vec), + /// A normalized union of two or more possible value shapes. + /// + /// Construct unions with [`ShapeRef::union`] or [`ShapeRef::try_union`]. + Union(UnionShape), /// Named type definition reference. Definition(ShapeId), /// Shape intentionally left opaque. @@ -536,35 +548,113 @@ pub enum ShapeRef { } impl ShapeRef { + /// Build a normalized union from one or more possible value shapes. + /// + /// Nested unions are flattened, duplicate alternatives are removed, and alternatives are + /// sorted into a canonical order. A single distinct alternative is returned directly. + /// + /// # Panics + /// + /// Panics when `alternatives` is empty. Use [`ShapeRef::try_union`] when the input may be + /// empty. + pub fn union(alternatives: I) -> Self + where + I: IntoIterator, + { + Self::try_union(alternatives).expect("shape union requires at least one alternative") + } + + /// Try to build a normalized union from possible value shapes. + /// + /// Returns `None` when `alternatives` is empty. Nested unions are flattened, duplicate + /// alternatives are removed, and alternatives are sorted into a canonical order. A single + /// distinct alternative is returned directly. + pub fn try_union(alternatives: I) -> Option + where + I: IntoIterator, + { + let mut normalized = Vec::new(); + for alternative in alternatives { + match alternative { + Self::Union(union) => normalized.extend(union.alternatives), + alternative => normalized.push(alternative), + } + } + let mut alternatives = normalized; + alternatives.sort(); + alternatives.dedup(); + + match alternatives.len() { + 0 => None, + 1 => alternatives.pop(), + _ => Some(Self::Union(UnionShape { alternatives })), + } + } + /// Return whether this is a signed integer shape. pub fn is_signed_integer(&self) -> bool { - matches!( - self, - Self::I8 | Self::I16 | Self::I32 | Self::I64 | Self::I128 | Self::Isize - ) + match self { + Self::I8 | Self::I16 | Self::I32 | Self::I64 | Self::I128 | Self::Isize => true, + Self::Union(union) => union.alternatives.iter().all(Self::is_signed_integer), + _ => false, + } } /// Return whether this is an unsigned integer shape. pub fn is_unsigned_integer(&self) -> bool { - matches!( - self, - Self::U8 | Self::U16 | Self::U32 | Self::U64 | Self::U128 | Self::Usize - ) + match self { + Self::U8 | Self::U16 | Self::U32 | Self::U64 | Self::U128 | Self::Usize => true, + Self::Union(union) => union.alternatives.iter().all(Self::is_unsigned_integer), + _ => false, + } } /// Return whether this is any integer shape. pub fn is_integer(&self) -> bool { - self.is_signed_integer() || self.is_unsigned_integer() + match self { + Self::Union(union) => union.alternatives.iter().all(Self::is_integer), + _ => self.is_signed_integer() || self.is_unsigned_integer(), + } } /// Return whether this is a floating point shape. pub fn is_float(&self) -> bool { - matches!(self, Self::F32 | Self::F64) + match self { + Self::F32 | Self::F64 => true, + Self::Union(union) => union.alternatives.iter().all(Self::is_float), + _ => false, + } } /// Return whether this is any numeric shape. pub fn is_number(&self) -> bool { - self.is_integer() || self.is_float() + match self { + Self::Union(union) => union.alternatives.iter().all(Self::is_number), + _ => self.is_integer() || self.is_float(), + } + } +} + +/// The normalized alternatives contained by [`ShapeRef::Union`]. +/// +/// A union always contains at least two distinct alternatives in canonical order. Use +/// [`ShapeRef::union`] or [`ShapeRef::try_union`] to construct one. Alternatives may overlap; a +/// union means that any alternative is possible, not that exactly one alternative must match. +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct UnionShape { + alternatives: Vec, +} + +impl UnionShape { + /// Return the canonical union alternatives. + pub fn alternatives(&self) -> &[ShapeRef] { + &self.alternatives + } +} + +impl fmt::Debug for UnionShape { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.alternatives.fmt(formatter) } } @@ -729,18 +819,10 @@ pub struct SerializeFieldShape { pub member: FieldMember, /// The primary Serde serialize name. pub name: &'static str, - /// The field output shape, or `None` when the field has no inferred output. - pub value_shape: Option, - /// Whether the field is flattened into the containing map. - pub flatten: bool, - /// Whether Serde skips this field during serialization. - pub skip: bool, + /// How this field contributes to the serialized wire shape. + pub wire_shape: FieldWireShape, /// The predicate used to skip this field during serialization. pub skip_if: Option<&'static str>, - /// Whether this field uses a custom serializer. - pub custom_serializer: bool, - /// Whether this is the transparent field of a transparent container. - pub transparent: bool, } /// Field-level deserialization metadata. @@ -752,18 +834,10 @@ pub struct DeserializeFieldShape { pub name: &'static str, /// All accepted Serde deserialize names, including the primary name. pub aliases: Vec<&'static str>, - /// The field input shape, or `None` when the field has no inferred input. - pub value_shape: Option, + /// How this field contributes to the deserialized wire shape. + pub wire_shape: FieldWireShape, /// The default used if this field is missing. pub default: DefaultShape, - /// Whether the field is flattened into the containing map. - pub flatten: bool, - /// Whether Serde skips this field during deserialization. - pub skip: bool, - /// Whether this field uses a custom deserializer. - pub custom_deserializer: bool, - /// Whether this is the transparent field of a transparent container. - pub transparent: bool, } /// The Rust member represented by a field. @@ -775,6 +849,20 @@ pub enum FieldMember { Unnamed(usize), } +/// How a field contributes to the wire representation in one Serde direction. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum FieldWireShape { + /// The field emits or accepts no value in this direction. + Omitted, + /// The field appears as a regular value at its Serde position. + Value(ShapeRef), + /// The field is flattened into the containing map. + Flatten(ShapeRef), + /// The field is serialized or deserialized directly at the containing type's position. + Inline(ShapeRef), +} + /// Variant-level serialization metadata. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SerializeVariantShape { @@ -784,16 +872,24 @@ pub struct SerializeVariantShape { pub name: &'static str, /// The variant field style. pub style: FieldsStyle, - /// The variant fields, if their output shape can be inferred. - pub fields: Vec, - /// Whether Serde skips this variant during serialization. - pub skip: bool, - /// Whether this variant uses a custom serializer. - pub custom_serializer: bool, + /// How the variant contributes its serialized content. + pub content: SerializeVariantContent, /// Whether this variant is individually marked untagged. pub untagged: bool, } +/// The serialized content controlled by an enum variant. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum SerializeVariantContent { + /// The variant is omitted during serialization. + Omitted, + /// Serde derives the variant content from these fields. + Fields(Vec), + /// A custom serializer controls the variant content. + Custom(OpaqueShape), +} + /// Variant-level deserialization metadata. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DeserializeVariantShape { @@ -805,18 +901,26 @@ pub struct DeserializeVariantShape { pub aliases: Vec<&'static str>, /// The variant field style. pub style: FieldsStyle, - /// The variant fields, if their input shape can be inferred. - pub fields: Vec, - /// Whether Serde skips this variant during deserialization. - pub skip: bool, - /// Whether this variant uses a custom deserializer. - pub custom_deserializer: bool, + /// How the variant contributes its deserialized content. + pub content: DeserializeVariantContent, /// Whether this is a Serde `other` catch-all variant. pub other: bool, /// Whether this variant is individually marked untagged. pub untagged: bool, } +/// The deserialized content controlled by an enum variant. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum DeserializeVariantContent { + /// The variant is omitted during deserialization. + Omitted, + /// Serde derives the variant content from these fields. + Fields(Vec), + /// A custom deserializer controls the variant content. + Custom(OpaqueShape), +} + /// A Serde default marker. #[derive(Clone, Debug, Eq, PartialEq)] pub enum DefaultShape { @@ -836,7 +940,7 @@ impl DefaultShape { } /// Shape intentionally left opaque. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct OpaqueShape { /// The Rust type or Serde item that is opaque. pub type_name: &'static str, @@ -847,7 +951,7 @@ pub struct OpaqueShape { } /// Reason a shape cannot be represented precisely. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum OpaqueReason { /// The type uses `#[serde(from = "...")]`. FromType, diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index a980814..fd743e5 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -38,6 +38,38 @@ fn classifies_flat_numeric_shapes() { assert!(!ShapeRef::String.is_number()); } +#[test] +fn classifies_union_numeric_shapes() { + assert!(ShapeRef::union([ShapeRef::I8, ShapeRef::U64]).is_integer()); + assert!(ShapeRef::union([ShapeRef::F32, ShapeRef::F64]).is_float()); + assert!(ShapeRef::union([ShapeRef::I16, ShapeRef::F64]).is_number()); + assert!(!ShapeRef::union([ShapeRef::String, ShapeRef::U64]).is_integer()); +} + +#[test] +fn normalizes_union_shapes() { + assert_eq!(ShapeRef::try_union([]), None); + assert_eq!(ShapeRef::union([ShapeRef::String]), ShapeRef::String); + assert_eq!( + ShapeRef::union([ShapeRef::String, ShapeRef::I8]), + ShapeRef::union([ShapeRef::I8, ShapeRef::String]) + ); + + let union = ShapeRef::union([ + ShapeRef::String, + ShapeRef::I8, + ShapeRef::union([ShapeRef::U64, ShapeRef::String]), + ShapeRef::I8, + ]); + let ShapeRef::Union(union) = union else { + panic!("multiple distinct alternatives should produce a union"); + }; + assert_eq!( + union.alternatives(), + &[ShapeRef::I8, ShapeRef::U64, ShapeRef::String] + ); +} + #[cfg(target_has_atomic = "ptr")] #[test] fn maps_atomic_shapes() { diff --git a/tests/derive/Cargo.toml b/tests/derive/Cargo.toml index 0b9977f..8cef4bc 100644 --- a/tests/derive/Cargo.toml +++ b/tests/derive/Cargo.toml @@ -27,6 +27,8 @@ serde-shape = { workspace = true, features = ["derive"] } [dev-dependencies] insta = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } [lints] workspace = true diff --git a/tests/derive/tests/derive.rs b/tests/derive/tests/derive.rs index dd7720f..cce38a3 100644 --- a/tests/derive/tests/derive.rs +++ b/tests/derive/tests/derive.rs @@ -16,9 +16,14 @@ use serde_shape::DeserializeDefinitionKind; use serde_shape::DeserializeShape; +use serde_shape::DeserializeVariantContent; use serde_shape::FieldMember; +use serde_shape::FieldWireShape; +use serde_shape::OpaqueReason; use serde_shape::SerializeDefinitionKind; use serde_shape::SerializeShape; +use serde_shape::SerializeVariantContent; +use serde_shape::ShapeRef; #[derive(DeserializeShape)] #[serde( @@ -199,19 +204,16 @@ fn exposes_deserialize_field_metadata() { assert_eq!(id.member, FieldMember::Named("id")); assert_eq!(id.name, "in-id"); assert_eq!(id.aliases, vec!["in-id", "legacy-id"]); - assert!(!id.skip); - assert!(!id.custom_deserializer); + assert!(matches!(&id.wire_shape, FieldWireShape::Value(_))); assert_eq!(maybe.name, "maybe"); - assert!(!maybe.skip); + assert!(matches!(&maybe.wire_shape, FieldWireShape::Value(_))); assert_eq!(secret.name, "secret-in"); - assert!(!secret.skip); + assert!(matches!(&secret.wire_shape, FieldWireShape::Value(_))); assert_eq!(output_only.name, "only-in"); - assert!(output_only.skip); - assert!(!output_only.custom_deserializer); - assert_eq!(output_only.value_shape, None); + assert_eq!(output_only.wire_shape, FieldWireShape::Omitted); } #[test] @@ -233,21 +235,21 @@ fn exposes_serialize_field_metadata() { assert_eq!(id.member, FieldMember::Named("id")); assert_eq!(id.name, "out-id"); - assert!(!id.skip); assert_eq!(id.skip_if, None); - assert!(!id.custom_serializer); + assert!(matches!(&id.wire_shape, FieldWireShape::Value(_))); assert_eq!(maybe.name, "maybe"); assert_eq!(maybe.skip_if, Some("is_missing")); assert_eq!(secret.name, "secret"); - assert!(secret.skip); - assert_eq!(secret.value_shape, None); + assert_eq!(secret.wire_shape, FieldWireShape::Omitted); assert_eq!(output_only.name, "only-out"); - assert!(!output_only.skip); - assert!(output_only.custom_serializer); - assert_eq!(output_only.value_shape, None); + let FieldWireShape::Value(ShapeRef::Opaque(opaque)) = &output_only.wire_shape else { + panic!("custom serialized field should expose an opaque wire shape"); + }; + assert_eq!(opaque.reason, OpaqueReason::CustomSerializer); + assert_eq!(opaque.detail, Some("serialize_not_shape")); } #[test] @@ -267,9 +269,11 @@ fn exposes_deserialize_variant_metadata() { assert_eq!(struct_variant.rust_name, "StructVariant"); assert_eq!(struct_variant.name, "struct-variant"); - assert!(!struct_variant.skip); - let [field] = struct_variant.fields.as_slice() else { + let DeserializeVariantContent::Fields(fields) = &struct_variant.content else { + panic!("struct variant should expose derived fields"); + }; + let [field] = fields.as_slice() else { panic!("struct variant should expose its field"); }; assert_eq!(field.name, "field_name"); @@ -278,12 +282,13 @@ fn exposes_deserialize_variant_metadata() { assert_eq!(renamed.aliases, vec!["deserialized", "legacy"]); assert_eq!(input_only.name, "input-only"); - assert!(!input_only.skip); + assert!(matches!( + &input_only.content, + DeserializeVariantContent::Fields(_) + )); assert_eq!(output_only.name, "output-only"); - assert!(output_only.skip); - assert!(!output_only.custom_deserializer); - assert!(output_only.fields.is_empty()); + assert_eq!(output_only.content, DeserializeVariantContent::Omitted); } #[test] @@ -303,9 +308,11 @@ fn exposes_serialize_variant_metadata() { assert_eq!(struct_variant.rust_name, "StructVariant"); assert_eq!(struct_variant.name, "STRUCT_VARIANT"); - assert!(!struct_variant.skip); - let [field] = struct_variant.fields.as_slice() else { + let SerializeVariantContent::Fields(fields) = &struct_variant.content else { + panic!("struct variant should expose derived fields"); + }; + let [field] = fields.as_slice() else { panic!("struct variant should expose its field"); }; assert_eq!(field.name, "fieldName"); @@ -313,12 +320,14 @@ fn exposes_serialize_variant_metadata() { assert_eq!(renamed.name, "SERIALIZED"); assert_eq!(input_only.name, "INPUT_ONLY"); - assert!(input_only.skip); + assert_eq!(input_only.content, SerializeVariantContent::Omitted); assert_eq!(output_only.name, "OUTPUT_ONLY"); - assert!(!output_only.skip); - assert!(output_only.custom_serializer); - assert!(output_only.fields.is_empty()); + let SerializeVariantContent::Custom(opaque) = &output_only.content else { + panic!("custom serialized variant should expose opaque content"); + }; + assert_eq!(opaque.reason, OpaqueReason::CustomSerializer); + assert_eq!(opaque.detail, Some("serialize_variant")); } #[test] diff --git a/tests/derive/tests/serde_compat.rs b/tests/derive/tests/serde_compat.rs new file mode 100644 index 0000000..3783685 --- /dev/null +++ b/tests/derive/tests/serde_compat.rs @@ -0,0 +1,277 @@ +// 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 serde::Deserialize; +use serde::Serialize; +use serde_shape::DeserializeDefinitionKind; +use serde_shape::DeserializeFieldShape; +use serde_shape::DeserializeShape; +use serde_shape::DeserializeVariantContent; +use serde_shape::DeserializeVariantShape; +use serde_shape::FieldWireShape; +use serde_shape::OpaqueReason; +use serde_shape::SerializeDefinitionKind; +use serde_shape::SerializeFieldShape; +use serde_shape::SerializeShape; +use serde_shape::SerializeVariantContent; +use serde_shape::SerializeVariantShape; +use serde_shape::ShapeRef; + +#[derive(Debug, PartialEq)] +struct FlatValue(u64); + +#[derive(Debug, PartialEq, Serialize, Deserialize, SerializeShape, DeserializeShape)] +struct FlattenedCustom { + #[serde(flatten, with = "flat_value")] + value: FlatValue, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, SerializeShape, DeserializeShape)] +#[serde(transparent)] +struct TransparentValue { + value: u64, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, SerializeShape, DeserializeShape)] +#[serde(transparent)] +struct TransparentCustom { + #[serde(with = "stringified")] + value: u64, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, SerializeShape, DeserializeShape)] +enum CustomVariant { + #[serde(with = "stringified")] + Value(u64), +} + +#[test] +fn composes_flatten_with_custom_field_boundaries() { + let value = FlattenedCustom { + value: FlatValue(7), + }; + let json = serde_json::to_value(&value).expect("value should serialize"); + assert_eq!(json, serde_json::json!({ "custom": 7 })); + assert_eq!( + serde_json::from_value::(json).expect("value should deserialize"), + value + ); + + let serialize_field = first_serialize_field::(); + let FieldWireShape::Flatten(ShapeRef::Opaque(opaque)) = serialize_field.wire_shape else { + panic!("custom serialized flatten field should be flattened and opaque"); + }; + assert_eq!(opaque.reason, OpaqueReason::CustomSerializer); + assert_eq!(opaque.detail, Some("flat_value::serialize")); + + let deserialize_field = first_deserialize_field::(); + let FieldWireShape::Flatten(ShapeRef::Opaque(opaque)) = deserialize_field.wire_shape else { + panic!("custom deserialized flatten field should be flattened and opaque"); + }; + assert_eq!(opaque.reason, OpaqueReason::CustomDeserializer); + assert_eq!(opaque.detail, Some("flat_value::deserialize")); +} + +#[test] +fn marks_named_transparent_fields_as_inline() { + let value = TransparentValue { value: 11 }; + let json = serde_json::to_value(&value).expect("value should serialize"); + assert_eq!(json, serde_json::json!(11)); + assert_eq!( + serde_json::from_value::(json).expect("value should deserialize"), + value + ); + + assert_eq!( + first_serialize_field::().wire_shape, + FieldWireShape::Inline(ShapeRef::U64) + ); + assert_eq!( + first_deserialize_field::().wire_shape, + FieldWireShape::Inline(ShapeRef::U64) + ); +} + +#[test] +fn composes_transparent_with_custom_field_boundaries() { + let value = TransparentCustom { value: 13 }; + let json = serde_json::to_value(&value).expect("value should serialize"); + assert_eq!(json, serde_json::json!("13")); + assert_eq!( + serde_json::from_value::(json).expect("value should deserialize"), + value + ); + + let serialize_field = first_serialize_field::(); + let FieldWireShape::Inline(ShapeRef::Opaque(opaque)) = serialize_field.wire_shape else { + panic!("custom serialized transparent field should be inline and opaque"); + }; + assert_eq!(opaque.reason, OpaqueReason::CustomSerializer); + assert_eq!(opaque.detail, Some("stringified::serialize")); + + let deserialize_field = first_deserialize_field::(); + let FieldWireShape::Inline(ShapeRef::Opaque(opaque)) = deserialize_field.wire_shape else { + panic!("custom deserialized transparent field should be inline and opaque"); + }; + assert_eq!(opaque.reason, OpaqueReason::CustomDeserializer); + assert_eq!(opaque.detail, Some("stringified::deserialize")); +} + +#[test] +fn retains_custom_variant_boundary_details() { + let value = CustomVariant::Value(17); + let json = serde_json::to_value(&value).expect("value should serialize"); + assert_eq!(json, serde_json::json!({ "Value": "17" })); + assert_eq!( + serde_json::from_value::(json).expect("value should deserialize"), + value + ); + + let serialize_variant = first_serialize_variant::(); + let SerializeVariantContent::Custom(opaque) = serialize_variant.content else { + panic!("custom serialized variant should expose opaque content"); + }; + assert_eq!(opaque.reason, OpaqueReason::CustomSerializer); + assert_eq!(opaque.detail, Some("stringified::serialize")); + + let deserialize_variant = first_deserialize_variant::(); + let DeserializeVariantContent::Custom(opaque) = deserialize_variant.content else { + panic!("custom deserialized variant should expose opaque content"); + }; + assert_eq!(opaque.reason, OpaqueReason::CustomDeserializer); + assert_eq!(opaque.detail, Some("stringified::deserialize")); +} + +fn first_serialize_field() -> SerializeFieldShape +where + T: SerializeShape, +{ + let graph = T::serialize_shape(); + let ShapeRef::Definition(id) = graph.root else { + panic!("root shape should be a definition"); + }; + let definition = graph.definition(id).expect("definition should exist"); + let SerializeDefinitionKind::Struct(shape) = &definition.kind else { + panic!("definition should be a struct"); + }; + shape.fields.first().expect("field should exist").clone() +} + +fn first_deserialize_field() -> DeserializeFieldShape +where + T: DeserializeShape, +{ + let graph = T::deserialize_shape(); + let ShapeRef::Definition(id) = graph.root else { + panic!("root shape should be a definition"); + }; + let definition = graph.definition(id).expect("definition should exist"); + let DeserializeDefinitionKind::Struct(shape) = &definition.kind else { + panic!("definition should be a struct"); + }; + shape.fields.first().expect("field should exist").clone() +} + +fn first_serialize_variant() -> SerializeVariantShape +where + T: SerializeShape, +{ + let graph = T::serialize_shape(); + let ShapeRef::Definition(id) = graph.root else { + panic!("root shape should be a definition"); + }; + let definition = graph.definition(id).expect("definition should exist"); + let SerializeDefinitionKind::Enum(shape) = &definition.kind else { + panic!("definition should be an enum"); + }; + shape + .variants + .first() + .expect("variant should exist") + .clone() +} + +fn first_deserialize_variant() -> DeserializeVariantShape +where + T: DeserializeShape, +{ + let graph = T::deserialize_shape(); + let ShapeRef::Definition(id) = graph.root else { + panic!("root shape should be a definition"); + }; + let definition = graph.definition(id).expect("definition should exist"); + let DeserializeDefinitionKind::Enum(shape) = &definition.kind else { + panic!("definition should be an enum"); + }; + shape + .variants + .first() + .expect("variant should exist") + .clone() +} + +mod flat_value { + use std::collections::BTreeMap; + + use serde::Deserialize; + use serde::Deserializer; + use serde::Serializer; + use serde::de::Error as _; + use serde::ser::SerializeMap; + + use super::FlatValue; + + pub fn serialize(value: &FlatValue, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("custom", &value.0)?; + map.end() + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut values = BTreeMap::::deserialize(deserializer)?; + values + .remove("custom") + .map(FlatValue) + .ok_or_else(|| D::Error::missing_field("custom")) + } +} + +mod stringified { + use serde::Deserialize; + use serde::Deserializer; + use serde::Serializer; + use serde::de::Error as _; + + pub fn serialize(value: &u64, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&value.to_string()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(D::Error::custom) + } +} diff --git a/tests/derive/tests/snapshots/derive__snapshots_internally_tagged_enum_shape_from_variant_attrs.snap b/tests/derive/tests/snapshots/derive__snapshots_internally_tagged_enum_shape_from_variant_attrs.snap index fff525b..46c821a 100644 --- a/tests/derive/tests/snapshots/derive__snapshots_internally_tagged_enum_shape_from_variant_attrs.snap +++ b/tests/derive/tests/snapshots/derive__snapshots_internally_tagged_enum_shape_from_variant_attrs.snap @@ -31,27 +31,23 @@ DeserializeShapeGraph { "s3-compatible", ], style: Struct, - fields: [ - DeserializeFieldShape { - member: Named( - "bucket_name", - ), - name: "bucket-name", - aliases: [ - "bucket-name", - ], - value_shape: Some( - String, - ), - default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, - }, - ], - skip: false, - custom_deserializer: false, + content: Fields( + [ + DeserializeFieldShape { + member: Named( + "bucket_name", + ), + name: "bucket-name", + aliases: [ + "bucket-name", + ], + wire_shape: Value( + String, + ), + default: None, + }, + ], + ), other: false, untagged: false, }, @@ -62,27 +58,23 @@ DeserializeShapeGraph { "az-blob", ], style: Struct, - fields: [ - DeserializeFieldShape { - member: Named( - "container_name", - ), - name: "container-name", - aliases: [ - "container-name", - ], - value_shape: Some( - String, - ), - default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, - }, - ], - skip: false, - custom_deserializer: false, + content: Fields( + [ + DeserializeFieldShape { + member: Named( + "container_name", + ), + name: "container-name", + aliases: [ + "container-name", + ], + wire_shape: Value( + String, + ), + default: None, + }, + ], + ), other: false, untagged: false, }, @@ -93,9 +85,9 @@ DeserializeShapeGraph { "other", ], style: Unit, - fields: [], - skip: false, - custom_deserializer: false, + content: Fields( + [], + ), other: true, untagged: false, }, diff --git a/tests/derive/tests/snapshots/derive__snapshots_phantom_data_generic_field_without_shape_bound.snap b/tests/derive/tests/snapshots/derive__snapshots_phantom_data_generic_field_without_shape_bound.snap index 120f284..174416f 100644 --- a/tests/derive/tests/snapshots/derive__snapshots_phantom_data_generic_field_without_shape_bound.snap +++ b/tests/derive/tests/snapshots/derive__snapshots_phantom_data_generic_field_without_shape_bound.snap @@ -29,14 +29,10 @@ DeserializeShapeGraph { aliases: [ "marker", ], - value_shape: Some( + wire_shape: Value( Unit, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { diff --git a/tests/derive/tests/snapshots/derive__snapshots_recursive_type_reusing_the_same_definition.snap b/tests/derive/tests/snapshots/derive__snapshots_recursive_type_reusing_the_same_definition.snap index fd0718a..02a8c0e 100644 --- a/tests/derive/tests/snapshots/derive__snapshots_recursive_type_reusing_the_same_definition.snap +++ b/tests/derive/tests/snapshots/derive__snapshots_recursive_type_reusing_the_same_definition.snap @@ -29,7 +29,7 @@ DeserializeShapeGraph { aliases: [ "child", ], - value_shape: Some( + wire_shape: Value( Option( Definition( ShapeId( @@ -39,10 +39,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { diff --git a/tests/derive/tests/snapshots/derive__snapshots_skipped_generic_field_without_shape_bound.snap b/tests/derive/tests/snapshots/derive__snapshots_skipped_generic_field_without_shape_bound.snap index eb80108..e0f81c4 100644 --- a/tests/derive/tests/snapshots/derive__snapshots_skipped_generic_field_without_shape_bound.snap +++ b/tests/derive/tests/snapshots/derive__snapshots_skipped_generic_field_without_shape_bound.snap @@ -29,12 +29,8 @@ DeserializeShapeGraph { aliases: [ "value", ], - value_shape: None, + wire_shape: Omitted, default: Default, - flatten: false, - skip: true, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { diff --git a/tests/derive/tests/snapshots/derive__snapshots_struct_shape_from_container_and_field_attrs.snap b/tests/derive/tests/snapshots/derive__snapshots_struct_shape_from_container_and_field_attrs.snap index 7300e64..e2e80a5 100644 --- a/tests/derive/tests/snapshots/derive__snapshots_struct_shape_from_container_and_field_attrs.snap +++ b/tests/derive/tests/snapshots/derive__snapshots_struct_shape_from_container_and_field_attrs.snap @@ -29,14 +29,10 @@ DeserializeShapeGraph { aliases: [ "http-port", ], - value_shape: Some( + wire_shape: Value( U16, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -47,16 +43,12 @@ DeserializeShapeGraph { "api-url", "endpoint", ], - value_shape: Some( + wire_shape: Value( Option( String, ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -66,16 +58,12 @@ DeserializeShapeGraph { aliases: [ "retries", ], - value_shape: Some( + wire_shape: Value( U8, ), default: Path( "default_retries", ), - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -85,7 +73,7 @@ DeserializeShapeGraph { aliases: [ "storage", ], - value_shape: Some( + wire_shape: Flatten( Definition( ShapeId( 1, @@ -93,10 +81,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: true, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -106,12 +90,8 @@ DeserializeShapeGraph { aliases: [ "skipped", ], - value_shape: None, + wire_shape: Omitted, default: None, - flatten: false, - skip: true, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -121,12 +101,18 @@ DeserializeShapeGraph { aliases: [ "secret", ], - value_shape: None, + wire_shape: Value( + Opaque( + OpaqueShape { + type_name: "derive::NotShape", + reason: CustomDeserializer, + detail: Some( + "custom_secret", + ), + }, + ), + ), default: None, - flatten: false, - skip: false, - custom_deserializer: true, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -165,27 +151,23 @@ DeserializeShapeGraph { "s3-compatible", ], style: Struct, - fields: [ - DeserializeFieldShape { - member: Named( - "bucket_name", - ), - name: "bucket-name", - aliases: [ - "bucket-name", - ], - value_shape: Some( - String, - ), - default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, - }, - ], - skip: false, - custom_deserializer: false, + content: Fields( + [ + DeserializeFieldShape { + member: Named( + "bucket_name", + ), + name: "bucket-name", + aliases: [ + "bucket-name", + ], + wire_shape: Value( + String, + ), + default: None, + }, + ], + ), other: false, untagged: false, }, @@ -196,27 +178,23 @@ DeserializeShapeGraph { "az-blob", ], style: Struct, - fields: [ - DeserializeFieldShape { - member: Named( - "container_name", - ), - name: "container-name", - aliases: [ - "container-name", - ], - value_shape: Some( - String, - ), - default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, - }, - ], - skip: false, - custom_deserializer: false, + content: Fields( + [ + DeserializeFieldShape { + member: Named( + "container_name", + ), + name: "container-name", + aliases: [ + "container-name", + ], + wire_shape: Value( + String, + ), + default: None, + }, + ], + ), other: false, untagged: false, }, @@ -227,9 +205,9 @@ DeserializeShapeGraph { "other", ], style: Unit, - fields: [], - skip: false, - custom_deserializer: false, + content: Fields( + [], + ), other: true, untagged: false, }, diff --git a/tests/derive/tests/snapshots/derive__snapshots_transparent_struct_shape.snap b/tests/derive/tests/snapshots/derive__snapshots_transparent_struct_shape.snap index 7477017..8dcc77b 100644 --- a/tests/derive/tests/snapshots/derive__snapshots_transparent_struct_shape.snap +++ b/tests/derive/tests/snapshots/derive__snapshots_transparent_struct_shape.snap @@ -29,14 +29,10 @@ DeserializeShapeGraph { aliases: [ "0", ], - value_shape: Some( + wire_shape: Inline( U64, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: true, }, ], attributes: DeserializeContainerAttributes { diff --git a/tests/integration/tests/env_vars.rs b/tests/integration/tests/env_vars.rs index 066955d..948d5f5 100644 --- a/tests/integration/tests/env_vars.rs +++ b/tests/integration/tests/env_vars.rs @@ -25,10 +25,13 @@ use serde_shape::DeserializeShape; use serde_shape::DeserializeShapeContext; use serde_shape::DeserializeShapeGraph; use serde_shape::DeserializeStructShape; +use serde_shape::DeserializeVariantContent; +use serde_shape::FieldWireShape; use serde_shape::FieldsStyle; use serde_shape::ShapeId; use serde_shape::ShapeRef; use serde_shape::Tagging; +use serde_shape::UnionShape; #[derive(Clone, Debug, Eq, PartialEq)] struct EnvOption { @@ -179,7 +182,7 @@ struct ByteSize(u64); impl DeserializeShape for ByteSize { fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { - ShapeRef::String + string_or_integer_shape() } } @@ -188,10 +191,28 @@ struct HumanDuration(u64); impl DeserializeShape for HumanDuration { fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { - ShapeRef::String + string_or_integer_shape() } } +fn string_or_integer_shape() -> ShapeRef { + ShapeRef::union([ + ShapeRef::String, + ShapeRef::I8, + ShapeRef::I16, + ShapeRef::I32, + ShapeRef::I64, + ShapeRef::I128, + ShapeRef::Isize, + ShapeRef::U8, + ShapeRef::U16, + ShapeRef::U32, + ShapeRef::U64, + ShapeRef::U128, + ShapeRef::Usize, + ]) +} + fn default_dir() -> PathBuf { PathBuf::from("/var/lib/percas") } @@ -255,6 +276,10 @@ impl EnvCollector<'_> { ShapeRef::Option(inner) => { self.visit_shape_ref(inner, path, true, condition); } + ShapeRef::Union(union) => { + let value_kind = self.union_kind(union); + self.push_leaf(path, &value_kind, optional, condition); + } ShapeRef::Definition(id) => { self.visit_definition(*id, path, optional, condition); } @@ -317,23 +342,23 @@ impl EnvCollector<'_> { match shape.style { FieldsStyle::Struct => { for field in &shape.fields { - let Some(field_shape) = &field.value_shape else { - continue; - }; let field_optional = optional || !field.default.is_none(); - if field.flatten { - self.visit_shape_ref(field_shape, path, field_optional, condition.clone()); - } else { - path.push(field.name.to_owned()); - self.visit_shape_ref(field_shape, path, field_optional, condition.clone()); - path.pop(); - } + self.visit_field_wire_shape( + field.name, + &field.wire_shape, + path, + field_optional, + condition.clone(), + ); } } FieldsStyle::Newtype if shape.fields.len() == 1 => { - if let Some(field_shape) = &shape.fields[0].value_shape { - self.visit_shape_ref(field_shape, path, optional, condition); - } + self.visit_newtype_wire_shape( + &shape.fields[0].wire_shape, + path, + optional, + condition, + ); } FieldsStyle::Tuple | FieldsStyle::Newtype | FieldsStyle::Unit => { self.push_leaf(path, "object", optional, condition); @@ -351,7 +376,7 @@ impl EnvCollector<'_> { let variants = shape .variants .iter() - .filter(|variant| !variant.skip) + .filter(|variant| !matches!(&variant.content, DeserializeVariantContent::Omitted)) .map(|variant| variant.name) .collect::>(); @@ -379,23 +404,36 @@ impl EnvCollector<'_> { ); for variant in &shape.variants { - if variant.skip { - continue; - } - let variant_condition = format!("{}={}", tag_path.join("."), variant.name); let variant_condition = Some(merge_conditions( condition.as_deref(), variant_condition.as_str(), )); - for field in &variant.fields { - let Some(field_shape) = &field.value_shape else { - continue; - }; - path.push(field.name.to_owned()); - self.visit_shape_ref(field_shape, path, optional, variant_condition.clone()); - path.pop(); + match &variant.content { + DeserializeVariantContent::Omitted => {} + DeserializeVariantContent::Fields(fields) => { + for field in fields { + self.visit_field_wire_shape( + field.name, + &field.wire_shape, + path, + optional, + variant_condition.clone(), + ); + } + } + DeserializeVariantContent::Custom(opaque) => { + self.push_leaf( + path, + &format!("opaque({:?})", opaque.reason), + optional, + variant_condition, + ); + } + _ => { + self.push_leaf(path, "unsupported", optional, variant_condition); + } } } return; @@ -409,6 +447,126 @@ impl EnvCollector<'_> { ); } + fn visit_field_wire_shape( + &mut self, + field_name: &str, + wire_shape: &FieldWireShape, + path: &mut Vec, + optional: bool, + condition: Option, + ) { + match wire_shape { + FieldWireShape::Omitted => {} + FieldWireShape::Value(shape_ref) => { + path.push(field_name.to_owned()); + self.visit_shape_ref(shape_ref, path, optional, condition); + path.pop(); + } + FieldWireShape::Flatten(shape_ref) => { + self.visit_shape_ref(shape_ref, path, optional, condition); + } + FieldWireShape::Inline(shape_ref) => { + self.visit_shape_ref(shape_ref, path, optional, condition); + } + _ => { + path.push(field_name.to_owned()); + self.push_leaf(path, "unsupported", optional, condition); + path.pop(); + } + } + } + + fn visit_newtype_wire_shape( + &mut self, + wire_shape: &FieldWireShape, + path: &mut Vec, + optional: bool, + condition: Option, + ) { + match wire_shape { + FieldWireShape::Omitted => {} + FieldWireShape::Value(shape_ref) + | FieldWireShape::Flatten(shape_ref) + | FieldWireShape::Inline(shape_ref) => { + self.visit_shape_ref(shape_ref, path, optional, condition); + } + _ => { + self.push_leaf(path, "unsupported", optional, condition); + } + } + } + + fn union_kind(&self, union: &UnionShape) -> String { + let alternatives = union.alternatives(); + if alternatives.iter().all(ShapeRef::is_integer) { + return "integer".to_owned(); + } + if alternatives.iter().all(ShapeRef::is_float) { + return "float".to_owned(); + } + if alternatives.iter().all(ShapeRef::is_number) { + return "number".to_owned(); + } + + alternatives + .iter() + .fold(Vec::::new(), |mut kinds, alternative| { + let kind = self.union_alternative_kind(alternative); + if !kinds.contains(&kind) { + kinds.push(kind); + } + kinds + }) + .join("|") + } + + fn union_alternative_kind(&self, shape_ref: &ShapeRef) -> String { + match shape_ref { + ShapeRef::Option(inner) => self.union_alternative_kind(inner), + ShapeRef::Seq(_) | ShapeRef::Array { .. } | ShapeRef::Tuple(_) => "array".to_owned(), + ShapeRef::Map { .. } => "object".to_owned(), + ShapeRef::Union(union) => self.union_kind(union), + ShapeRef::Definition(id) => { + let definition = self.shape.definition(*id).expect("shape definition exists"); + match &definition.kind { + DeserializeDefinitionKind::Struct(shape) if shape.attributes.transparent => { + shape + .fields + .iter() + .find_map(|field| match &field.wire_shape { + FieldWireShape::Inline(inner) => { + Some(self.union_alternative_kind(inner)) + } + FieldWireShape::Omitted + | FieldWireShape::Value(_) + | FieldWireShape::Flatten(_) => None, + _ => None, + }) + .unwrap_or_else(|| "unit".to_owned()) + } + DeserializeDefinitionKind::Struct(shape) + if shape.style == FieldsStyle::Newtype && shape.fields.len() == 1 => + { + match &shape.fields[0].wire_shape { + FieldWireShape::Omitted => "unit".to_owned(), + FieldWireShape::Value(inner) + | FieldWireShape::Flatten(inner) + | FieldWireShape::Inline(inner) => self.union_alternative_kind(inner), + _ => "unknown".to_owned(), + } + } + DeserializeDefinitionKind::Struct(_) => "object".to_owned(), + DeserializeDefinitionKind::Enum(_) => "enum".to_owned(), + DeserializeDefinitionKind::Opaque(opaque) => { + format!("opaque({:?})", opaque.reason) + } + } + } + ShapeRef::Opaque(opaque) => format!("opaque({:?})", opaque.reason), + shape_ref => primitive_kind(shape_ref).to_owned(), + } + } + fn push_leaf( &mut self, path: &[String], @@ -448,6 +606,8 @@ fn primitive_kind(shape_ref: &ShapeRef) -> &'static str { "integer" } else if shape_ref.is_float() { "float" + } else if shape_ref.is_number() { + "number" } else { match shape_ref { ShapeRef::Unit => "unit", @@ -458,6 +618,7 @@ fn primitive_kind(shape_ref: &ShapeRef) -> &'static str { | ShapeRef::Array { .. } | ShapeRef::Map { .. } | ShapeRef::Tuple(_) + | ShapeRef::Union(_) | ShapeRef::Definition(_) | ShapeRef::Opaque(_) => { unreachable!("compound shapes are handled before leaf mapping") diff --git a/tests/integration/tests/snapshots/env_vars__snapshots_config_shape.snap b/tests/integration/tests/snapshots/env_vars__snapshots_config_shape.snap index 3f13525..c13d4b8 100644 --- a/tests/integration/tests/snapshots/env_vars__snapshots_config_shape.snap +++ b/tests/integration/tests/snapshots/env_vars__snapshots_config_shape.snap @@ -29,7 +29,7 @@ DeserializeShapeGraph { aliases: [ "server", ], - value_shape: Some( + wire_shape: Value( Definition( ShapeId( 1, @@ -37,10 +37,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -50,7 +46,7 @@ DeserializeShapeGraph { aliases: [ "storage", ], - value_shape: Some( + wire_shape: Value( Definition( ShapeId( 2, @@ -58,10 +54,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -71,7 +63,7 @@ DeserializeShapeGraph { aliases: [ "telemetry", ], - value_shape: Some( + wire_shape: Value( Definition( ShapeId( 7, @@ -79,10 +71,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -117,16 +105,12 @@ DeserializeShapeGraph { aliases: [ "dir", ], - value_shape: Some( + wire_shape: Value( String, ), default: Path( "default_dir", ), - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -136,16 +120,12 @@ DeserializeShapeGraph { aliases: [ "listen_data_addr", ], - value_shape: Some( + wire_shape: Value( String, ), default: Path( "default_listen_data_addr", ), - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -155,16 +135,12 @@ DeserializeShapeGraph { aliases: [ "advertise_data_addr", ], - value_shape: Some( + wire_shape: Value( Option( String, ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -174,16 +150,12 @@ DeserializeShapeGraph { aliases: [ "initial_peers", ], - value_shape: Some( + wire_shape: Value( Seq( String, ), ), default: Default, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -193,16 +165,12 @@ DeserializeShapeGraph { aliases: [ "cluster_id", ], - value_shape: Some( + wire_shape: Value( String, ), default: Path( "default_cluster_id", ), - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -237,7 +205,7 @@ DeserializeShapeGraph { aliases: [ "backend", ], - value_shape: Some( + wire_shape: Value( Definition( ShapeId( 3, @@ -245,10 +213,6 @@ DeserializeShapeGraph { ), ), default: Default, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -258,16 +222,28 @@ DeserializeShapeGraph { aliases: [ "disk_capacity", ], - value_shape: Some( - String, + wire_shape: Value( + Union( + [ + I8, + I16, + I32, + I64, + I128, + Isize, + U8, + U16, + U32, + U64, + U128, + Usize, + String, + ], + ), ), default: Path( "default_disk_capacity", ), - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -277,16 +253,28 @@ DeserializeShapeGraph { aliases: [ "memory_capacity", ], - value_shape: Some( - String, + wire_shape: Value( + Union( + [ + I8, + I16, + I32, + I64, + I128, + Isize, + U8, + U16, + U32, + U64, + U128, + Usize, + String, + ], + ), ), default: Path( "default_memory_capacity", ), - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -296,7 +284,7 @@ DeserializeShapeGraph { aliases: [ "disk_throttle", ], - value_shape: Some( + wire_shape: Value( Option( Definition( ShapeId( @@ -306,10 +294,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -345,27 +329,23 @@ DeserializeShapeGraph { "local", ], style: Struct, - fields: [ - DeserializeFieldShape { - member: Named( - "data_dir", - ), - name: "data_dir", - aliases: [ - "data_dir", - ], - value_shape: Some( - String, - ), - default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, - }, - ], - skip: false, - custom_deserializer: false, + content: Fields( + [ + DeserializeFieldShape { + member: Named( + "data_dir", + ), + name: "data_dir", + aliases: [ + "data_dir", + ], + wire_shape: Value( + String, + ), + default: None, + }, + ], + ), other: false, untagged: false, }, @@ -376,44 +356,36 @@ DeserializeShapeGraph { "s3", ], style: Struct, - fields: [ - DeserializeFieldShape { - member: Named( - "bucket", - ), - name: "bucket", - aliases: [ - "bucket", - ], - value_shape: Some( - String, - ), - default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, - }, - DeserializeFieldShape { - member: Named( - "region", - ), - name: "region", - aliases: [ - "region", - ], - value_shape: Some( - String, - ), - default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, - }, - ], - skip: false, - custom_deserializer: false, + content: Fields( + [ + DeserializeFieldShape { + member: Named( + "bucket", + ), + name: "bucket", + aliases: [ + "bucket", + ], + wire_shape: Value( + String, + ), + default: None, + }, + DeserializeFieldShape { + member: Named( + "region", + ), + name: "region", + aliases: [ + "region", + ], + wire_shape: Value( + String, + ), + default: None, + }, + ], + ), other: false, untagged: false, }, @@ -452,14 +424,10 @@ DeserializeShapeGraph { aliases: [ "read_iops", ], - value_shape: Some( + wire_shape: Value( U64, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -469,14 +437,10 @@ DeserializeShapeGraph { aliases: [ "write_iops", ], - value_shape: Some( + wire_shape: Value( U64, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -486,7 +450,7 @@ DeserializeShapeGraph { aliases: [ "iops_counter", ], - value_shape: Some( + wire_shape: Value( Definition( ShapeId( 5, @@ -494,10 +458,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -532,7 +492,7 @@ DeserializeShapeGraph { aliases: [ "mode", ], - value_shape: Some( + wire_shape: Value( Definition( ShapeId( 6, @@ -540,10 +500,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -553,14 +509,10 @@ DeserializeShapeGraph { aliases: [ "size", ], - value_shape: Some( + wire_shape: Value( Usize, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -594,9 +546,9 @@ DeserializeShapeGraph { "window", ], style: Unit, - fields: [], - skip: false, - custom_deserializer: false, + content: Fields( + [], + ), other: false, untagged: false, }, @@ -607,9 +559,9 @@ DeserializeShapeGraph { "leaky_bucket", ], style: Unit, - fields: [], - skip: false, - custom_deserializer: false, + content: Fields( + [], + ), other: false, untagged: false, }, @@ -646,7 +598,7 @@ DeserializeShapeGraph { aliases: [ "logs", ], - value_shape: Some( + wire_shape: Value( Definition( ShapeId( 8, @@ -654,10 +606,6 @@ DeserializeShapeGraph { ), ), default: Default, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -667,7 +615,7 @@ DeserializeShapeGraph { aliases: [ "traces", ], - value_shape: Some( + wire_shape: Value( Option( Definition( ShapeId( @@ -677,10 +625,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -690,7 +634,7 @@ DeserializeShapeGraph { aliases: [ "metrics", ], - value_shape: Some( + wire_shape: Value( Option( Definition( ShapeId( @@ -700,10 +644,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -738,7 +678,7 @@ DeserializeShapeGraph { aliases: [ "sink", ], - value_shape: Some( + wire_shape: Flatten( Definition( ShapeId( 9, @@ -746,10 +686,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: true, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -759,14 +695,10 @@ DeserializeShapeGraph { aliases: [ "filter", ], - value_shape: Some( + wire_shape: Value( String, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -802,46 +734,38 @@ DeserializeShapeGraph { "file", ], style: Struct, - fields: [ - DeserializeFieldShape { - member: Named( - "dir", - ), - name: "dir", - aliases: [ - "dir", - ], - value_shape: Some( - String, - ), - default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, - }, - DeserializeFieldShape { - member: Named( - "max_files", - ), - name: "max_files", - aliases: [ - "max_files", - ], - value_shape: Some( - Option( - Usize, + content: Fields( + [ + DeserializeFieldShape { + member: Named( + "dir", ), - ), - default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, - }, - ], - skip: false, - custom_deserializer: false, + name: "dir", + aliases: [ + "dir", + ], + wire_shape: Value( + String, + ), + default: None, + }, + DeserializeFieldShape { + member: Named( + "max_files", + ), + name: "max_files", + aliases: [ + "max_files", + ], + wire_shape: Value( + Option( + Usize, + ), + ), + default: None, + }, + ], + ), other: false, untagged: false, }, @@ -852,9 +776,9 @@ DeserializeShapeGraph { "stderr", ], style: Unit, - fields: [], - skip: false, - custom_deserializer: false, + content: Fields( + [], + ), other: false, untagged: false, }, @@ -865,27 +789,23 @@ DeserializeShapeGraph { "opentelemetry", ], style: Struct, - fields: [ - DeserializeFieldShape { - member: Named( - "otlp_endpoint", - ), - name: "otlp_endpoint", - aliases: [ - "otlp_endpoint", - ], - value_shape: Some( - String, - ), - default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, - }, - ], - skip: false, - custom_deserializer: false, + content: Fields( + [ + DeserializeFieldShape { + member: Named( + "otlp_endpoint", + ), + name: "otlp_endpoint", + aliases: [ + "otlp_endpoint", + ], + wire_shape: Value( + String, + ), + default: None, + }, + ], + ), other: false, untagged: false, }, @@ -924,14 +844,10 @@ DeserializeShapeGraph { aliases: [ "capture_log_filter", ], - value_shape: Some( + wire_shape: Value( String, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -941,7 +857,7 @@ DeserializeShapeGraph { aliases: [ "opentelemetry", ], - value_shape: Some( + wire_shape: Value( Option( Definition( ShapeId( @@ -951,10 +867,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -989,14 +901,10 @@ DeserializeShapeGraph { aliases: [ "otlp_endpoint", ], - value_shape: Some( + wire_shape: Value( String, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -1031,7 +939,7 @@ DeserializeShapeGraph { aliases: [ "opentelemetry", ], - value_shape: Some( + wire_shape: Value( Option( Definition( ShapeId( @@ -1041,10 +949,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { @@ -1079,14 +983,10 @@ DeserializeShapeGraph { aliases: [ "otlp_endpoint", ], - value_shape: Some( + wire_shape: Value( String, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -1096,16 +996,28 @@ DeserializeShapeGraph { aliases: [ "push_interval", ], - value_shape: Some( - String, + wire_shape: Value( + Union( + [ + I8, + I16, + I32, + I64, + I128, + Isize, + U8, + U16, + U32, + U64, + U128, + Usize, + String, + ], + ), ), default: Path( "default_metrics_push_interval", ), - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { diff --git a/tests/integration/tests/snapshots/env_vars__snapshots_env_options.snap b/tests/integration/tests/snapshots/env_vars__snapshots_env_options.snap index e2e69a7..245f332 100644 --- a/tests/integration/tests/snapshots/env_vars__snapshots_env_options.snap +++ b/tests/integration/tests/snapshots/env_vars__snapshots_env_options.snap @@ -75,7 +75,7 @@ expression: "env_options::(\"PERCAS_CONFIG\")" EnvOption { env_name: "PERCAS_CONFIG_STORAGE_DISK_CAPACITY", config_path: "storage.disk_capacity", - value_kind: "string", + value_kind: "integer|string", optional: true, condition: None, }, @@ -110,7 +110,7 @@ expression: "env_options::(\"PERCAS_CONFIG\")" EnvOption { env_name: "PERCAS_CONFIG_STORAGE_MEMORY_CAPACITY", config_path: "storage.memory_capacity", - value_kind: "string", + value_kind: "integer|string", optional: true, condition: None, }, @@ -165,7 +165,7 @@ expression: "env_options::(\"PERCAS_CONFIG\")" EnvOption { env_name: "PERCAS_CONFIG_TELEMETRY_METRICS_OPENTELEMETRY_PUSH_INTERVAL", config_path: "telemetry.metrics.opentelemetry.push_interval", - value_kind: "string", + value_kind: "integer|string", optional: true, condition: None, }, diff --git a/tests/no_std/tests/snapshots/snapshots__snapshots_no_std_config_deserialize_shape.snap b/tests/no_std/tests/snapshots/snapshots__snapshots_no_std_config_deserialize_shape.snap index ecb6b9f..e4ea5ba 100644 --- a/tests/no_std/tests/snapshots/snapshots__snapshots_no_std_config_deserialize_shape.snap +++ b/tests/no_std/tests/snapshots/snapshots__snapshots_no_std_config_deserialize_shape.snap @@ -29,14 +29,10 @@ DeserializeShapeGraph { aliases: [ "name", ], - value_shape: Some( + wire_shape: Value( String, ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -46,7 +42,7 @@ DeserializeShapeGraph { aliases: [ "values", ], - value_shape: Some( + wire_shape: Value( Seq( Option( U16, @@ -54,10 +50,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, DeserializeFieldShape { member: Named( @@ -67,7 +59,7 @@ DeserializeShapeGraph { aliases: [ "child", ], - value_shape: Some( + wire_shape: Value( Option( Definition( ShapeId( @@ -77,10 +69,6 @@ DeserializeShapeGraph { ), ), default: None, - flatten: false, - skip: false, - custom_deserializer: false, - transparent: false, }, ], attributes: DeserializeContainerAttributes { diff --git a/tests/no_std/tests/snapshots/snapshots__snapshots_no_std_config_serialize_shape.snap b/tests/no_std/tests/snapshots/snapshots__snapshots_no_std_config_serialize_shape.snap index 559a0c4..f58ca3c 100644 --- a/tests/no_std/tests/snapshots/snapshots__snapshots_no_std_config_serialize_shape.snap +++ b/tests/no_std/tests/snapshots/snapshots__snapshots_no_std_config_serialize_shape.snap @@ -26,39 +26,31 @@ SerializeShapeGraph { "name", ), name: "name", - value_shape: Some( + wire_shape: Value( String, ), - flatten: false, - skip: false, skip_if: None, - custom_serializer: false, - transparent: false, }, SerializeFieldShape { member: Named( "values", ), name: "values", - value_shape: Some( + wire_shape: Value( Seq( Option( U16, ), ), ), - flatten: false, - skip: false, skip_if: None, - custom_serializer: false, - transparent: false, }, SerializeFieldShape { member: Named( "child", ), name: "child", - value_shape: Some( + wire_shape: Value( Option( Definition( ShapeId( @@ -67,11 +59,7 @@ SerializeShapeGraph { ), ), ), - flatten: false, - skip: false, skip_if: None, - custom_serializer: false, - transparent: false, }, ], attributes: SerializeContainerAttributes {