diff --git a/instant-xml-macros/src/de.rs b/instant-xml-macros/src/de.rs index d1fa1cd..2f23113 100644 --- a/instant-xml-macros/src/de.rs +++ b/instant-xml-macros/src/de.rs @@ -196,9 +196,15 @@ fn deserialize_struct( // Common values let mut declare_values = TokenStream::new(); let mut return_val = TokenStream::new(); + let mut direct = TokenStream::new(); let mut borrowed = BTreeSet::new(); for (index, field) in fields.named.iter().enumerate() { + if !direct.is_empty() { + return syn::Error::new(field.span(), "direct field must be the last") + .into_compile_error(); + } + let field_meta = match FieldMeta::from_field(field, &container_meta) { Ok(meta) => meta, Err(err) => return err.into_compile_error(), @@ -216,6 +222,7 @@ fn deserialize_struct( &mut return_val, tokens, &mut borrowed, + &mut direct, field_meta, &container_meta, ); @@ -299,6 +306,7 @@ fn deserialize_struct( } } } + #direct node => return Err(Error::UnexpectedNode(format!("{:?}", node))), } } @@ -323,6 +331,7 @@ fn named_field( return_val: &mut TokenStream, tokens: &mut Tokens, borrowed: &mut BTreeSet, + direct: &mut TokenStream, mut field_meta: FieldMeta, container_meta: &ContainerMeta, ) -> Result<(), syn::Error> { @@ -353,19 +362,21 @@ fn named_field( discard_lifetimes(&mut no_lifetime_type, borrowed, field_meta.borrow, true); let enum_name = Ident::new(&format!("__Value{index}"), Span::call_site()); - tokens.r#enum.extend(quote!(#enum_name,)); + if !field_meta.direct { + tokens.r#enum.extend(quote!(#enum_name,)); - if !tokens.branches.is_empty() { - tokens.branches.extend(quote!(else)); + if !tokens.branches.is_empty() { + tokens.branches.extend(quote!(else)); + } + tokens.branches.extend(quote!( + if <#no_lifetime_type as FromXml>::KIND.matches(id, Id { ns: #ns, name: #field_tag }) + )); + + tokens.branches.extend(match field_meta.attribute { + true => quote!({ __Attributes::#enum_name }), + false => quote!({ __Elements::#enum_name }), + }); } - tokens.branches.extend(quote!( - if <#no_lifetime_type as FromXml>::KIND.matches(id, Id { ns: #ns, name: #field_tag }) - )); - - tokens.branches.extend(match field_meta.attribute { - true => quote!({ __Attributes::#enum_name }), - false => quote!({ __Elements::#enum_name }), - }); declare_values.extend(quote!( let mut #enum_name: Option<#no_lifetime_type> = None; @@ -386,12 +397,26 @@ fn named_field( if !field_meta.attribute { if let Some(with) = deserialize_with { + if field_meta.direct { + return Err(syn::Error::new( + field.span(), + "direct attribute is not supported deserialization functions", + )); + } + tokens.r#match.extend(quote!( __Elements::#enum_name => { let mut nested = deserializer.nested(data); #with(&mut nested, &mut #enum_name)?; }, )); + } else if field_meta.direct { + direct.extend(quote!( + Node::Text(text) => { + let mut nested = deserializer.for_node(Node::Text(text)); + FromXml::deserialize(&mut nested, &mut #enum_name)?; + } + )); } else { tokens.r#match.extend(quote!( __Elements::#enum_name => match <#no_lifetime_type as FromXml>::KIND { @@ -408,6 +433,13 @@ fn named_field( )); } } else { + if field_meta.direct { + return Err(syn::Error::new( + field.span(), + "direct attribute is not supported for attribute fields", + )); + } + if let Some(with) = deserialize_with { tokens.r#match.extend(quote!( __Attributes::#enum_name => { diff --git a/instant-xml-macros/src/lib.rs b/instant-xml-macros/src/lib.rs index 9ad4a1c..e4f972e 100644 --- a/instant-xml-macros/src/lib.rs +++ b/instant-xml-macros/src/lib.rs @@ -108,6 +108,7 @@ impl<'input> ContainerMeta<'input> { struct FieldMeta { attribute: bool, borrow: bool, + direct: bool, ns: NamespaceMeta, tag: TokenStream, serialize_with: Option, @@ -129,6 +130,7 @@ impl FieldMeta { match item { MetaItem::Attribute => meta.attribute = true, MetaItem::Borrow => meta.borrow = true, + MetaItem::Direct => meta.direct = true, MetaItem::Ns(ns) => meta.ns = ns, MetaItem::Rename(lit) => meta.tag = quote!(#lit), MetaItem::SerializeWith(lit) => meta.serialize_with = Some(lit), diff --git a/instant-xml-macros/src/meta.rs b/instant-xml-macros/src/meta.rs index bb168b7..c59c3e9 100644 --- a/instant-xml-macros/src/meta.rs +++ b/instant-xml-macros/src/meta.rs @@ -295,6 +295,9 @@ pub(crate) fn meta_items(attrs: &[syn::Attribute]) -> Vec<(MetaItem, Span)> { } else if id == "borrow" { items.push((MetaItem::Borrow, span)); MetaState::Comma + } else if id == "direct" { + items.push((MetaItem::Direct, span)); + MetaState::Comma } else if id == "ns" { MetaState::Ns } else if id == "rename" { @@ -477,6 +480,7 @@ impl fmt::Debug for Namespace { pub(crate) enum MetaItem { Attribute, Borrow, + Direct, Ns(NamespaceMeta), Rename(Literal), Mode(Mode), diff --git a/instant-xml-macros/src/ser.rs b/instant-xml-macros/src/ser.rs index d47d818..7fb9cb9 100644 --- a/instant-xml-macros/src/ser.rs +++ b/instant-xml-macros/src/ser.rs @@ -277,19 +277,6 @@ fn named_field( } }; - if let Some(with) = field_meta.serialize_with { - 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(()); - } - let tag = field_meta.tag; let default_ns = match &meta.ns.uri { Some(ns) => quote!(#ns), @@ -297,6 +284,13 @@ fn named_field( }; 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()) { @@ -344,9 +338,33 @@ fn named_field( let mut no_lifetime_type = field.ty.clone(); discard_lifetimes(&mut no_lifetime_type, borrowed, false, true); - body.extend(quote!( - self.#field_name.serialize(Some(::instant_xml::Id { ns: #ns, name: #tag }), serializer)?; - )); + 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`", + )); + } + + 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!( + self.#field_name.serialize(None, serializer)?; + )); + } else { + body.extend(quote!( + self.#field_name.serialize(Some(::instant_xml::Id { ns: #ns, name: #tag }), serializer)?; + )); + } Ok(()) } diff --git a/instant-xml/tests/direct.rs b/instant-xml/tests/direct.rs new file mode 100644 index 0000000..931bd32 --- /dev/null +++ b/instant-xml/tests/direct.rs @@ -0,0 +1,23 @@ +use similar_asserts::assert_eq; + +use instant_xml::{from_str, to_string, FromXml, ToXml}; + +#[derive(Debug, Eq, FromXml, PartialEq, ToXml)] +struct Foo { + #[xml(attribute)] + flag: bool, + #[xml(direct)] + inner: String, +} + +#[test] +fn direct() { + let v = Foo { + flag: true, + inner: "hello".to_string(), + }; + let xml = "hello"; + + assert_eq!(to_string(&v).unwrap(), xml); + assert_eq!(from_str::(xml), Ok(v)); +}