diff --git a/Cargo.lock b/Cargo.lock index 3322e23..75f02d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -294,6 +303,7 @@ dependencies = [ name = "serde-shape-derive" version = "0.0.1" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "serde_derive_internals", @@ -317,6 +327,7 @@ dependencies = [ "insta", "serde", "serde-shape", + "serde_test", "toml_edit", ] @@ -381,6 +392,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_test" +version = "1.0.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" +dependencies = [ + "serde", +] + [[package]] name = "similar" version = "2.7.0" diff --git a/Cargo.toml b/Cargo.toml index 8a658db..6811622 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,11 +39,13 @@ 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" } serde = { version = "1.0.229", features = ["derive"] } serde_derive_internals = { version = "0.29.1" } serde_json = { version = "1.0.151" } +serde_test = { version = "1.0.177" } syn = { version = "2.0.104" } toml_edit = { version = "0.25.13", features = ["serde"] } which = { version = "8.0.4" } diff --git a/README.md b/README.md index 8e9693a..fe56db0 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,22 @@ See the [crate documentation][docs-url] for the full shape graph model, derive b - `derive`: enables `#[derive(SerializeShape)]` and `#[derive(DeserializeShape)]`. - `std`: enables shape implementations for standard-library-only types. +## Built-in shapes + +The built-in implementations follow Serde's own data-model calls in each direction. + +| Group | Supported types | +| --- | --- | +| Scalars | Rust primitives, `String`, `str`, non-zero integers, and atomics available on the target | +| Containers | `Option`, `Result`, arrays, slices for serialization, tuples through arity 16, `Vec`, `VecDeque`, `LinkedList`, `BinaryHeap`, `BTreeSet`, and `BTreeMap` | +| Wrappers | References, `Box`, `Cow`, `Cell`, `RefCell`, `Wrapping`, `Reverse`, and `PhantomData` | +| Time | `core::time::Duration` | +| `std` feature | `HashMap`, `HashSet`, `Path`, `PathBuf`, IP and socket address types, `Mutex`, and `RwLock` | + +Network address shapes are unions of their human-readable string representation and their compact Serde representation. A serialized byte slice is a sequence, while borrowed byte deserialization uses `ShapeRef::Bytes`. + +For an unsupported foreign type, use a local newtype and implement `SerializeShape` or `DeserializeShape` manually. Custom Serde functions remain visible as opaque boundaries because their wire behavior cannot be inferred. + ## `no_std` support `serde-shape` is `no_std` by default and requires `alloc`. diff --git a/serde-shape-derive/Cargo.toml b/serde-shape-derive/Cargo.toml index 20e7c17..0eb41ce 100644 --- a/serde-shape-derive/Cargo.toml +++ b/serde-shape-derive/Cargo.toml @@ -32,6 +32,7 @@ rust-version.workspace = true proc-macro = true [dependencies] +proc-macro-crate = { workspace = true } proc-macro2 = { workspace = true } quote = { workspace = true } serde_derive_internals = { workspace = true } diff --git a/serde-shape-derive/src/lib.rs b/serde-shape-derive/src/lib.rs index f7d7231..72fa3f3 100644 --- a/serde-shape-derive/src/lib.rs +++ b/serde-shape-derive/src/lib.rs @@ -17,6 +17,10 @@ use std::collections::BTreeSet; use proc_macro::TokenStream; +use proc_macro_crate::FoundCrate; +use proc_macro_crate::crate_name; +use proc_macro2::Ident; +use proc_macro2::Span; use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; use quote::quote; @@ -58,6 +62,7 @@ pub fn derive_deserialize_shape(input: TokenStream) -> TokenStream { } fn expand_serialize_shape(input: &DeriveInput) -> syn::Result { + let serde_shape = serde_shape_crate()?; let container = parse_container(input, Derive::Serialize)?; let ident = &input.ident; let mut generics = input.generics.clone(); @@ -66,17 +71,22 @@ fn expand_serialize_shape(input: &DeriveInput) -> syn::Result { let body = serialize_shape_body(&container); Ok(quote! { - impl #impl_generics ::serde_shape::SerializeShape for #ident #ty_generics #where_clause { - fn serialize_shape_in( - context: &mut ::serde_shape::SerializeShapeContext, - ) -> ::serde_shape::ShapeRef { - #body + const _: () = { + use #serde_shape as __serde_shape; + + impl #impl_generics __serde_shape::SerializeShape for #ident #ty_generics #where_clause { + fn serialize_shape_in( + context: &mut __serde_shape::SerializeShapeContext, + ) -> __serde_shape::ShapeRef { + #body + } } - } + }; }) } fn expand_deserialize_shape(input: &DeriveInput) -> syn::Result { + let serde_shape = serde_shape_crate()?; let container = parse_container(input, Derive::Deserialize)?; let ident = &input.ident; let mut generics = input.generics.clone(); @@ -85,16 +95,34 @@ fn expand_deserialize_shape(input: &DeriveInput) -> syn::Result { let body = deserialize_shape_body(&container); Ok(quote! { - impl #impl_generics ::serde_shape::DeserializeShape for #ident #ty_generics #where_clause { - fn deserialize_shape_in( - context: &mut ::serde_shape::DeserializeShapeContext, - ) -> ::serde_shape::ShapeRef { - #body + const _: () = { + use #serde_shape as __serde_shape; + + impl #impl_generics __serde_shape::DeserializeShape for #ident #ty_generics #where_clause { + fn deserialize_shape_in( + context: &mut __serde_shape::DeserializeShapeContext, + ) -> __serde_shape::ShapeRef { + #body + } } - } + }; }) } +fn serde_shape_crate() -> syn::Result { + match crate_name("serde-shape") { + Ok(FoundCrate::Itself) => Ok(quote!(::serde_shape)), + Ok(FoundCrate::Name(name)) => { + let ident = Ident::new(&name.replace('-', "_"), Span::call_site()); + Ok(quote!(::#ident)) + } + Err(err) => Err(syn::Error::new( + Span::call_site(), + format!("serde-shape derive could not resolve the serde-shape crate: {err}"), + )), + } +} + fn parse_container<'a>(input: &'a DeriveInput, derive: Derive) -> syn::Result> { let cx = Ctxt::new(); let Some(container) = ast::Container::from_ast(&cx, input, derive) else { @@ -141,7 +169,7 @@ fn add_serialize_shape_bounds(generics: &mut syn::Generics, container: &ast::Con generics .make_where_clause() .predicates - .push(parse_quote!(#ty: ::serde_shape::SerializeShape)); + .push(parse_quote!(#ty: __serde_shape::SerializeShape)); } } @@ -182,7 +210,7 @@ fn add_deserialize_shape_bounds(generics: &mut syn::Generics, container: &ast::C generics .make_where_clause() .predicates - .push(parse_quote!(#ty: ::serde_shape::DeserializeShape)); + .push(parse_quote!(#ty: __serde_shape::DeserializeShape)); } } @@ -217,50 +245,84 @@ fn collect_field_bound_type( type_params: &BTreeSet, field_bound_types: &mut Vec, ) { - let mut used_type_params = BTreeSet::new(); - collect_type_params(field.ty, type_params, &mut used_type_params); - if !used_type_params.is_empty() { - field_bound_types.push((*field.ty).clone()); - } + collect_shape_bound_types(field.ty, type_params, field_bound_types); } -fn collect_type_params( +fn collect_shape_bound_types( ty: &Type, type_params: &BTreeSet, - used_type_params: &mut BTreeSet, + field_bound_types: &mut Vec, ) { match ty { - Type::Array(ty) => collect_type_params(&ty.elem, type_params, used_type_params), + Type::Array(ty) => collect_shape_bound_types(&ty.elem, type_params, field_bound_types), Type::BareFn(ty) => { for input in &ty.inputs { - collect_type_params(&input.ty, type_params, used_type_params); + collect_shape_bound_types(&input.ty, type_params, field_bound_types); } - collect_return_type_params(&ty.output, type_params, used_type_params); + collect_return_type_params(&ty.output, type_params, field_bound_types); + } + Type::Group(ty) => collect_shape_bound_types(&ty.elem, type_params, field_bound_types), + Type::ImplTrait(ty) => { + collect_type_param_bounds(&ty.bounds, type_params, field_bound_types); } - Type::Group(ty) => collect_type_params(&ty.elem, type_params, used_type_params), - Type::ImplTrait(ty) => collect_type_param_bounds(&ty.bounds, type_params, used_type_params), - Type::Paren(ty) => collect_type_params(&ty.elem, type_params, used_type_params), + Type::Paren(ty) => collect_shape_bound_types(&ty.elem, type_params, field_bound_types), Type::Path(ty) => { + if ty + .path + .segments + .last() + .is_some_and(|segment| segment.ident == "PhantomData") + { + return; + } + + let is_associated_type = ty.qself.as_ref().is_some_and(|qself| { + let mut qself_bounds = Vec::new(); + collect_shape_bound_types(&qself.ty, type_params, &mut qself_bounds); + !qself_bounds.is_empty() + }) || (ty.path.leading_colon.is_none() + && ty.path.segments.len() > 1 + && ty + .path + .segments + .first() + .is_some_and(|segment| type_params.contains(&segment.ident.to_string()))); + + if is_associated_type { + push_bound_type(field_bound_types, Type::Path(ty.clone())); + return; + } + + if ty.qself.is_none() + && ty.path.leading_colon.is_none() + && ty.path.segments.len() == 1 + && ty + .path + .segments + .first() + .is_some_and(|segment| type_params.contains(&segment.ident.to_string())) + { + push_bound_type(field_bound_types, Type::Path(ty.clone())); + return; + } + if let Some(qself) = &ty.qself { - collect_type_params(&qself.ty, type_params, used_type_params); + collect_shape_bound_types(&qself.ty, type_params, field_bound_types); } + for segment in &ty.path.segments { - let ident = segment.ident.to_string(); - if type_params.contains(&ident) { - used_type_params.insert(ident); - } - collect_path_arguments(&segment.arguments, type_params, used_type_params); + collect_path_arguments(&segment.arguments, type_params, field_bound_types); } } - Type::Ptr(ty) => collect_type_params(&ty.elem, type_params, used_type_params), - Type::Reference(ty) => collect_type_params(&ty.elem, type_params, used_type_params), - Type::Slice(ty) => collect_type_params(&ty.elem, type_params, used_type_params), + Type::Ptr(ty) => collect_shape_bound_types(&ty.elem, type_params, field_bound_types), + Type::Reference(ty) => collect_shape_bound_types(&ty.elem, type_params, field_bound_types), + Type::Slice(ty) => collect_shape_bound_types(&ty.elem, type_params, field_bound_types), Type::TraitObject(ty) => { - collect_type_param_bounds(&ty.bounds, type_params, used_type_params); + collect_type_param_bounds(&ty.bounds, type_params, field_bound_types); } Type::Tuple(ty) => { for elem in &ty.elems { - collect_type_params(elem, type_params, used_type_params); + collect_shape_bound_types(elem, type_params, field_bound_types); } } Type::Infer(_) | Type::Macro(_) | Type::Never(_) | Type::Verbatim(_) => {} @@ -271,7 +333,7 @@ fn collect_type_params( fn collect_path_arguments( arguments: &PathArguments, type_params: &BTreeSet, - used_type_params: &mut BTreeSet, + field_bound_types: &mut Vec, ) { match arguments { PathArguments::None => {} @@ -279,16 +341,16 @@ fn collect_path_arguments( for argument in &arguments.args { match argument { GenericArgument::Type(ty) => { - collect_type_params(ty, type_params, used_type_params); + collect_shape_bound_types(ty, type_params, field_bound_types); } GenericArgument::AssocType(assoc) => { - collect_type_params(&assoc.ty, type_params, used_type_params); + collect_shape_bound_types(&assoc.ty, type_params, field_bound_types); } GenericArgument::Constraint(constraint) => { collect_type_param_bounds( &constraint.bounds, type_params, - used_type_params, + field_bound_types, ); } GenericArgument::Lifetime(_) @@ -300,9 +362,9 @@ fn collect_path_arguments( } PathArguments::Parenthesized(arguments) => { for input in &arguments.inputs { - collect_type_params(input, type_params, used_type_params); + collect_shape_bound_types(input, type_params, field_bound_types); } - collect_return_type_params(&arguments.output, type_params, used_type_params); + collect_return_type_params(&arguments.output, type_params, field_bound_types); } } } @@ -310,12 +372,12 @@ fn collect_path_arguments( fn collect_type_param_bounds( bounds: &syn::punctuated::Punctuated, type_params: &BTreeSet, - used_type_params: &mut BTreeSet, + field_bound_types: &mut Vec, ) { for bound in bounds { if let TypeParamBound::Trait(bound) = bound { for segment in &bound.path.segments { - collect_path_arguments(&segment.arguments, type_params, used_type_params); + collect_path_arguments(&segment.arguments, type_params, field_bound_types); } } } @@ -324,10 +386,20 @@ fn collect_type_param_bounds( fn collect_return_type_params( return_type: &ReturnType, type_params: &BTreeSet, - used_type_params: &mut BTreeSet, + field_bound_types: &mut Vec, ) { if let ReturnType::Type(_, ty) = return_type { - collect_type_params(ty, type_params, used_type_params); + collect_shape_bound_types(ty, type_params, field_bound_types); + } +} + +fn push_bound_type(field_bound_types: &mut Vec, ty: Type) { + let tokens = ty.to_token_stream().to_string(); + if field_bound_types + .iter() + .all(|existing| existing.to_token_stream().to_string() != tokens) + { + field_bound_types.push(ty); } } @@ -337,7 +409,7 @@ fn serialize_shape_body(container: &ast::Container<'_>) -> TokenStream2 { quote! { context.define_named_type( - ::serde_shape::SerializeTypeName { + __serde_shape::SerializeTypeName { rust_name: ::core::any::type_name::(), name: #name, }, @@ -354,7 +426,7 @@ fn deserialize_shape_body(container: &ast::Container<'_>) -> TokenStream2 { quote! { context.define_named_type( - ::serde_shape::DeserializeTypeName { + __serde_shape::DeserializeTypeName { rust_name: ::core::any::type_name::(), name: #name, }, @@ -379,9 +451,9 @@ fn serialize_definition_kind(container: &ast::Container<'_>) -> TokenStream2 { let style = fields_style(*style); let fields = fields.iter().map(serialize_field_shape); quote! { - ::serde_shape::SerializeDefinitionKind::Struct(::serde_shape::SerializeStructShape { + __serde_shape::SerializeDefinitionKind::Struct(__serde_shape::SerializeStructShape { style: #style, - fields: ::serde_shape::__private::vec![#(#fields),*], + fields: __serde_shape::__private::vec![#(#fields),*], attributes: #attributes, }) } @@ -390,9 +462,9 @@ fn serialize_definition_kind(container: &ast::Container<'_>) -> TokenStream2 { let repr = tagging(container.attrs.tag()); let variants = variants.iter().map(serialize_variant_shape); quote! { - ::serde_shape::SerializeDefinitionKind::Enum(::serde_shape::SerializeEnumShape { + __serde_shape::SerializeDefinitionKind::Enum(__serde_shape::SerializeEnumShape { repr: #repr, - variants: ::serde_shape::__private::vec![#(#variants),*], + variants: __serde_shape::__private::vec![#(#variants),*], attributes: #attributes, }) } @@ -417,9 +489,9 @@ fn deserialize_definition_kind(container: &ast::Container<'_>) -> TokenStream2 { let style = fields_style(*style); let fields = fields.iter().map(deserialize_field_shape); quote! { - ::serde_shape::DeserializeDefinitionKind::Struct(::serde_shape::DeserializeStructShape { + __serde_shape::DeserializeDefinitionKind::Struct(__serde_shape::DeserializeStructShape { style: #style, - fields: ::serde_shape::__private::vec![#(#fields),*], + fields: __serde_shape::__private::vec![#(#fields),*], attributes: #attributes, }) } @@ -428,9 +500,9 @@ fn deserialize_definition_kind(container: &ast::Container<'_>) -> TokenStream2 { let repr = tagging(container.attrs.tag()); let variants = variants.iter().map(deserialize_variant_shape); quote! { - ::serde_shape::DeserializeDefinitionKind::Enum(::serde_shape::DeserializeEnumShape { + __serde_shape::DeserializeDefinitionKind::Enum(__serde_shape::DeserializeEnumShape { repr: #repr, - variants: ::serde_shape::__private::vec![#(#variants),*], + variants: __serde_shape::__private::vec![#(#variants),*], attributes: #attributes, }) } @@ -446,7 +518,7 @@ where let detail = lit(detail.to_token_stream().to_string()); quote! { - ::serde_shape::SerializeDefinitionKind::Opaque(::serde_shape::OpaqueShape { + __serde_shape::SerializeDefinitionKind::Opaque(__serde_shape::OpaqueShape { type_name: ::core::any::type_name::(), reason: #reason, detail: ::core::option::Option::Some(#detail), @@ -462,7 +534,7 @@ where let detail = lit(detail.to_token_stream().to_string()); quote! { - ::serde_shape::DeserializeDefinitionKind::Opaque(::serde_shape::OpaqueShape { + __serde_shape::DeserializeDefinitionKind::Opaque(__serde_shape::OpaqueShape { type_name: ::core::any::type_name::(), reason: #reason, detail: ::core::option::Option::Some(#detail), @@ -477,7 +549,7 @@ fn serialize_container_attributes(attrs: &attr::Container) -> TokenStream2 { let non_exhaustive = attrs.non_exhaustive(); quote! { - ::serde_shape::SerializeContainerAttributes { + __serde_shape::SerializeContainerAttributes { tagging: #tagging, has_flatten: #has_flatten, transparent: #transparent, @@ -496,7 +568,7 @@ fn deserialize_container_attributes(attrs: &attr::Container) -> TokenStream2 { let non_exhaustive = attrs.non_exhaustive(); quote! { - ::serde_shape::DeserializeContainerAttributes { + __serde_shape::DeserializeContainerAttributes { tagging: #tagging, deny_unknown_fields: #deny_unknown_fields, default: #default, @@ -515,27 +587,27 @@ fn serialize_variant_shape(variant: &ast::Variant<'_>) -> TokenStream2 { let skip = variant.attrs.skip_serializing(); let untagged = variant.attrs.untagged(); let content = if skip { - quote!(::serde_shape::SerializeVariantContent::Omitted) + 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 { + __serde_shape::SerializeVariantContent::Custom(__serde_shape::OpaqueShape { type_name: ::core::any::type_name::(), - reason: ::serde_shape::OpaqueReason::CustomSerializer, + reason: __serde_shape::OpaqueReason::CustomSerializer, detail: #detail, }) } } else { let fields = variant.fields.iter().map(serialize_field_shape); quote! { - ::serde_shape::SerializeVariantContent::Fields( - ::serde_shape::__private::vec![#(#fields),*], + __serde_shape::SerializeVariantContent::Fields( + __serde_shape::__private::vec![#(#fields),*], ) } }; quote! { - ::serde_shape::SerializeVariantShape { + __serde_shape::SerializeVariantShape { rust_name: #rust_name, name: #name, style: #style, @@ -554,27 +626,27 @@ fn deserialize_variant_shape(variant: &ast::Variant<'_>) -> TokenStream2 { let other = variant.attrs.other(); let untagged = variant.attrs.untagged(); let content = if skip { - quote!(::serde_shape::DeserializeVariantContent::Omitted) + 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 { + __serde_shape::DeserializeVariantContent::Custom(__serde_shape::OpaqueShape { type_name: ::core::any::type_name::(), - reason: ::serde_shape::OpaqueReason::CustomDeserializer, + reason: __serde_shape::OpaqueReason::CustomDeserializer, detail: #detail, }) } } else { let fields = variant.fields.iter().map(deserialize_field_shape); quote! { - ::serde_shape::DeserializeVariantContent::Fields( - ::serde_shape::__private::vec![#(#fields),*], + __serde_shape::DeserializeVariantContent::Fields( + __serde_shape::__private::vec![#(#fields),*], ) } }; quote! { - ::serde_shape::DeserializeVariantShape { + __serde_shape::DeserializeVariantShape { rust_name: #rust_name, name: #name, aliases: #aliases, @@ -595,32 +667,32 @@ fn serialize_field_shape(field: &ast::Field<'_>) -> TokenStream2 { let transparent = field.attrs.transparent(); let ty = field.ty; let wire_shape = if skip { - quote!(::serde_shape::FieldWireShape::Omitted) + quote!(__serde_shape::FieldWireShape::Omitted) } else { 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 { + __serde_shape::ShapeRef::Opaque(__serde_shape::OpaqueShape { type_name: ::core::any::type_name::<#ty>(), - reason: ::serde_shape::OpaqueReason::CustomSerializer, + reason: __serde_shape::OpaqueReason::CustomSerializer, detail: #detail, }) } } else { - quote!(<#ty as ::serde_shape::SerializeShape>::serialize_shape_in(context)) + quote!(<#ty as __serde_shape::SerializeShape>::serialize_shape_in(context)) }; if transparent { - quote!(::serde_shape::FieldWireShape::Inline(#value_shape)) + quote!(__serde_shape::FieldWireShape::Inline(#value_shape)) } else if flatten { - quote!(::serde_shape::FieldWireShape::Flatten(#value_shape)) + quote!(__serde_shape::FieldWireShape::Flatten(#value_shape)) } else { - quote!(::serde_shape::FieldWireShape::Value(#value_shape)) + quote!(__serde_shape::FieldWireShape::Value(#value_shape)) } }; quote! { - ::serde_shape::SerializeFieldShape { + __serde_shape::SerializeFieldShape { member: #member, name: #name, wire_shape: #wire_shape, @@ -639,32 +711,32 @@ fn deserialize_field_shape(field: &ast::Field<'_>) -> TokenStream2 { let transparent = field.attrs.transparent(); let ty = field.ty; let wire_shape = if skip { - quote!(::serde_shape::FieldWireShape::Omitted) + quote!(__serde_shape::FieldWireShape::Omitted) } else { 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 { + __serde_shape::ShapeRef::Opaque(__serde_shape::OpaqueShape { type_name: ::core::any::type_name::<#ty>(), - reason: ::serde_shape::OpaqueReason::CustomDeserializer, + reason: __serde_shape::OpaqueReason::CustomDeserializer, detail: #detail, }) } } else { - quote!(<#ty as ::serde_shape::DeserializeShape>::deserialize_shape_in(context)) + quote!(<#ty as __serde_shape::DeserializeShape>::deserialize_shape_in(context)) }; if transparent { - quote!(::serde_shape::FieldWireShape::Inline(#value_shape)) + quote!(__serde_shape::FieldWireShape::Inline(#value_shape)) } else if flatten { - quote!(::serde_shape::FieldWireShape::Flatten(#value_shape)) + quote!(__serde_shape::FieldWireShape::Flatten(#value_shape)) } else { - quote!(::serde_shape::FieldWireShape::Value(#value_shape)) + quote!(__serde_shape::FieldWireShape::Value(#value_shape)) } }; quote! { - ::serde_shape::DeserializeFieldShape { + __serde_shape::DeserializeFieldShape { member: #member, name: #name, aliases: #aliases, @@ -678,67 +750,67 @@ fn field_member(member: &Member) -> TokenStream2 { match member { Member::Named(ident) => { let ident = lit(ident.to_string()); - quote!(::serde_shape::FieldMember::Named(#ident)) + quote!(__serde_shape::FieldMember::Named(#ident)) } Member::Unnamed(index) => { let index = index.index as usize; - quote!(::serde_shape::FieldMember::Unnamed(#index)) + quote!(__serde_shape::FieldMember::Unnamed(#index)) } } } fn fields_style(style: ast::Style) -> TokenStream2 { match style { - ast::Style::Struct => quote!(::serde_shape::FieldsStyle::Struct), - ast::Style::Tuple => quote!(::serde_shape::FieldsStyle::Tuple), - ast::Style::Newtype => quote!(::serde_shape::FieldsStyle::Newtype), - ast::Style::Unit => quote!(::serde_shape::FieldsStyle::Unit), + ast::Style::Struct => quote!(__serde_shape::FieldsStyle::Struct), + ast::Style::Tuple => quote!(__serde_shape::FieldsStyle::Tuple), + ast::Style::Newtype => quote!(__serde_shape::FieldsStyle::Newtype), + ast::Style::Unit => quote!(__serde_shape::FieldsStyle::Unit), } } fn tagging(tag: &attr::TagType) -> TokenStream2 { match tag { - attr::TagType::External => quote!(::serde_shape::Tagging::External), + attr::TagType::External => quote!(__serde_shape::Tagging::External), attr::TagType::Internal { tag } => { let tag = lit(tag); - quote!(::serde_shape::Tagging::Internal { tag: #tag }) + quote!(__serde_shape::Tagging::Internal { tag: #tag }) } attr::TagType::Adjacent { tag, content } => { let tag = lit(tag); let content = lit(content); - quote!(::serde_shape::Tagging::Adjacent { + quote!(__serde_shape::Tagging::Adjacent { tag: #tag, content: #content, }) } - attr::TagType::None => quote!(::serde_shape::Tagging::Untagged), + attr::TagType::None => quote!(__serde_shape::Tagging::Untagged), } } fn default_shape(default: &attr::Default) -> TokenStream2 { match default { - attr::Default::None => quote!(::serde_shape::DefaultShape::None), - attr::Default::Default => quote!(::serde_shape::DefaultShape::Default), + attr::Default::None => quote!(__serde_shape::DefaultShape::None), + attr::Default::Default => quote!(__serde_shape::DefaultShape::Default), attr::Default::Path(path) => { let path = lit(path.to_token_stream().to_string()); - quote!(::serde_shape::DefaultShape::Path(#path)) + quote!(__serde_shape::DefaultShape::Path(#path)) } } } fn opaque_reason(reason: &str) -> TokenStream2 { match reason { - "FromType" => quote!(::serde_shape::OpaqueReason::FromType), - "TryFromType" => quote!(::serde_shape::OpaqueReason::TryFromType), - "IntoType" => quote!(::serde_shape::OpaqueReason::IntoType), - "Remote" => quote!(::serde_shape::OpaqueReason::Remote), - _ => quote!(::serde_shape::OpaqueReason::Unsupported), + "FromType" => quote!(__serde_shape::OpaqueReason::FromType), + "TryFromType" => quote!(__serde_shape::OpaqueReason::TryFromType), + "IntoType" => quote!(__serde_shape::OpaqueReason::IntoType), + "Remote" => quote!(__serde_shape::OpaqueReason::Remote), + _ => quote!(__serde_shape::OpaqueReason::Unsupported), } } fn aliases(aliases: &BTreeSet) -> TokenStream2 { let aliases = aliases.iter().map(lit); - quote!(::serde_shape::__private::vec![#(#aliases),*]) + quote!(__serde_shape::__private::vec![#(#aliases),*]) } fn option_lit(value: Option<&str>) -> TokenStream2 { diff --git a/serde-shape/src/impls/container.rs b/serde-shape/src/impls/container.rs index 227bbe7..f3e4921 100644 --- a/serde-shape/src/impls/container.rs +++ b/serde-shape/src/impls/container.rs @@ -109,6 +109,15 @@ seq_shape! { } +impl SerializeShape for [T] +where + T: SerializeShape, +{ + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + ShapeRef::Seq(Box::new(T::serialize_shape_in(context))) + } +} + #[cfg(feature = "std")] seq_shape! { (T, S) std::collections::HashSet diff --git a/serde-shape/src/impls/mod.rs b/serde-shape/src/impls/mod.rs index 51b35f2..38b544c 100644 --- a/serde-shape/src/impls/mod.rs +++ b/serde-shape/src/impls/mod.rs @@ -13,6 +13,10 @@ // limitations under the License. mod container; +#[cfg(feature = "std")] +mod net; mod primitive; +mod result; +mod time; mod tuple; mod wrapper; diff --git a/serde-shape/src/impls/net.rs b/serde-shape/src/impls/net.rs new file mode 100644 index 0000000..acb8b95 --- /dev/null +++ b/serde-shape/src/impls/net.rs @@ -0,0 +1,233 @@ +// 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 alloc::boxed::Box; +use alloc::vec; +use core::any::type_name; +use std::net::IpAddr; +use std::net::Ipv4Addr; +use std::net::Ipv6Addr; +use std::net::SocketAddr; +use std::net::SocketAddrV4; +use std::net::SocketAddrV6; + +use crate::DefaultShape; +use crate::DeserializeContainerAttributes; +use crate::DeserializeDefinitionKind; +use crate::DeserializeEnumShape; +use crate::DeserializeFieldShape; +use crate::DeserializeShape; +use crate::DeserializeShapeContext; +use crate::DeserializeTypeName; +use crate::DeserializeVariantContent; +use crate::DeserializeVariantShape; +use crate::FieldMember; +use crate::FieldWireShape; +use crate::FieldsStyle; +use crate::SerializeContainerAttributes; +use crate::SerializeDefinitionKind; +use crate::SerializeEnumShape; +use crate::SerializeFieldShape; +use crate::SerializeShape; +use crate::SerializeShapeContext; +use crate::SerializeTypeName; +use crate::SerializeVariantContent; +use crate::SerializeVariantShape; +use crate::ShapeRef; +use crate::Tagging; + +macro_rules! union_shape { + ($ty:ty => $binary:expr) => { + impl SerializeShape for $ty { + fn serialize_shape_in(_context: &mut SerializeShapeContext) -> ShapeRef { + ShapeRef::union([ShapeRef::String, $binary]) + } + } + + impl DeserializeShape for $ty { + fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::union([ShapeRef::String, $binary]) + } + } + }; +} + +union_shape!(Ipv4Addr => ipv4_binary_shape()); +union_shape!(Ipv6Addr => ipv6_binary_shape()); +union_shape!(SocketAddrV4 => socket_v4_binary_shape()); +union_shape!(SocketAddrV6 => socket_v6_binary_shape()); + +impl SerializeShape for IpAddr { + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + let binary = context.define_named_type( + SerializeTypeName { + rust_name: type_name::(), + name: "IpAddr", + }, + |_| { + SerializeDefinitionKind::Enum(SerializeEnumShape { + repr: Tagging::External, + variants: vec![ + serialize_newtype_variant("V4", ipv4_binary_shape()), + serialize_newtype_variant("V6", ipv6_binary_shape()), + ], + attributes: serialize_enum_attributes(), + }) + }, + ); + ShapeRef::union([ShapeRef::String, binary]) + } +} + +impl DeserializeShape for IpAddr { + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + let binary = context.define_named_type( + DeserializeTypeName { + rust_name: type_name::(), + name: "IpAddr", + }, + |_| { + DeserializeDefinitionKind::Enum(DeserializeEnumShape { + repr: Tagging::External, + variants: vec![ + deserialize_newtype_variant("V4", ipv4_binary_shape()), + deserialize_newtype_variant("V6", ipv6_binary_shape()), + ], + attributes: deserialize_enum_attributes(), + }) + }, + ); + ShapeRef::union([ShapeRef::String, binary]) + } +} + +impl SerializeShape for SocketAddr { + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + let binary = context.define_named_type( + SerializeTypeName { + rust_name: type_name::(), + name: "SocketAddr", + }, + |_| { + SerializeDefinitionKind::Enum(SerializeEnumShape { + repr: Tagging::External, + variants: vec![ + serialize_newtype_variant("V4", socket_v4_binary_shape()), + serialize_newtype_variant("V6", socket_v6_binary_shape()), + ], + attributes: serialize_enum_attributes(), + }) + }, + ); + ShapeRef::union([ShapeRef::String, binary]) + } +} + +impl DeserializeShape for SocketAddr { + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + let binary = context.define_named_type( + DeserializeTypeName { + rust_name: type_name::(), + name: "SocketAddr", + }, + |_| { + DeserializeDefinitionKind::Enum(DeserializeEnumShape { + repr: Tagging::External, + variants: vec![ + deserialize_newtype_variant("V4", socket_v4_binary_shape()), + deserialize_newtype_variant("V6", socket_v6_binary_shape()), + ], + attributes: deserialize_enum_attributes(), + }) + }, + ); + ShapeRef::union([ShapeRef::String, binary]) + } +} + +fn ipv4_binary_shape() -> ShapeRef { + ShapeRef::Array { + item: Box::new(ShapeRef::U8), + len: 4, + } +} + +fn ipv6_binary_shape() -> ShapeRef { + ShapeRef::Array { + item: Box::new(ShapeRef::U8), + len: 16, + } +} + +fn socket_v4_binary_shape() -> ShapeRef { + ShapeRef::Tuple(vec![ipv4_binary_shape(), ShapeRef::U16]) +} + +fn socket_v6_binary_shape() -> ShapeRef { + ShapeRef::Tuple(vec![ipv6_binary_shape(), ShapeRef::U16]) +} + +fn serialize_newtype_variant(name: &'static str, shape: ShapeRef) -> SerializeVariantShape { + SerializeVariantShape { + rust_name: name, + name, + style: FieldsStyle::Newtype, + content: SerializeVariantContent::Fields(vec![SerializeFieldShape { + member: FieldMember::Unnamed(0), + name: "0", + wire_shape: FieldWireShape::Value(shape), + skip_if: None, + }]), + untagged: false, + } +} + +fn deserialize_newtype_variant(name: &'static str, shape: ShapeRef) -> DeserializeVariantShape { + DeserializeVariantShape { + rust_name: name, + name, + aliases: vec![name], + style: FieldsStyle::Newtype, + content: DeserializeVariantContent::Fields(vec![DeserializeFieldShape { + member: FieldMember::Unnamed(0), + name: "0", + aliases: vec!["0"], + wire_shape: FieldWireShape::Value(shape), + default: DefaultShape::None, + }]), + other: false, + untagged: false, + } +} + +fn serialize_enum_attributes() -> SerializeContainerAttributes { + SerializeContainerAttributes { + tagging: Tagging::External, + has_flatten: false, + transparent: false, + non_exhaustive: false, + } +} + +fn deserialize_enum_attributes() -> DeserializeContainerAttributes { + DeserializeContainerAttributes { + tagging: Tagging::External, + deny_unknown_fields: false, + default: DefaultShape::None, + has_flatten: false, + transparent: false, + expecting: None, + non_exhaustive: false, + } +} diff --git a/serde-shape/src/impls/primitive.rs b/serde-shape/src/impls/primitive.rs index 9fba12f..5c34c21 100644 --- a/serde-shape/src/impls/primitive.rs +++ b/serde-shape/src/impls/primitive.rs @@ -57,7 +57,6 @@ primitive_shape! { f32 => ShapeRef::F32; f64 => ShapeRef::F64; str => ShapeRef::String; - [u8] => ShapeRef::Bytes; String => ShapeRef::String; core::num::NonZeroI8 => ShapeRef::I8; core::num::NonZeroI16 => ShapeRef::I16; @@ -73,16 +72,16 @@ primitive_shape! { core::num::NonZeroUsize => ShapeRef::Usize; } +impl DeserializeShape for [u8] { + fn deserialize_shape_in(_context: &mut DeserializeShapeContext) -> ShapeRef { + ShapeRef::Bytes + } +} + #[cfg(feature = "std")] primitive_shape! { std::path::Path => ShapeRef::String; std::path::PathBuf => ShapeRef::String; - std::net::IpAddr => ShapeRef::String; - std::net::Ipv4Addr => ShapeRef::String; - std::net::Ipv6Addr => ShapeRef::String; - std::net::SocketAddr => ShapeRef::String; - std::net::SocketAddrV4 => ShapeRef::String; - std::net::SocketAddrV6 => ShapeRef::String; } #[cfg(target_has_atomic = "8")] diff --git a/serde-shape/src/impls/result.rs b/serde-shape/src/impls/result.rs new file mode 100644 index 0000000..b97146b --- /dev/null +++ b/serde-shape/src/impls/result.rs @@ -0,0 +1,137 @@ +// 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 alloc::vec; +use core::any::type_name; + +use crate::DefaultShape; +use crate::DeserializeContainerAttributes; +use crate::DeserializeDefinitionKind; +use crate::DeserializeEnumShape; +use crate::DeserializeFieldShape; +use crate::DeserializeShape; +use crate::DeserializeShapeContext; +use crate::DeserializeTypeName; +use crate::DeserializeVariantContent; +use crate::DeserializeVariantShape; +use crate::FieldMember; +use crate::FieldWireShape; +use crate::FieldsStyle; +use crate::SerializeContainerAttributes; +use crate::SerializeDefinitionKind; +use crate::SerializeEnumShape; +use crate::SerializeFieldShape; +use crate::SerializeShape; +use crate::SerializeShapeContext; +use crate::SerializeTypeName; +use crate::SerializeVariantContent; +use crate::SerializeVariantShape; +use crate::ShapeRef; +use crate::Tagging; + +impl SerializeShape for Result +where + T: SerializeShape, + E: SerializeShape, +{ + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + context.define_named_type( + SerializeTypeName { + rust_name: type_name::(), + name: "Result", + }, + |context| { + SerializeDefinitionKind::Enum(SerializeEnumShape { + repr: Tagging::External, + variants: vec![ + serialize_result_variant("Ok", T::serialize_shape_in(context)), + serialize_result_variant("Err", E::serialize_shape_in(context)), + ], + attributes: SerializeContainerAttributes { + tagging: Tagging::External, + has_flatten: false, + transparent: false, + non_exhaustive: false, + }, + }) + }, + ) + } +} + +impl DeserializeShape for Result +where + T: DeserializeShape, + E: DeserializeShape, +{ + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + context.define_named_type( + DeserializeTypeName { + rust_name: type_name::(), + name: "Result", + }, + |context| { + DeserializeDefinitionKind::Enum(DeserializeEnumShape { + repr: Tagging::External, + variants: vec![ + deserialize_result_variant("Ok", T::deserialize_shape_in(context)), + deserialize_result_variant("Err", E::deserialize_shape_in(context)), + ], + attributes: DeserializeContainerAttributes { + tagging: Tagging::External, + deny_unknown_fields: false, + default: DefaultShape::None, + has_flatten: false, + transparent: false, + expecting: None, + non_exhaustive: false, + }, + }) + }, + ) + } +} + +fn serialize_result_variant(name: &'static str, shape: ShapeRef) -> SerializeVariantShape { + SerializeVariantShape { + rust_name: name, + name, + style: FieldsStyle::Newtype, + content: SerializeVariantContent::Fields(vec![SerializeFieldShape { + member: FieldMember::Unnamed(0), + name: "0", + wire_shape: FieldWireShape::Value(shape), + skip_if: None, + }]), + untagged: false, + } +} + +fn deserialize_result_variant(name: &'static str, shape: ShapeRef) -> DeserializeVariantShape { + DeserializeVariantShape { + rust_name: name, + name, + aliases: vec![name], + style: FieldsStyle::Newtype, + content: DeserializeVariantContent::Fields(vec![DeserializeFieldShape { + member: FieldMember::Unnamed(0), + name: "0", + aliases: vec!["0"], + wire_shape: FieldWireShape::Value(shape), + default: DefaultShape::None, + }]), + other: false, + untagged: false, + } +} diff --git a/serde-shape/src/impls/time.rs b/serde-shape/src/impls/time.rs new file mode 100644 index 0000000..c30e9f1 --- /dev/null +++ b/serde-shape/src/impls/time.rs @@ -0,0 +1,115 @@ +// 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 alloc::vec; +use core::any::type_name; +use core::time::Duration; + +use crate::DefaultShape; +use crate::DeserializeContainerAttributes; +use crate::DeserializeDefinitionKind; +use crate::DeserializeFieldShape; +use crate::DeserializeShape; +use crate::DeserializeShapeContext; +use crate::DeserializeStructShape; +use crate::DeserializeTypeName; +use crate::FieldMember; +use crate::FieldWireShape; +use crate::FieldsStyle; +use crate::SerializeContainerAttributes; +use crate::SerializeDefinitionKind; +use crate::SerializeFieldShape; +use crate::SerializeShape; +use crate::SerializeShapeContext; +use crate::SerializeStructShape; +use crate::SerializeTypeName; +use crate::ShapeRef; +use crate::Tagging; + +impl SerializeShape for Duration { + fn serialize_shape_in(context: &mut SerializeShapeContext) -> ShapeRef { + context.define_named_type( + SerializeTypeName { + rust_name: type_name::(), + name: "Duration", + }, + |_| { + SerializeDefinitionKind::Struct(SerializeStructShape { + style: FieldsStyle::Struct, + fields: vec![ + SerializeFieldShape { + member: FieldMember::Named("secs"), + name: "secs", + wire_shape: FieldWireShape::Value(ShapeRef::U64), + skip_if: None, + }, + SerializeFieldShape { + member: FieldMember::Named("nanos"), + name: "nanos", + wire_shape: FieldWireShape::Value(ShapeRef::U32), + skip_if: None, + }, + ], + attributes: SerializeContainerAttributes { + tagging: Tagging::External, + has_flatten: false, + transparent: false, + non_exhaustive: false, + }, + }) + }, + ) + } +} + +impl DeserializeShape for Duration { + fn deserialize_shape_in(context: &mut DeserializeShapeContext) -> ShapeRef { + context.define_named_type( + DeserializeTypeName { + rust_name: type_name::(), + name: "Duration", + }, + |_| { + DeserializeDefinitionKind::Struct(DeserializeStructShape { + style: FieldsStyle::Struct, + fields: vec![ + DeserializeFieldShape { + member: FieldMember::Named("secs"), + name: "secs", + aliases: vec!["secs"], + wire_shape: FieldWireShape::Value(ShapeRef::U64), + default: DefaultShape::None, + }, + DeserializeFieldShape { + member: FieldMember::Named("nanos"), + name: "nanos", + aliases: vec!["nanos"], + wire_shape: FieldWireShape::Value(ShapeRef::U32), + default: DefaultShape::None, + }, + ], + attributes: DeserializeContainerAttributes { + tagging: Tagging::External, + deny_unknown_fields: true, + default: DefaultShape::None, + has_flatten: false, + transparent: false, + expecting: None, + non_exhaustive: false, + }, + }) + }, + ) + } +} diff --git a/serde-shape/src/impls/tuple.rs b/serde-shape/src/impls/tuple.rs index 40cc0fb..3d8751c 100644 --- a/serde-shape/src/impls/tuple.rs +++ b/serde-shape/src/impls/tuple.rs @@ -57,4 +57,8 @@ tuple_shape! { T0, T1, T2, T3, T4, T5, T6, T7, T8, T9; T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10; T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11; + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12; + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13; + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14; + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15; } diff --git a/serde-shape/src/lib.rs b/serde-shape/src/lib.rs index b47b478..a47e265 100644 --- a/serde-shape/src/lib.rs +++ b/serde-shape/src/lib.rs @@ -191,12 +191,14 @@ #![deny(missing_docs)] extern crate alloc; +extern crate self as serde_shape; #[cfg(feature = "std")] extern crate std; use alloc::boxed::Box; use alloc::collections::BTreeMap; use alloc::vec::Vec; +use core::any::TypeId; use core::fmt; /// Private exports used by generated derive code. @@ -379,22 +381,25 @@ impl DeserializeShapeGraph { #[derive(Debug, Default)] pub struct SerializeShapeContext { definitions: Vec>, - definitions_by_rust_name: BTreeMap<&'static str, ShapeId>, + definitions_by_identity: BTreeMap<(TypeId, &'static str), ShapeId>, } impl SerializeShapeContext { /// Define a named type once and return a reference to its definition. + /// + /// The concrete builder type and diagnostic Rust name form the graph-local identity. Call this + /// method from one stable closure expression for every occurrence of the same named type. pub fn define_named_type(&mut self, type_name: SerializeTypeName, build: F) -> ShapeRef where - F: FnOnce(&mut Self) -> SerializeDefinitionKind, + F: FnOnce(&mut Self) -> SerializeDefinitionKind + 'static, { - if let Some(id) = self.definitions_by_rust_name.get(type_name.rust_name) { + let identity = (TypeId::of::(), type_name.rust_name); + if let Some(id) = self.definitions_by_identity.get(&identity) { return ShapeRef::Definition(*id); } let id = ShapeId(self.definitions.len()); - self.definitions_by_rust_name - .insert(type_name.rust_name, id); + self.definitions_by_identity.insert(identity, id); self.definitions.push(None); let kind = build(self); @@ -418,22 +423,25 @@ impl SerializeShapeContext { #[derive(Debug, Default)] pub struct DeserializeShapeContext { definitions: Vec>, - definitions_by_rust_name: BTreeMap<&'static str, ShapeId>, + definitions_by_identity: BTreeMap<(TypeId, &'static str), ShapeId>, } impl DeserializeShapeContext { /// Define a named type once and return a reference to its definition. + /// + /// The concrete builder type and diagnostic Rust name form the graph-local identity. Call this + /// method from one stable closure expression for every occurrence of the same named type. pub fn define_named_type(&mut self, type_name: DeserializeTypeName, build: F) -> ShapeRef where - F: FnOnce(&mut Self) -> DeserializeDefinitionKind, + F: FnOnce(&mut Self) -> DeserializeDefinitionKind + 'static, { - if let Some(id) = self.definitions_by_rust_name.get(type_name.rust_name) { + let identity = (TypeId::of::(), type_name.rust_name); + if let Some(id) = self.definitions_by_identity.get(&identity) { return ShapeRef::Definition(*id); } let id = ShapeId(self.definitions.len()); - self.definitions_by_rust_name - .insert(type_name.rust_name, id); + self.definitions_by_identity.insert(identity, id); self.definitions.push(None); let kind = build(self); diff --git a/serde-shape/src/tests.rs b/serde-shape/src/tests.rs index fd743e5..b975949 100644 --- a/serde-shape/src/tests.rs +++ b/serde-shape/src/tests.rs @@ -19,13 +19,26 @@ use alloc::collections::BinaryHeap; use alloc::collections::LinkedList; use alloc::collections::VecDeque; use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; use core::cell::Cell; use core::cmp::Reverse; use core::num::Wrapping; +use crate::DeserializeDefinitionKind; +use crate::DeserializeShapeContext; use crate::DeserializeShapeGraph; +use crate::DeserializeTypeName; +use crate::FieldWireShape; +use crate::FieldsStyle; +use crate::OpaqueReason; +use crate::OpaqueShape; +use crate::SerializeDefinitionKind; +use crate::SerializeShapeContext; use crate::SerializeShapeGraph; +use crate::SerializeTypeName; use crate::ShapeRef; +use crate::Tagging; #[test] fn classifies_flat_numeric_shapes() { @@ -70,6 +83,71 @@ fn normalizes_union_shapes() { ); } +#[test] +fn keeps_distinct_definition_builders_with_the_same_type_name() { + let mut serialize = SerializeShapeContext::default(); + let first = serialize.define_named_type( + SerializeTypeName { + rust_name: "duplicate::Type", + name: "First", + }, + |_| { + SerializeDefinitionKind::Opaque(OpaqueShape { + type_name: "duplicate::Type", + reason: OpaqueReason::Unsupported, + detail: Some("first"), + }) + }, + ); + let second = serialize.define_named_type( + SerializeTypeName { + rust_name: "duplicate::Type", + name: "Second", + }, + |_| { + SerializeDefinitionKind::Opaque(OpaqueShape { + type_name: "duplicate::Type", + reason: OpaqueReason::Unsupported, + detail: Some("second"), + }) + }, + ); + + assert_ne!(first, second); + assert_eq!(serialize.finish().len(), 2); + + let mut deserialize = DeserializeShapeContext::default(); + let first = deserialize.define_named_type( + DeserializeTypeName { + rust_name: "duplicate::Type", + name: "First", + }, + |_| { + DeserializeDefinitionKind::Opaque(OpaqueShape { + type_name: "duplicate::Type", + reason: OpaqueReason::Unsupported, + detail: Some("first"), + }) + }, + ); + let second = deserialize.define_named_type( + DeserializeTypeName { + rust_name: "duplicate::Type", + name: "Second", + }, + |_| { + DeserializeDefinitionKind::Opaque(OpaqueShape { + type_name: "duplicate::Type", + reason: OpaqueReason::Unsupported, + detail: Some("second"), + }) + }, + ); + + assert_ne!(first, second); + assert_eq!(deserialize.finish().len(), 2); +} + #[cfg(target_has_atomic = "ptr")] #[test] fn maps_atomic_shapes() { @@ -98,6 +176,108 @@ fn builds_map_shape() { assert!(deserialize_shape.definitions.is_empty()); } +#[test] +fn distinguishes_byte_sequences_from_borrowed_byte_input() { + assert_eq!( + SerializeShapeGraph::for_type::<[u8]>().root, + ShapeRef::Seq(Box::new(ShapeRef::U8)) + ); + assert_eq!( + SerializeShapeGraph::for_type::>().root, + ShapeRef::Seq(Box::new(ShapeRef::U8)) + ); + assert_eq!( + DeserializeShapeGraph::for_type::<[u8]>().root, + ShapeRef::Bytes + ); +} + +#[test] +fn maps_result_as_an_externally_tagged_enum() { + let serialize = SerializeShapeGraph::for_type::>(); + let ShapeRef::Definition(id) = serialize.root else { + panic!("result should produce a named definition"); + }; + let SerializeDefinitionKind::Enum(shape) = &serialize.definition(id).unwrap().kind else { + panic!("result definition should be an enum"); + }; + + assert_eq!(shape.repr, Tagging::External); + assert_eq!(shape.variants.len(), 2); + assert_eq!(shape.variants[0].name, "Ok"); + assert_eq!(shape.variants[0].style, FieldsStyle::Newtype); + let crate::SerializeVariantContent::Fields(fields) = &shape.variants[0].content else { + panic!("Ok should contain one reflected field"); + }; + assert_eq!(fields[0].wire_shape, FieldWireShape::Value(ShapeRef::U8)); + + let deserialize = DeserializeShapeGraph::for_type::>(); + let ShapeRef::Definition(id) = deserialize.root else { + panic!("result should produce a named definition"); + }; + let DeserializeDefinitionKind::Enum(shape) = &deserialize.definition(id).unwrap().kind else { + panic!("result definition should be an enum"); + }; + assert_eq!(shape.variants[1].name, "Err"); + let crate::DeserializeVariantContent::Fields(fields) = &shape.variants[1].content else { + panic!("Err should contain one reflected field"); + }; + assert_eq!( + fields[0].wire_shape, + FieldWireShape::Value(ShapeRef::String) + ); +} + +#[test] +fn maps_duration_as_serde_struct_fields() { + let deserialize = DeserializeShapeGraph::for_type::(); + let ShapeRef::Definition(id) = deserialize.root else { + panic!("duration should produce a named definition"); + }; + let DeserializeDefinitionKind::Struct(shape) = &deserialize.definition(id).unwrap().kind else { + panic!("duration definition should be a struct"); + }; + + assert!(shape.attributes.deny_unknown_fields); + assert_eq!(shape.fields[0].name, "secs"); + assert_eq!( + shape.fields[0].wire_shape, + FieldWireShape::Value(ShapeRef::U64) + ); + assert_eq!(shape.fields[1].name, "nanos"); + assert_eq!( + shape.fields[1].wire_shape, + FieldWireShape::Value(ShapeRef::U32) + ); +} + +#[test] +fn supports_serde_tuple_arity() { + type Tuple16 = ( + u8, + u8, + u8, + u8, + u8, + u8, + u8, + u8, + u8, + u8, + u8, + u8, + u8, + u8, + u8, + u8, + ); + + let ShapeRef::Tuple(items) = SerializeShapeGraph::for_type::().root else { + panic!("16-element tuple should produce a tuple shape"); + }; + assert_eq!(items, vec![ShapeRef::U8; 16]); +} + #[test] fn maps_common_core_and_alloc_shapes() { assert_eq!( @@ -145,8 +325,39 @@ fn maps_common_std_shapes() { SerializeShapeGraph::for_type::().root, ShapeRef::String ); + let ipv4_binary = ShapeRef::Array { + item: Box::new(ShapeRef::U8), + len: 4, + }; + assert_eq!( + SerializeShapeGraph::for_type::().root, + ShapeRef::union([ShapeRef::String, ipv4_binary.clone()]) + ); + + let socket = DeserializeShapeGraph::for_type::(); + let ShapeRef::Union(root) = &socket.root else { + panic!("socket address should reflect human-readable and binary shapes"); + }; + assert!(root.alternatives().contains(&ShapeRef::String)); + let definition_id = root + .alternatives() + .iter() + .find_map(|shape| match shape { + ShapeRef::Definition(id) => Some(*id), + _ => None, + }) + .expect("binary socket shape should be a named enum"); + let DeserializeDefinitionKind::Enum(shape) = &socket.definition(definition_id).unwrap().kind + else { + panic!("binary socket shape should be an enum"); + }; + assert_eq!(shape.repr, Tagging::External); + assert_eq!(shape.variants[0].name, "V4"); + let crate::DeserializeVariantContent::Fields(fields) = &shape.variants[0].content else { + panic!("V4 should contain its binary socket tuple"); + }; assert_eq!( - DeserializeShapeGraph::for_type::().root, - ShapeRef::String + fields[0].wire_shape, + FieldWireShape::Value(ShapeRef::Tuple(vec![ipv4_binary, ShapeRef::U16])) ); } diff --git a/tests/derive/Cargo.toml b/tests/derive/Cargo.toml index 8cef4bc..ab66558 100644 --- a/tests/derive/Cargo.toml +++ b/tests/derive/Cargo.toml @@ -23,7 +23,9 @@ rust-version.workspace = true release = false [dependencies] -serde-shape = { workspace = true, features = ["derive"] } +renamed-shape = { package = "serde-shape", path = "../../serde-shape", features = [ + "derive", +] } [dev-dependencies] insta = { workspace = true } diff --git a/tests/derive/tests/derive.rs b/tests/derive/tests/derive.rs index cce38a3..8075bc1 100644 --- a/tests/derive/tests/derive.rs +++ b/tests/derive/tests/derive.rs @@ -14,16 +14,16 @@ #![allow(dead_code)] -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; +use renamed_shape::DeserializeDefinitionKind; +use renamed_shape::DeserializeShape; +use renamed_shape::DeserializeVariantContent; +use renamed_shape::FieldMember; +use renamed_shape::FieldWireShape; +use renamed_shape::OpaqueReason; +use renamed_shape::SerializeDefinitionKind; +use renamed_shape::SerializeShape; +use renamed_shape::SerializeVariantContent; +use renamed_shape::ShapeRef; #[derive(DeserializeShape)] #[serde( @@ -89,6 +89,27 @@ struct Recursive { child: Option>, } +#[derive(SerializeShape, DeserializeShape)] +struct RecursiveGeneric { + value: T, + child: Option>>, +} + +trait HasValue { + type Value; +} + +struct ValueProvider; + +impl HasValue for ValueProvider { + type Value = u16; +} + +#[derive(SerializeShape, DeserializeShape)] +struct AssociatedValue { + value: T::Value, +} + #[derive(SerializeShape, DeserializeShape)] #[serde(rename(serialize = "wire-output", deserialize = "wire-input"))] struct SplitIo { @@ -184,10 +205,28 @@ fn snapshots_recursive_type_reusing_the_same_definition() { insta::assert_debug_snapshot!(Recursive::deserialize_shape()); } +#[test] +fn derives_recursive_generic_shapes_without_cyclic_bounds() { + let serialize = RecursiveGeneric::::serialize_shape(); + let deserialize = RecursiveGeneric::::deserialize_shape(); + + assert_eq!(serialize.definitions.len(), 1); + assert_eq!(deserialize.definitions.len(), 1); +} + +#[test] +fn derives_shape_bounds_for_associated_values() { + let serialize = AssociatedValue::::serialize_shape(); + let deserialize = AssociatedValue::::deserialize_shape(); + + assert_eq!(serialize.definitions.len(), 1); + assert_eq!(deserialize.definitions.len(), 1); +} + #[test] fn exposes_deserialize_field_metadata() { let shape = SplitIo::deserialize_shape(); - let serde_shape::ShapeRef::Definition(id) = shape.root else { + let renamed_shape::ShapeRef::Definition(id) = shape.root else { panic!("root shape should be a definition"); }; let definition = shape.definition(id).expect("definition exists"); @@ -219,7 +258,7 @@ fn exposes_deserialize_field_metadata() { #[test] fn exposes_serialize_field_metadata() { let shape = SplitIo::serialize_shape(); - let serde_shape::ShapeRef::Definition(id) = shape.root else { + let renamed_shape::ShapeRef::Definition(id) = shape.root else { panic!("root shape should be a definition"); }; let definition = shape.definition(id).expect("definition exists"); @@ -255,7 +294,7 @@ fn exposes_serialize_field_metadata() { #[test] fn exposes_deserialize_variant_metadata() { let shape = SplitEnum::deserialize_shape(); - let serde_shape::ShapeRef::Definition(id) = shape.root else { + let renamed_shape::ShapeRef::Definition(id) = shape.root else { panic!("root shape should be a definition"); }; let definition = shape.definition(id).expect("definition exists"); @@ -294,7 +333,7 @@ fn exposes_deserialize_variant_metadata() { #[test] fn exposes_serialize_variant_metadata() { let shape = SplitEnum::serialize_shape(); - let serde_shape::ShapeRef::Definition(id) = shape.root else { + let renamed_shape::ShapeRef::Definition(id) = shape.root else { panic!("root shape should be a definition"); }; let definition = shape.definition(id).expect("definition exists"); @@ -337,16 +376,16 @@ fn derives_one_direction_without_requiring_the_other_direction() { assert!(matches!( serialize_shape.definition(match serialize_shape.root { - serde_shape::ShapeRef::Definition(id) => id, + renamed_shape::ShapeRef::Definition(id) => id, _ => panic!("serialize root shape should be a definition"), }), - Some(serde_shape::SerializeDefinitionShape { .. }) + Some(renamed_shape::SerializeDefinitionShape { .. }) )); assert!(matches!( deserialize_shape.definition(match deserialize_shape.root { - serde_shape::ShapeRef::Definition(id) => id, + renamed_shape::ShapeRef::Definition(id) => id, _ => panic!("deserialize root shape should be a definition"), }), - Some(serde_shape::DeserializeDefinitionShape { .. }) + Some(renamed_shape::DeserializeDefinitionShape { .. }) )); } diff --git a/tests/derive/tests/serde_compat.rs b/tests/derive/tests/serde_compat.rs index 3783685..f2dba8f 100644 --- a/tests/derive/tests/serde_compat.rs +++ b/tests/derive/tests/serde_compat.rs @@ -12,21 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. +use renamed_shape::DeserializeDefinitionKind; +use renamed_shape::DeserializeFieldShape; +use renamed_shape::DeserializeShape; +use renamed_shape::DeserializeVariantContent; +use renamed_shape::DeserializeVariantShape; +use renamed_shape::FieldWireShape; +use renamed_shape::OpaqueReason; +use renamed_shape::SerializeDefinitionKind; +use renamed_shape::SerializeFieldShape; +use renamed_shape::SerializeShape; +use renamed_shape::SerializeVariantContent; +use renamed_shape::SerializeVariantShape; +use renamed_shape::ShapeRef; 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); diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index b2adfaa..6182945 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -28,6 +28,7 @@ serde-shape = { workspace = true, features = ["derive", "std"] } [dev-dependencies] insta = { workspace = true } serde = { workspace = true } +serde_test = { workspace = true } toml_edit = { workspace = true } [lints] diff --git a/tests/integration/tests/serde_builtins.rs b/tests/integration/tests/serde_builtins.rs new file mode 100644 index 0000000..3762b00 --- /dev/null +++ b/tests/integration/tests/serde_builtins.rs @@ -0,0 +1,73 @@ +// 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 std::net::Ipv4Addr; +use std::net::SocketAddr; +use std::net::SocketAddrV4; + +use serde_test::Configure; +use serde_test::Token; +use serde_test::assert_ser_tokens; + +#[test] +fn byte_slices_use_serde_sequence_calls() { + let bytes = [1_u8, 2]; + assert_ser_tokens( + &&bytes[..], + &[ + Token::Seq { len: Some(2) }, + Token::U8(1), + Token::U8(2), + Token::SeqEnd, + ], + ); +} + +#[test] +fn network_types_switch_between_readable_and_compact_calls() { + let ipv4 = Ipv4Addr::new(127, 0, 0, 1); + assert_ser_tokens(&ipv4.readable(), &[Token::Str("127.0.0.1")]); + assert_ser_tokens( + &ipv4.compact(), + &[ + Token::Tuple { len: 4 }, + Token::U8(127), + Token::U8(0), + Token::U8(0), + Token::U8(1), + Token::TupleEnd, + ], + ); + + let socket = SocketAddr::V4(SocketAddrV4::new(ipv4, 8080)); + assert_ser_tokens(&socket.readable(), &[Token::Str("127.0.0.1:8080")]); + assert_ser_tokens( + &socket.compact(), + &[ + Token::NewtypeVariant { + name: "SocketAddr", + variant: "V4", + }, + Token::Tuple { len: 2 }, + Token::Tuple { len: 4 }, + Token::U8(127), + Token::U8(0), + Token::U8(0), + Token::U8(1), + Token::TupleEnd, + Token::U16(8080), + Token::TupleEnd, + ], + ); +} diff --git a/tests/integration/tests/snapshots/configenv__snapshots_config_shape.snap b/tests/integration/tests/snapshots/configenv__snapshots_config_shape.snap index 8dd1715..a7be5b3 100644 --- a/tests/integration/tests/snapshots/configenv__snapshots_config_shape.snap +++ b/tests/integration/tests/snapshots/configenv__snapshots_config_shape.snap @@ -49,7 +49,7 @@ DeserializeShapeGraph { wire_shape: Value( Definition( ShapeId( - 2, + 3, ), ), ), @@ -66,7 +66,7 @@ DeserializeShapeGraph { wire_shape: Value( Definition( ShapeId( - 7, + 8, ), ), ), @@ -121,7 +121,16 @@ DeserializeShapeGraph { "listen_data_addr", ], wire_shape: Value( - String, + Union( + [ + String, + Definition( + ShapeId( + 2, + ), + ), + ], + ), ), default: Path( "default_listen_data_addr", @@ -137,7 +146,16 @@ DeserializeShapeGraph { ], wire_shape: Value( Option( - String, + Union( + [ + String, + Definition( + ShapeId( + 2, + ), + ), + ], + ), ), ), default: None, @@ -189,6 +207,101 @@ DeserializeShapeGraph { id: ShapeId( 2, ), + type_name: DeserializeTypeName { + rust_name: "core::net::socket_addr::SocketAddr", + name: "SocketAddr", + }, + kind: Enum( + DeserializeEnumShape { + repr: External, + variants: [ + DeserializeVariantShape { + rust_name: "V4", + name: "V4", + aliases: [ + "V4", + ], + style: Newtype, + content: Fields( + [ + DeserializeFieldShape { + member: Unnamed( + 0, + ), + name: "0", + aliases: [ + "0", + ], + wire_shape: Value( + Tuple( + [ + Array { + item: U8, + len: 4, + }, + U16, + ], + ), + ), + default: None, + }, + ], + ), + other: false, + untagged: false, + }, + DeserializeVariantShape { + rust_name: "V6", + name: "V6", + aliases: [ + "V6", + ], + style: Newtype, + content: Fields( + [ + DeserializeFieldShape { + member: Unnamed( + 0, + ), + name: "0", + aliases: [ + "0", + ], + wire_shape: Value( + Tuple( + [ + Array { + item: U8, + len: 16, + }, + U16, + ], + ), + ), + default: None, + }, + ], + ), + other: false, + untagged: false, + }, + ], + attributes: DeserializeContainerAttributes { + tagging: External, + deny_unknown_fields: false, + default: None, + has_flatten: false, + transparent: false, + expecting: None, + non_exhaustive: false, + }, + }, + ), + }, + DeserializeDefinitionShape { + id: ShapeId( + 3, + ), type_name: DeserializeTypeName { rust_name: "configenv::StorageConfig", name: "StorageConfig", @@ -208,7 +321,7 @@ DeserializeShapeGraph { wire_shape: Value( Definition( ShapeId( - 3, + 4, ), ), ), @@ -288,7 +401,7 @@ DeserializeShapeGraph { Option( Definition( ShapeId( - 4, + 5, ), ), ), @@ -310,7 +423,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 3, + 4, ), type_name: DeserializeTypeName { rust_name: "configenv::StorageBackend", @@ -406,7 +519,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 4, + 5, ), type_name: DeserializeTypeName { rust_name: "configenv::DiskThrottle", @@ -453,7 +566,7 @@ DeserializeShapeGraph { wire_shape: Value( Definition( ShapeId( - 5, + 6, ), ), ), @@ -474,7 +587,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 5, + 6, ), type_name: DeserializeTypeName { rust_name: "configenv::CounterConfig", @@ -495,7 +608,7 @@ DeserializeShapeGraph { wire_shape: Value( Definition( ShapeId( - 6, + 7, ), ), ), @@ -529,7 +642,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 6, + 7, ), type_name: DeserializeTypeName { rust_name: "configenv::CounterMode", @@ -580,7 +693,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 7, + 8, ), type_name: DeserializeTypeName { rust_name: "configenv::TelemetryConfig", @@ -601,7 +714,7 @@ DeserializeShapeGraph { wire_shape: Value( Definition( ShapeId( - 8, + 9, ), ), ), @@ -619,7 +732,7 @@ DeserializeShapeGraph { Option( Definition( ShapeId( - 10, + 11, ), ), ), @@ -638,7 +751,7 @@ DeserializeShapeGraph { Option( Definition( ShapeId( - 12, + 13, ), ), ), @@ -660,7 +773,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 8, + 9, ), type_name: DeserializeTypeName { rust_name: "configenv::LogsConfig", @@ -681,7 +794,7 @@ DeserializeShapeGraph { wire_shape: Flatten( Definition( ShapeId( - 9, + 10, ), ), ), @@ -715,7 +828,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 9, + 10, ), type_name: DeserializeTypeName { rust_name: "configenv::LogSink", @@ -826,7 +939,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 10, + 11, ), type_name: DeserializeTypeName { rust_name: "configenv::TracesConfig", @@ -861,7 +974,7 @@ DeserializeShapeGraph { Option( Definition( ShapeId( - 11, + 12, ), ), ), @@ -883,7 +996,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 11, + 12, ), type_name: DeserializeTypeName { rust_name: "configenv::OpentelemetryTracesConfig", @@ -921,7 +1034,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 12, + 13, ), type_name: DeserializeTypeName { rust_name: "configenv::MetricsConfig", @@ -943,7 +1056,7 @@ DeserializeShapeGraph { Option( Definition( ShapeId( - 13, + 14, ), ), ), @@ -965,7 +1078,7 @@ DeserializeShapeGraph { }, DeserializeDefinitionShape { id: ShapeId( - 13, + 14, ), type_name: DeserializeTypeName { rust_name: "configenv::OpentelemetryMetricsConfig", diff --git a/tests/integration/tests/snapshots/configenv__snapshots_env_options.snap b/tests/integration/tests/snapshots/configenv__snapshots_env_options.snap index b5b8b75..e9950fd 100644 --- a/tests/integration/tests/snapshots/configenv__snapshots_env_options.snap +++ b/tests/integration/tests/snapshots/configenv__snapshots_env_options.snap @@ -9,7 +9,7 @@ expression: "env_options::(\"PERCAS_CONFIG\")" "server", "advertise_data_addr", ], - value_kind: "string", + value_kind: "string|enum", optional: true, condition: None, }, @@ -49,7 +49,7 @@ expression: "env_options::(\"PERCAS_CONFIG\")" "server", "listen_data_addr", ], - value_kind: "string", + value_kind: "string|enum", optional: true, condition: None, },