diff --git a/instant-xml-macros/src/de.rs b/instant-xml-macros/src/de.rs index a273c2e..e8d3f2d 100644 --- a/instant-xml-macros/src/de.rs +++ b/instant-xml-macros/src/de.rs @@ -234,7 +234,6 @@ fn deserialize_struct( let mut declare_values = TokenStream::new(); let mut return_val = TokenStream::new(); let mut direct = TokenStream::new(); - let mut after_loop = TokenStream::new(); let mut borrowed = BTreeSet::new(); for (index, field) in fields.named.iter().enumerate() { @@ -264,7 +263,6 @@ fn deserialize_struct( field_meta, &input.ident, &container_meta, - &mut after_loop, ); if let Err(err) = result { @@ -363,7 +361,6 @@ fn deserialize_struct( node => return Err(Error::UnexpectedNode(format!("{:?} in {}", node, #ident_str))), } } - #after_loop *into = Some(Self { #return_val }); Ok(()) @@ -404,7 +401,6 @@ fn deserialize_inline_struct( let mut declare_values = TokenStream::new(); let mut return_val = TokenStream::new(); let mut direct = TokenStream::new(); - let mut after_loop = TokenStream::new(); let mut borrowed = BTreeSet::new(); let mut matches = TokenStream::new(); @@ -437,7 +433,6 @@ fn deserialize_inline_struct( field_meta, &input.ident, &meta, - &mut after_loop, ); let data = match result { @@ -553,7 +548,6 @@ fn named_field<'a>( mut field_meta: FieldMeta, type_name: &Ident, container_meta: &ContainerMeta, - after_loop: &mut TokenStream, ) -> Result, syn::Error> { let field_name = field.ident.as_ref().unwrap(); let field_tag = field_meta.tag; @@ -650,16 +644,6 @@ fn named_field<'a>( <#no_lifetime_type as FromXml>::deserialize(&mut #val_name, #field_str, &mut nested)?; } )); - // We can only enter this FromXml impl if the caller found the opening - // tag, so if we don't see the text node before the closing tag that is - // implied by terminating the loop, we need to populate the - // direct field with the implied empty text node. - after_loop.extend(quote!( - if !seen_direct { - let mut nested = deserializer.for_node(Node::Text("".into())); - <#no_lifetime_type as FromXml>::deserialize(&mut #val_name, #field_str, &mut nested)?; - } - )); } else { tokens.r#match.extend(quote!( __Elements::#enum_name => match <#no_lifetime_type as FromXml>::KIND { @@ -700,9 +684,24 @@ fn named_field<'a>( } }; - return_val.extend(quote!( - #field_name: #val_name.try_done(#field_str)?, - )); + if !field_meta.direct { + return_val.extend(quote!( + #field_name: #val_name.try_done(#field_str)?, + )); + } else { + return_val.extend(quote!( + #field_name: match #val_name.try_done(#field_str) { + Ok(value) => value, + Err(Error::MissingValue(_)) => { + let mut acc = <#no_lifetime_type as FromXml>::Accumulator::default(); + let mut nested = deserializer.for_node(Node::Text("".into())); + <#no_lifetime_type as FromXml>::deserialize(&mut acc, #field_str, &mut nested)?; + acc.try_done(#field_str)? + } + Err(e) => return Err(e), + } + )); + } Ok(FieldData { field_name, diff --git a/instant-xml-macros/src/ser.rs b/instant-xml-macros/src/ser.rs index e92be6d..e17e703 100644 --- a/instant-xml-macros/src/ser.rs +++ b/instant-xml-macros/src/ser.rs @@ -1,7 +1,7 @@ use std::collections::BTreeSet; use proc_macro2::TokenStream; -use quote::quote; +use quote::{quote, ToTokens}; use syn::spanned::Spanned; use super::{discard_lifetimes, meta_items, ContainerMeta, FieldMeta, Mode, VariantMeta}; @@ -168,32 +168,19 @@ fn serialize_struct( data: &syn::DataStruct, meta: ContainerMeta, ) -> proc_macro2::TokenStream { - let tag = meta.tag(); - let mut body = TokenStream::new(); - let mut attributes = TokenStream::new(); - let mut borrowed = BTreeSet::new(); + let mut out = StructOutput::default(); match &data.fields { syn::Fields::Named(fields) => { - body.extend(quote!(serializer.end_start()?;)); - for field in &fields.named { - if let Err(err) = - named_field(field, &mut body, &mut attributes, &mut borrowed, &meta) - { - return err.to_compile_error(); - } + if let Err(err) = out.named_fields(fields, false, &meta) { + return err; } - body.extend(quote!(serializer.write_close(prefix, #tag)?;)); } syn::Fields::Unnamed(fields) => { - body.extend(quote!(serializer.end_start()?;)); - for (index, field) in fields.unnamed.iter().enumerate() { - if let Err(err) = unnamed_field(field, index, &mut body, &mut borrowed) { - return err.to_compile_error(); - } + if let Err(err) = out.unnamed_fields(fields, false, &meta) { + return err; } - body.extend(quote!(serializer.write_close(prefix, #tag)?;)); } - syn::Fields::Unit => body.extend(quote!(serializer.end_empty()?;)), + syn::Fields::Unit => out.body.extend(quote!(serializer.end_empty()?;)), } let default_namespace = meta.default_namespace(); @@ -234,8 +221,7 @@ fn serialize_struct( let old = serializer.push(new)?; // Finalize start element - #attributes - #body + #out serializer.pop(old); Ok(()) @@ -266,35 +252,19 @@ fn serialize_inline_struct( .to_compile_error(); } - let mut body = TokenStream::new(); - let mut attributes = TokenStream::new(); - let mut borrowed = BTreeSet::new(); + let mut out = StructOutput::default(); match &data.fields { syn::Fields::Named(fields) => { - for field in &fields.named { - if let Err(err) = - named_field(field, &mut body, &mut attributes, &mut borrowed, &meta) - { - return err.to_compile_error(); - } - - if !attributes.is_empty() { - return syn::Error::new( - input.span(), - "no attributes allowed on inline structs", - ) - .to_compile_error(); - } + if let Err(err) = out.named_fields(fields, true, &meta) { + return err; } } syn::Fields::Unnamed(fields) => { - for (index, field) in fields.unnamed.iter().enumerate() { - if let Err(err) = unnamed_field(field, index, &mut body, &mut borrowed) { - return err.to_compile_error(); - } + if let Err(err) = out.unnamed_fields(fields, true, &meta) { + return err; } } - syn::Fields::Unit => body.extend(quote!(serializer.end_empty()?;)), + syn::Fields::Unit => out.body.extend(quote!(serializer.end_empty()?;)), } let mut generics = input.generics.clone(); @@ -313,57 +283,135 @@ fn serialize_inline_struct( field: Option<::instant_xml::Id<'_>>, serializer: &mut instant_xml::Serializer, ) -> ::std::result::Result<(), instant_xml::Error> { - #body + #out Ok(()) } }; ) } -fn named_field( - field: &syn::Field, - body: &mut TokenStream, - attributes: &mut TokenStream, - borrowed: &mut BTreeSet, - meta: &ContainerMeta, -) -> Result<(), syn::Error> { - let field_name = field.ident.as_ref().unwrap(); - let field_meta = match FieldMeta::from_field(field, meta) { - Ok(meta) => meta, - Err(err) => { - body.extend(err.into_compile_error()); - return Ok(()); +#[derive(Default)] +struct StructOutput { + body: TokenStream, + attributes: TokenStream, + borrowed: BTreeSet, +} + +impl StructOutput { + fn named_fields( + &mut self, + fields: &syn::FieldsNamed, + inline: bool, + meta: &ContainerMeta, + ) -> Result<(), proc_macro2::TokenStream> { + let fields = fields + .named + .iter() + .map(|field| FieldMeta::from_field(field, meta).map(|meta| (field, meta))) + .collect::, _>>() + .map_err(|err| err.to_compile_error())?; + + let mut direct = None; + for (field, field_meta) in &fields { + if direct.is_some() { + return Err( + syn::Error::new(field.span(), "direct field must be the last") + .into_compile_error(), + ); + } + + if field_meta.direct { + direct = Some(field.ident.as_ref().unwrap()); + } } - }; - let tag = field_meta.tag; - let default_ns = match &meta.ns.uri { - Some(ns) => quote!(#ns), - None => quote!(""), - }; + if !inline { + self.body.extend(match direct { + Some(field) => quote!( + match self.#field.present() { + true => serializer.end_start()?, + false => serializer.end_empty()?, + } + ), + None => quote!(serializer.end_start()?;), + }) + } - if field_meta.attribute { - if field_meta.direct { - return Err(syn::Error::new( - field.span(), - "direct attribute is not supported on attributes", - )); + for (field, field_meta) in fields { + if let Err(err) = self.named_field(field, field_meta, meta) { + return Err(err.to_compile_error()); + } + + if inline && !self.attributes.is_empty() { + return Err(syn::Error::new( + field.span(), + "no attributes allowed on inline structs", + ) + .to_compile_error()); + } + } + + if !inline { + let tag = meta.tag(); + self.body.extend(match direct { + Some(field) => quote!( + match self.#field.present() { + true => serializer.write_close(prefix, #tag)?, + false => (), + } + ), + None => quote!(serializer.write_close(prefix, #tag)?;), + }); } - let (ns, error) = match &field_meta.ns.uri { - Some(Namespace::Path(path)) => match path.get_ident() { - Some(prefix) => match &meta.ns.prefixes.get(&prefix.to_string()) { - Some(ns) => (quote!(#ns), quote!()), + Ok(()) + } + + fn named_field( + &mut self, + field: &syn::Field, + field_meta: FieldMeta, + meta: &ContainerMeta, + ) -> Result<(), syn::Error> { + let field_name = field.ident.as_ref().unwrap(); + + let tag = field_meta.tag; + let default_ns = match &meta.ns.uri { + Some(ns) => quote!(#ns), + None => quote!(""), + }; + + if field_meta.attribute { + if field_meta.direct { + return Err(syn::Error::new( + field.span(), + "direct attribute is not supported on attributes", + )); + } + + let (ns, error) = match &field_meta.ns.uri { + Some(Namespace::Path(path)) => match path.get_ident() { + Some(prefix) => match &meta.ns.prefixes.get(&prefix.to_string()) { + Some(ns) => (quote!(#ns), quote!()), + None => ( + quote!(""), + syn::Error::new( + field_meta.ns.uri.span(), + format!("unknown prefix `{prefix}` (prefix must be defined on the field's type)"), + ) + .into_compile_error(), + ), + }, None => ( quote!(""), syn::Error::new( field_meta.ns.uri.span(), - format!("unknown prefix `{prefix}` (prefix must be defined on the field's type)"), + "attribute namespace must be a prefix identifier", ) .into_compile_error(), ), }, - None => ( + Some(Namespace::Literal(_)) => ( quote!(""), syn::Error::new( field_meta.ns.uri.span(), @@ -371,90 +419,110 @@ fn named_field( ) .into_compile_error(), ), - }, - Some(Namespace::Literal(_)) => ( - quote!(""), - syn::Error::new( - field_meta.ns.uri.span(), - "attribute namespace must be a prefix identifier", - ) - .into_compile_error(), - ), - None => (default_ns, quote!()), + None => (default_ns, quote!()), + }; + + self.attributes.extend(quote!( + #error + if self.#field_name.present() { + serializer.write_attr(#tag, #ns, &self.#field_name)?; + } + )); + return Ok(()); + } + + let ns = match field_meta.ns.uri { + Some(ref ns) => quote!(#ns), + None => default_ns, }; - attributes.extend(quote!( - #error - if self.#field_name.present() { - serializer.write_attr(#tag, #ns, &self.#field_name)?; + let mut no_lifetime_type = field.ty.clone(); + discard_lifetimes(&mut no_lifetime_type, &mut self.borrowed, false, true); + if let Some(with) = field_meta.serialize_with { + if field_meta.direct { + return Err(syn::Error::new( + field.span(), + "direct serialization is not supported with `serialize_with`", + )); } - )); - return Ok(()); + + let path = with.to_string(); + let path = syn::parse_str::(path.trim_matches('"')).map_err(|err| { + syn::Error::new( + with.span(), + format!("failed to parse serialize_with as path: {err}"), + ) + })?; + + self.body + .extend(quote!(#path(&self.#field_name, serializer)?;)); + return Ok(()); + } else if field_meta.direct { + self.body.extend(quote!( + <#no_lifetime_type as ToXml>::serialize( + &self.#field_name, None, serializer + )?; + )); + } else { + self.body.extend(quote!( + <#no_lifetime_type as ToXml>::serialize( + &self.#field_name, + Some(::instant_xml::Id { ns: #ns, name: #tag }), + serializer, + )?; + )); + } + + Ok(()) } - let ns = match field_meta.ns.uri { - Some(ref ns) => quote!(#ns), - None => default_ns, - }; + fn unnamed_fields( + &mut self, + fields: &syn::FieldsUnnamed, + inline: bool, + meta: &ContainerMeta, + ) -> Result<(), proc_macro2::TokenStream> { + if !inline { + self.body.extend(quote!(serializer.end_start()?;)); + } + + for (index, field) in fields.unnamed.iter().enumerate() { + if let Err(err) = self.unnamed_field(field, index) { + return Err(err.to_compile_error()); + } + } + + if !inline { + let tag = meta.tag(); + self.body + .extend(quote!(serializer.write_close(prefix, #tag)?;)); + } - let mut no_lifetime_type = field.ty.clone(); - discard_lifetimes(&mut no_lifetime_type, borrowed, false, true); - if let Some(with) = field_meta.serialize_with { - if field_meta.direct { + Ok(()) + } + + fn unnamed_field(&mut self, field: &syn::Field, index: usize) -> Result<(), syn::Error> { + if !field.attrs.is_empty() { return Err(syn::Error::new( field.span(), - "direct serialization is not supported with `serialize_with`", + "unnamed fields cannot have attributes", )); } - let path = with.to_string(); - let path = syn::parse_str::(path.trim_matches('"')).map_err(|err| { - syn::Error::new( - with.span(), - format!("failed to parse serialize_with as path: {err}"), - ) - })?; - - body.extend(quote!(#path(&self.#field_name, serializer)?;)); - return Ok(()); - } else if field_meta.direct { - body.extend(quote!( - <#no_lifetime_type as ToXml>::serialize( - &self.#field_name, None, serializer - )?; + let mut no_lifetime_type = field.ty.clone(); + discard_lifetimes(&mut no_lifetime_type, &mut self.borrowed, false, true); + let index = syn::Index::from(index); + self.body.extend(quote!( + self.#index.serialize(None, serializer)?; )); - } else { - body.extend(quote!( - <#no_lifetime_type as ToXml>::serialize( - &self.#field_name, - Some(::instant_xml::Id { ns: #ns, name: #tag }), - serializer, - )?; - )); - } - Ok(()) + Ok(()) + } } -fn unnamed_field( - field: &syn::Field, - index: usize, - body: &mut TokenStream, - borrowed: &mut BTreeSet, -) -> Result<(), syn::Error> { - if !field.attrs.is_empty() { - return Err(syn::Error::new( - field.span(), - "unnamed fields cannot have attributes", - )); +impl ToTokens for StructOutput { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.attributes.to_tokens(tokens); + self.body.to_tokens(tokens); } - - let mut no_lifetime_type = field.ty.clone(); - discard_lifetimes(&mut no_lifetime_type, borrowed, false, true); - let index = syn::Index::from(index); - body.extend(quote!( - self.#index.serialize(None, serializer)?; - )); - - Ok(()) } diff --git a/instant-xml/tests/direct-no-value.rs b/instant-xml/tests/direct-no-value.rs new file mode 100644 index 0000000..3339d7f --- /dev/null +++ b/instant-xml/tests/direct-no-value.rs @@ -0,0 +1,21 @@ +use instant_xml::{from_str, to_string, FromXml, ToXml}; + +#[derive(ToXml, FromXml, Debug, PartialEq, Eq)] +struct Foo { + #[xml(attribute)] + attribute: Option, + #[xml(direct)] + direct: Option, +} + +#[test] +fn serde_direct_no_value_test() { + let v = Foo { + attribute: Some("Attribute text".to_string()), + direct: None, + }; + let xml = r#""#; + + assert_eq!(xml, to_string(&v).unwrap()); //this fails because the serializer still writes "" + assert_eq!(from_str::(xml).unwrap(), v); //this fails because the serializer still writes Some("") to direct +}