From 786f035f765055c57ecbeee442cbd7ae388e4964 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Wed, 22 Feb 2023 17:23:19 +0100 Subject: [PATCH] Implement transparent mode for deserialization --- instant-xml-macros/src/de.rs | 206 +++++++++++++++++++++++++++++-- instant-xml/src/impls.rs | 6 + instant-xml/src/lib.rs | 2 +- instant-xml/tests/transparent.rs | 12 +- 4 files changed, 209 insertions(+), 17 deletions(-) diff --git a/instant-xml-macros/src/de.rs b/instant-xml-macros/src/de.rs index 88d4064..abb983b 100644 --- a/instant-xml-macros/src/de.rs +++ b/instant-xml-macros/src/de.rs @@ -20,13 +20,28 @@ pub(crate) fn from_xml(input: &syn::DeriveInput) -> TokenStream { syn::Fields::Unnamed(fields) => deserialize_tuple_struct(input, fields, meta), syn::Fields::Unit => deserialize_unit_struct(input, &meta), }, + (syn::Data::Struct(data), Some(Mode::Transparent)) => match &data.fields { + syn::Fields::Named(fields) => deserialize_inline_struct(input, fields, meta), + _ => syn::Error::new( + input.span(), + "inline mode is only supported on types with named fields", + ) + .to_compile_error(), + }, (syn::Data::Enum(data), Some(Mode::Scalar)) => deserialize_scalar_enum(input, data, meta), (syn::Data::Enum(data), Some(Mode::Forward)) => deserialize_forward_enum(input, data, meta), - (syn::Data::Struct(_), _) => { - syn::Error::new(input.span(), "no enum mode allowed on struct type").to_compile_error() - } + (syn::Data::Struct(_), Some(mode)) => syn::Error::new( + input.span(), + format_args!("{mode:?} mode not allowed on struct type"), + ) + .to_compile_error(), + (syn::Data::Enum(_), Some(mode)) => syn::Error::new( + input.span(), + format_args!("{mode:?} mode not allowed on enum type"), + ) + .to_compile_error(), (syn::Data::Enum(_), None) => { - syn::Error::new(input.span(), "missing enum mode").to_compile_error() + syn::Error::new(input.span(), "missing mode").to_compile_error() } _ => todo!(), } @@ -348,9 +363,168 @@ fn deserialize_struct( ) } +fn deserialize_inline_struct( + input: &syn::DeriveInput, + fields: &syn::FieldsNamed, + meta: ContainerMeta, +) -> TokenStream { + if !meta.ns.prefixes.is_empty() { + return syn::Error::new( + input.span(), + "inline structs cannot have namespace declarations", + ) + .to_compile_error(); + } else if let Some(ns) = meta.ns.uri { + return syn::Error::new( + ns.span(), + "inline structs cannot have namespace declarations", + ) + .to_compile_error(); + } else if let Some(rename) = meta.rename { + return syn::Error::new(rename.span(), "inline structs cannot be renamed") + .to_compile_error(); + } + + // Varying values + let mut elements_tokens = Tokens::default(); + + // Common values + let mut declare_values = TokenStream::new(); + let mut return_val = TokenStream::new(); + let mut direct = TokenStream::new(); + + let mut borrowed = BTreeSet::new(); + let mut matches = TokenStream::new(); + let mut acc_field_defs = TokenStream::new(); + let mut acc_field_inits = TokenStream::new(); + let mut deserialize = TokenStream::new(); + let mut acc_field_defaults = TokenStream::new(); + for (index, field) in fields.named.iter().enumerate() { + let field_meta = match FieldMeta::from_field(field, &meta) { + Ok(meta) => meta, + Err(err) => return err.into_compile_error(), + }; + + if field_meta.direct { + return syn::Error::new(field.span(), "inline structs cannot have a direct field") + .to_compile_error(); + } else if field_meta.attribute { + return syn::Error::new(field.span(), "inline structs cannot have attribute fields") + .to_compile_error(); + } + + let result = named_field( + field, + index, + &mut declare_values, + &mut return_val, + &mut elements_tokens, + &mut borrowed, + &mut direct, + field_meta, + &input.ident, + &meta, + ); + + let data = match result { + Ok(data) => data, + Err(err) => return err.into_compile_error(), + }; + + if !matches.is_empty() { + matches.extend(quote!(||)); + } + + let field_ty = data.no_lifetime_type; + matches.extend(quote!( + #field_ty::matches(id, None) + )); + + let field_name = &field.ident; + acc_field_defs.extend(quote!(#field_name: <#field_ty as FromXml<'xml>>::Accumulator,)); + let field_str = format!("{}::{}", input.ident, data.field_name); + acc_field_inits.extend(quote!(#field_name: self.#field_name.try_done(#field_str)?,)); + acc_field_defaults.extend(quote!(#field_name: Default::default(),)); + + if !deserialize.is_empty() { + deserialize.extend(quote!(else)); + } + if let Some(with) = data.deserialize_with { + deserialize.extend(quote!(if #field_ty::matches(current, None) { + #with(&mut into.#field_name, #field_str, deserializer)?; + })); + } else { + deserialize.extend(quote!(if #field_ty::matches(current, None) { + match <#field_ty as FromXml>::KIND { + Kind::Element => { + <#field_ty>::deserialize(&mut into.#field_name, #field_str, deserializer)?; + } + Kind::Scalar => { + <#field_ty>::deserialize(&mut into.#field_name, #field_str, deserializer)?; + deserializer.ignore()?; + } + } + })); + } + } + + // Attributes + let ident = &input.ident; + let accumulator = Ident::new(&format!("__{}Accumulator", ident), Span::call_site()); + let generics = meta.xml_generics(borrowed); + + let (xml_impl_generics, xml_ty_generics, _) = generics.split_for_impl(); + let (_, ty_generics, where_clause) = input.generics.split_for_impl(); + + quote!( + impl #xml_impl_generics FromXml<'xml> for #ident #ty_generics #where_clause { + #[inline] + fn matches(id: ::instant_xml::Id<'_>, _: Option<::instant_xml::Id<'_>>) -> bool { + #matches + } + + fn deserialize<'cx>( + into: &mut Self::Accumulator, + _: &'static str, + deserializer: &mut ::instant_xml::Deserializer<'cx, 'xml>, + ) -> Result<(), ::instant_xml::Error> { + use ::instant_xml::Kind; + + let current = deserializer.parent(); + #deserialize + + Ok(()) + } + + type Accumulator = #accumulator #xml_ty_generics; + const KIND: ::instant_xml::Kind = ::instant_xml::Kind::Element; + } + + struct #accumulator #xml_ty_generics #where_clause { + #acc_field_defs + } + + impl #xml_impl_generics ::instant_xml::Accumulate<#ident #ty_generics> for #accumulator #xml_ty_generics #where_clause { + fn try_done(self, field: &'static str) -> Result<#ident #ty_generics, ::instant_xml::Error> { + Ok(#ident { + #acc_field_inits + }) + } + } + + impl #xml_impl_generics Default for #accumulator #xml_ty_generics #where_clause { + fn default() -> Self { + Self { + #acc_field_defaults + } + } + } + ) +} + #[allow(clippy::too_many_arguments)] -fn named_field( - field: &syn::Field, +fn named_field<'a>( + field: &'a syn::Field, index: usize, declare_values: &mut TokenStream, return_val: &mut TokenStream, @@ -360,7 +534,7 @@ fn named_field( mut field_meta: FieldMeta, type_name: &Ident, container_meta: &ContainerMeta, -) -> Result<(), syn::Error> { +) -> Result, syn::Error> { let field_name = field.ident.as_ref().unwrap(); let field_tag = field_meta.tag; let default_ns = match &field_meta.ns.uri { @@ -424,7 +598,7 @@ fn named_field( let field_str = format!("{type_name}::{field_name}"); if !field_meta.attribute { - if let Some(with) = deserialize_with { + if let Some(with) = &deserialize_with { if field_meta.direct { return Err(syn::Error::new( field.span(), @@ -468,7 +642,7 @@ fn named_field( )); } - if let Some(with) = deserialize_with { + if let Some(with) = &deserialize_with { tokens.r#match.extend(quote!( __Attributes::#enum_name => { let mut nested = deserializer.nested(data); @@ -483,13 +657,23 @@ fn named_field( }, )); } - } + }; return_val.extend(quote!( #field_name: #enum_name.try_done(#field_str)?, )); - Ok(()) + Ok(FieldData { + field_name, + no_lifetime_type, + deserialize_with, + }) +} + +struct FieldData<'a> { + field_name: &'a Ident, + no_lifetime_type: syn::Type, + deserialize_with: Option, } fn deserialize_tuple_struct( diff --git a/instant-xml/src/impls.rs b/instant-xml/src/impls.rs index dbb54ec..79c42a7 100644 --- a/instant-xml/src/impls.rs +++ b/instant-xml/src/impls.rs @@ -375,6 +375,12 @@ pub struct OptionAccumulator> { marker: PhantomData, } +impl> OptionAccumulator { + pub fn get_mut(&mut self) -> &mut A { + &mut self.value + } +} + impl> Default for OptionAccumulator { fn default() -> Self { Self { diff --git a/instant-xml/src/lib.rs b/instant-xml/src/lib.rs index 7b26bef..0747bf3 100644 --- a/instant-xml/src/lib.rs +++ b/instant-xml/src/lib.rs @@ -9,7 +9,7 @@ pub mod de; mod impls; use de::Context; pub use de::Deserializer; -pub use impls::{display_to_xml, from_xml_str}; +pub use impls::{display_to_xml, from_xml_str, OptionAccumulator}; #[doc(hidden)] pub mod ser; pub use ser::Serializer; diff --git a/instant-xml/tests/transparent.rs b/instant-xml/tests/transparent.rs index 4ba018e..bd4f364 100644 --- a/instant-xml/tests/transparent.rs +++ b/instant-xml/tests/transparent.rs @@ -1,23 +1,23 @@ -use instant_xml::{to_string, ToXml}; +use instant_xml::{from_str, to_string, FromXml, ToXml}; -#[derive(Debug, Eq, PartialEq, ToXml)] +#[derive(Debug, Eq, FromXml, PartialEq, ToXml)] struct Wrapper { inline: Inline, } -#[derive(Debug, Eq, PartialEq, ToXml)] +#[derive(Debug, Eq, FromXml, PartialEq, ToXml)] #[xml(transparent)] struct Inline { foo: Foo, bar: Bar, } -#[derive(Debug, Eq, PartialEq, ToXml)] +#[derive(Debug, Eq, FromXml, PartialEq, ToXml)] struct Foo { i: u8, } -#[derive(Debug, Eq, PartialEq, ToXml)] +#[derive(Debug, Eq, FromXml, PartialEq, ToXml)] struct Bar { s: String, } @@ -32,6 +32,8 @@ fn inline() { }, }, }; + let xml = r#"42hello"#; assert_eq!(xml, to_string(&v).unwrap()); + assert_eq!(v, from_str(xml).unwrap()); }