From ba40445c5e77a4713ded0e856d1b435553c27137 Mon Sep 17 00:00:00 2001 From: choinskib <37154155+choinskib@users.noreply.github.com> Date: Tue, 23 Aug 2022 13:34:25 +0200 Subject: [PATCH] Simple deserializer (#7) --- instant-xml-macros/src/de.rs | 254 ++++++++++++++++++++++++++++++++++ instant-xml-macros/src/lib.rs | 45 ++++-- instant-xml/src/impls.rs | 27 ++++ instant-xml/src/lib.rs | 198 +++++++++++++++++++++++++- instant-xml/src/parse.rs | 158 ++++++++++++++++----- instant-xml/tests/all.rs | 80 ++++++++--- 6 files changed, 688 insertions(+), 74 deletions(-) create mode 100644 instant-xml-macros/src/de.rs create mode 100644 instant-xml/src/impls.rs diff --git a/instant-xml-macros/src/de.rs b/instant-xml-macros/src/de.rs new file mode 100644 index 0000000..e120f8a --- /dev/null +++ b/instant-xml-macros/src/de.rs @@ -0,0 +1,254 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; + +use crate::{get_namespaces, retrieve_attr}; + +struct Tokens { + enum_: TokenStream, + consts: TokenStream, + names: TokenStream, + match_: TokenStream, +} + +impl Default for Tokens { + fn default() -> Self { + Self { + enum_: TokenStream::new(), + consts: TokenStream::new(), + names: TokenStream::new(), + match_: TokenStream::new(), + } + } +} + +pub struct Deserializer { + out: TokenStream, +} + +impl quote::ToTokens for Deserializer { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend(self.out.clone()); + } +} + +impl Deserializer { + pub fn new(input: &syn::DeriveInput) -> Deserializer { + let ident = &input.ident; + let name = ident.to_string(); + let mut out = TokenStream::new(); + + let (_, other_namespaces) = get_namespaces(&input.attrs); + let mut namespaces_map = quote!(let mut namespaces_map = std::collections::HashMap::new();); + for (k, v) in other_namespaces.iter() { + namespaces_map.extend(quote!( + namespaces_map.insert(#k, #v); + )) + } + + // Varying values + let mut elements_tokens = Tokens::default(); + let mut attributes_tokens = Tokens::default(); + + // Common values + let mut declare_values = TokenStream::new(); + let mut return_val = TokenStream::new(); + + match &input.data { + syn::Data::Struct(ref data) => { + match data.fields { + syn::Fields::Named(ref fields) => { + fields.named.iter().enumerate().for_each(|(index, field)| { + let is_element; + let tokens = match retrieve_attr("attribute", &field.attrs) { + Some(true) => { + is_element = false; + &mut attributes_tokens + } + _ => { + is_element = true; + &mut elements_tokens + } + }; + + Self::process_field( + field, + index, + &mut declare_values, + &mut return_val, + tokens, + is_element, + ); + }); + } + syn::Fields::Unnamed(_) => todo!(), + syn::Fields::Unit => {} + }; + } + _ => todo!(), + }; + + // Elements + let elements_enum = elements_tokens.enum_; + let elements_consts = elements_tokens.consts; + let elements_names = elements_tokens.names; + let elem_type_match = elements_tokens.match_; + + // Attributes + let attributes_enum = attributes_tokens.enum_; + let attributes_consts = attributes_tokens.consts; + let attributes_names = attributes_tokens.names; + let attr_type_match = attributes_tokens.match_; + + out.extend(quote!( + fn deserialize(deserializer: &mut ::instant_xml::Deserializer) -> Result { + use ::instant_xml::parse::XmlRecord; + use ::instant_xml::{Error, Deserializer, Visitor} ; + + enum __Elements { + #elements_enum + __Ignore, + } + + fn get_element(value: &str) -> __Elements { + #elements_consts + match value { + #elements_names + _ => __Elements::__Ignore + } + } + + enum __Attributes { + #attributes_enum + __Ignore, + } + + fn get_attribute(value: &str) -> __Attributes { + #attributes_consts + match value { + #attributes_names + _ => __Attributes::__Ignore + } + } + + struct StructVisitor; + impl<'xml> Visitor<'xml> for StructVisitor { + type Value = #ident; + + fn visit_struct<'a>(&self, deserializer: &mut ::instant_xml::Deserializer) -> Result + { + #declare_values + + while let Some(( key, _ )) = deserializer.peek_next_attribute() { + match get_attribute(&key) { + #attr_type_match + __Attributes::__Ignore => todo!(), + } + } + while let Some(item) = &deserializer.peek_next_tag()? { + match item { + XmlRecord::Open(item) => { + match get_element(&item.key.as_ref()) { + #elem_type_match + __Elements::__Ignore => todo!(), + } + } + XmlRecord::Close(tag) => { + if tag == &#name { + break; + } + }, + XmlRecord::Element(_) => panic!("Unexpected element"), + } + } + + Ok(Self::Value { + #return_val + }) + } + } + + #namespaces_map; + deserializer.deserialize_struct(StructVisitor{}, #name, &namespaces_map) + } + )); + + out.extend(quote!( + const TAG_NAME: ::instant_xml::TagName<'xml> = ::instant_xml::TagName::Custom(#name); + )); + + Deserializer { out } + } + + fn process_field( + field: &syn::Field, + index: usize, + declare_values: &mut TokenStream, + return_val: &mut TokenStream, + tokens: &mut Tokens, + is_element: bool, + ) { + let field_var = field.ident.as_ref().unwrap(); + let field_var_str = field_var.to_string(); + let const_field_var_str = Ident::new(&field_var_str.to_uppercase(), Span::call_site()); + let field_type = match &field.ty { + syn::Type::Path(v) => v.path.get_ident(), + _ => todo!(), + }; + + let enum_name = Ident::new(&format!("__Value{index}"), Span::call_site()); + tokens.enum_.extend(quote!(#enum_name,)); + + tokens.consts.extend(quote!( + const #const_field_var_str: &str = match #field_type::TAG_NAME { + ::instant_xml::TagName::FieldName => #field_var_str, + ::instant_xml::TagName::Custom(v) => v, + }; + )); + + if is_element { + tokens.names.extend(quote!( + #const_field_var_str => __Elements::#enum_name, + )); + } else { + tokens.names.extend(quote!( + #const_field_var_str => __Attributes::#enum_name, + )); + } + + declare_values.extend(quote!( + let mut #enum_name: Option<#field_type> = None; + )); + + if is_element { + tokens.match_.extend(quote!( + __Elements::#enum_name => { + if #enum_name.is_some() { + panic!("duplicated value"); + } + + if let Some(item) = item.prefix { + let prefix = item.to_owned(); + deserializer.verify_namespace(&prefix); + } + + #enum_name = Some(#field_type::deserialize(deserializer)?); + }, + )); + } else { + tokens.match_.extend(quote!( + __Attributes::#enum_name => { + if #enum_name.is_some() { + panic!("duplicated value"); + } + + deserializer.set_next_type_as_attribute()?; + #enum_name = Some(#field_type::deserialize(deserializer)?); + }, + )); + } + + return_val.extend(quote!( + #field_var: #enum_name.expect("Expected some value"), + )); + } +} diff --git a/instant-xml-macros/src/lib.rs b/instant-xml-macros/src/lib.rs index 520d89f..ab51d63 100644 --- a/instant-xml-macros/src/lib.rs +++ b/instant-xml-macros/src/lib.rs @@ -1,5 +1,6 @@ extern crate proc_macro; +mod de; mod se; use std::collections::{BTreeSet, HashMap}; @@ -12,7 +13,7 @@ use crate::se::Serializer; const XML: &str = "xml"; -enum FieldAttribute { +pub(crate) enum FieldAttribute { Namespace(String), PrefixIdentifier(String), } @@ -66,6 +67,30 @@ pub(crate) fn retrieve_field_attribute(name: &str, input: &syn::Field) -> Option None } +pub(crate) fn retrieve_attr(name: &str, attributes: &Vec) -> Option { + for attr in attributes { + if !attr.path.is_ident(XML) { + continue; + } + + let nested = match attr.parse_meta() { + Ok(Meta::List(meta)) => meta.nested, + _ => return Some(false), + }; + + let path = match nested.first() { + Some(NestedMeta::Meta(Meta::Path(path))) => path, + _ => return Some(false), + }; + + if path.get_ident()? == name { + return Some(true); + } + } + + None +} + fn retrieve_attr_list(name: &str, attributes: &Vec) -> Option { for attr in attributes { if !attr.path.is_ident(XML) { @@ -74,8 +99,7 @@ fn retrieve_attr_list(name: &str, attributes: &Vec) -> Option meta.nested, - Ok(_) => todo!(), - _ => todo!(), + _ => return None, }; let list = match nested.first() { @@ -134,7 +158,7 @@ pub fn to_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream { // Check if prefix exist #( if serializer.parent_prefixes.get(#missing_prefixes).is_none() { - return Err(instant_xml::Error::WrongPrefix); + return Err(instant_xml::Error::UnexpectedPrefix); } )*; @@ -159,18 +183,13 @@ pub fn to_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream { #[proc_macro_derive(FromXml, attributes(xml))] pub fn from_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let ast = parse_macro_input!(input as syn::ItemStruct); + let ast = parse_macro_input!(input as syn::DeriveInput); let ident = &ast.ident; - let name = ident.to_string(); + + let deserializer = de::Deserializer::new(&ast); proc_macro::TokenStream::from(quote!( impl<'xml> FromXml<'xml> for #ident { - fn from_xml(input: &str) -> Result { - use ::instant_xml::parse::Parse; - let mut iter = ::instant_xml::xmlparser::Tokenizer::from(input); - iter.next().element_start(None, #name)?; - iter.next().element_end(None, #name)?; - Ok(Self) - } + #deserializer } )) } diff --git a/instant-xml/src/impls.rs b/instant-xml/src/impls.rs new file mode 100644 index 0000000..3069184 --- /dev/null +++ b/instant-xml/src/impls.rs @@ -0,0 +1,27 @@ +use std::str::FromStr; + +use crate::{Deserializer, EntityType, Error, FromXml, TagName, Visitor}; + +struct BoolVisitor; + +impl<'de> Visitor<'de> for BoolVisitor { + type Value = bool; + + fn visit_str<'a>(self, value: &str) -> Result { + match FromStr::from_str(value) { + Ok(v) => Ok(v), + Err(e) => Err(Error::Other(e.to_string())), + } + } +} + +impl<'xml> FromXml<'xml> for bool { + const TAG_NAME: TagName<'xml> = TagName::FieldName; + + fn deserialize(deserializer: &mut Deserializer) -> Result { + match deserializer.consume_next_type() { + EntityType::Element => deserializer.deserialize_bool(BoolVisitor), + EntityType::Attribute => deserializer.deserialize_attribute(BoolVisitor), + } + } +} diff --git a/instant-xml/src/lib.rs b/instant-xml/src/lib.rs index 99f2c89..81e8bed 100644 --- a/instant-xml/src/lib.rs +++ b/instant-xml/src/lib.rs @@ -5,10 +5,29 @@ use thiserror::Error; pub use xmlparser; pub use macros::{FromXml, ToXml}; +use parse::XmlParser; +pub mod impls; #[doc(hidden)] pub mod parse; +pub struct TagData<'xml> { + pub key: &'xml str, + pub attributes: Vec<(&'xml str, &'xml str)>, + + // TODO: handle default namespace + pub default_namespace: Option<&'xml str>, + + pub namespaces: Option>, + pub prefix: Option<&'xml str>, +} + +pub enum XmlRecord<'xml> { + Open(TagData<'xml>), + Element(&'xml str), + Close(&'xml str), +} + pub trait ToXml { fn to_xml(&self) -> Result { let mut output = String::new(); @@ -181,8 +200,167 @@ pub struct FieldContext<'xml> { pub attribute: Option>, } +#[derive(Clone, PartialEq, Eq)] +pub enum EntityType { + Element, + Attribute, +} + +pub enum TagName<'xml> { + FieldName, + Custom(&'xml str), +} + pub trait FromXml<'xml>: Sized { - fn from_xml(input: &str) -> Result; + const TAG_NAME: TagName<'xml>; + + fn from_xml(input: &str) -> Result { + let mut deserializer = Deserializer::new(input); + Self::deserialize(&mut deserializer) + } + + fn deserialize(deserializer: &mut Deserializer) -> Result; +} + +pub trait Visitor<'xml>: Sized { + type Value; + + fn visit_str(self, _value: &str) -> Result { + unimplemented!(); + } + + fn visit_struct<'a>(&self, _deserializer: &'a mut Deserializer) -> Result { + unimplemented!(); + } +} + +pub struct Deserializer<'xml> { + parser: XmlParser<'xml>, + namespaces: HashMap<&'xml str, &'xml str>, + tag_attributes: Vec<(&'xml str, &'xml str)>, + next_type: EntityType, +} + +impl<'xml> Deserializer<'xml> { + pub fn new(input: &'xml str) -> Self { + Self { + parser: XmlParser::new(input), + namespaces: std::collections::HashMap::new(), + tag_attributes: Vec::new(), + next_type: EntityType::Element, + } + } + + pub fn peek_next_tag(&mut self) -> Result, Error> { + self.parser.peek_next_tag() + } + + pub fn verify_namespace(&self, namespace_to_verify: &str) -> bool { + self.namespaces.get(namespace_to_verify).is_some() + } + + pub fn peek_next_attribute(&self) -> Option<&(&'xml str, &'xml str)> { + self.tag_attributes.last() + } + + pub fn deserialize_struct( + &mut self, + visitor: V, + name: &str, + namespaces: &HashMap<&'xml str, &'xml str>, + ) -> Result + where + V: Visitor<'xml>, + { + let new_namespaces = namespaces + .iter() + .filter(|(k, v)| self.namespaces.insert(k, v).is_none()) + .collect::>(); + + self.process_open_tag(name, namespaces)?; + let ret = visitor.visit_struct(self)?; + + self.check_close_tag(name)?; + let _ = new_namespaces + .iter() + .map(|(k, _)| self.namespaces.remove(*k)); + + Ok(ret) + } + + pub fn set_next_type_as_attribute(&mut self) -> Result<(), Error> { + if self.next_type == EntityType::Attribute { + return Err(Error::UnexpectedState); + } + + self.next_type = EntityType::Attribute; + Ok(()) + } + + pub fn consume_next_type(&mut self) -> EntityType { + let ret = self.next_type.clone(); + self.next_type = EntityType::Element; + ret + } + + fn deserialize_bool(&mut self, visitor: V) -> Result + where + V: Visitor<'xml>, + { + self.parser.next(); + match self.parser.next() { + Some(Ok(XmlRecord::Element(v))) => { + let ret = visitor.visit_str(v); + self.parser.next(); + ret + } + _ => Err(Error::UnexpectedValue), + } + } + + fn deserialize_attribute(&mut self, visitor: V) -> Result + where + V: Visitor<'xml>, + { + match self.tag_attributes.pop() { + Some((_, value)) => visitor.visit_str(value), + None => Err(Error::UnexpectedEndOfStream), + } + } + + fn process_open_tag( + &mut self, + name: &str, + namespaces: &HashMap<&'xml str, &'xml str>, + ) -> Result<(), Error> { + let item = match self.parser.next() { + Some(Ok(XmlRecord::Open(item))) if item.key == name => item, + _ => return Err(Error::UnexpectedValue), + }; + + for (k, v) in item.namespaces.unwrap() { + match namespaces.get(k) { + Some(item) if *item != v => return Err(Error::UnexpectedPrefix), + None => return Err(Error::MissingdPrefix), + _ => (), + } + } + + self.tag_attributes = item.attributes; + Ok(()) + } + + fn check_close_tag(&mut self, name: &str) -> Result<(), Error> { + let item = match self.parser.next() { + Some(item) => item?, + None => return Err(Error::MissingTag), + }; + + match item { + XmlRecord::Close(v) if v == name => Ok(()), + _ => Err(Error::UnexpectedTag), + } + } } pub trait FromXmlOwned: for<'xml> FromXml<'xml> {} @@ -198,10 +376,24 @@ pub enum Error { Format(#[from] fmt::Error), #[error("parse: {0}")] Parse(#[from] xmlparser::Error), + #[error("other: {0}")] + Other(std::string::String), #[error("unexpected end of stream")] UnexpectedEndOfStream, #[error("unexpected value")] UnexpectedValue, - #[error("wrong prefix")] - WrongPrefix, + #[error("unexpected tag")] + UnexpectedTag, + #[error("missing tag")] + MissingTag, + #[error("missing value")] + MissingValue, + #[error("unexpected token")] + UnexpectedToken, + #[error("missing prefix")] + MissingdPrefix, + #[error("unexpected prefix")] + UnexpectedPrefix, + #[error("unexpected state")] + UnexpectedState, } diff --git a/instant-xml/src/parse.rs b/instant-xml/src/parse.rs index 6d41343..7e73517 100644 --- a/instant-xml/src/parse.rs +++ b/instant-xml/src/parse.rs @@ -1,48 +1,134 @@ -use xmlparser::{ElementEnd, Token}; +use std::collections::HashMap; +use std::iter::Peekable; -use super::Error; +use xmlparser::{ElementEnd, Token, Tokenizer}; -impl<'a> Parse for Option, xmlparser::Error>> { - fn element_start(self, ns: Option<&str>, tag: &str) -> Result<(), Error> { - match self { - Some(Ok(Token::ElementStart { prefix, local, .. })) => { - let prefix_ns = prefix.as_str(); - let (has_prefix, expect_prefix) = (!prefix_ns.is_empty(), ns.is_some()); - if has_prefix != expect_prefix { - return dbg!(Err(Error::UnexpectedValue)); - } +use crate::Error; +pub use crate::{TagData, XmlRecord}; - if has_prefix && Some(prefix_ns) != ns { - return dbg!(Err(Error::UnexpectedValue)); - } +pub struct XmlParser<'xml> { + stack: Vec<&'xml str>, + iter: Peekable>, +} - if local.as_str() != tag { - return dbg!(Err(Error::UnexpectedValue)); - } +impl<'a> XmlParser<'a> { + pub fn new(input: &'a str) -> XmlParser<'a> { + XmlParser { + stack: Vec::new(), + iter: Tokenizer::from(input).peekable(), + } + } - Ok(()) + pub fn peek_next_tag(&mut self) -> Result, Error> { + let item = match self.iter.peek() { + Some(v) => v, + None => return Ok(None), + }; + + match item { + Ok(Token::ElementStart { prefix, local, .. }) => { + let prefix = match prefix.is_empty() { + true => None, + false => Some(prefix.as_str()), + }; + + Ok(Some(XmlRecord::Open(TagData { + key: local, + attributes: Vec::new(), + default_namespace: None, + namespaces: None, + prefix, + }))) } - Some(Ok(_)) => Err(Error::UnexpectedValue), - Some(Err(err)) => Err(err.into()), - None => Err(Error::UnexpectedEndOfStream), - } - } + Ok(Token::ElementEnd { + end: ElementEnd::Close(..), + .. + }) => { + if self.stack.is_empty() { + return Err(Error::UnexpectedEndOfStream); + } - fn element_end(self, _: Option<&str>, _: &str) -> Result<(), Error> { - match self { - Some(Ok(Token::ElementEnd { end, .. })) => match end { - ElementEnd::Open => todo!(), - ElementEnd::Close(_, _) => todo!(), - ElementEnd::Empty => Ok(()), - }, - Some(Ok(_)) => Err(Error::UnexpectedValue), - Some(Err(err)) => Err(err.into()), - None => Err(Error::UnexpectedEndOfStream), + return Ok(Some(XmlRecord::Close(self.stack.last().unwrap()))); + } + Ok(_) => Err(Error::UnexpectedToken), + Err(e) => Err(Error::Parse(*e)), } } } -pub trait Parse { - fn element_start(self, ns: Option<&str>, tag: &str) -> Result<(), Error>; - fn element_end(self, ns: Option<&str>, tag: &str) -> Result<(), Error>; +impl<'xml> Iterator for XmlParser<'xml> { + type Item = Result, Error>; + + #[inline] + fn next(&mut self) -> Option { + let mut key: Option<&str> = None; + let mut prefix_ret: Option<&str> = None; + let mut default_namespace = None; + let mut namespaces = HashMap::new(); + let mut attributes = Vec::new(); + + loop { + let item = match self.iter.next() { + Some(v) => v, + None => return None, + }; + + match item { + Ok(Token::ElementStart { prefix, local, .. }) => { + key = Some(local.as_str()); + prefix_ret = match prefix.is_empty() { + true => None, + false => Some(prefix.as_str()), + }; + } + Ok(Token::ElementEnd { end, .. }) => match end { + ElementEnd::Open => { + self.stack.push(key.unwrap()); + + return Some(Ok(XmlRecord::Open(TagData { + key: key.unwrap(), + attributes, + default_namespace, + namespaces: Some(namespaces), + prefix: prefix_ret, + }))); + } + ElementEnd::Close(_, v) => match self.stack.pop() { + Some(last) if last == v.as_str() => { + return Some(Ok(XmlRecord::Close(last))); + } + _ => return Some(Err(Error::UnexpectedValue)), + }, + ElementEnd::Empty => { + todo!(); + } + }, + Ok(Token::Attribute { + prefix, + local, + value, + .. + }) => { + if prefix.is_empty() && local.as_str() == "xmlns" { + // Default namespace + default_namespace = Some(value.as_str()); + } else if prefix.as_str() == "xmlns" { + // Namespaces + namespaces.insert(local.as_str(), value.as_str()); + } else if prefix.is_empty() { + // Other attributes + attributes.push((local.as_str(), value.as_str())); + } else { + // TODO: Can the attributes have the prefix? + todo!(); + } + } + Ok(Token::Text { text }) => { + return Some(Ok(XmlRecord::Element(text.as_str()))); + } + Ok(_) => return Some(Err(Error::UnexpectedToken)), + Err(e) => return Some(Err(Error::Parse(e))), + } + } + } } diff --git a/instant-xml/tests/all.rs b/instant-xml/tests/all.rs index 12282bc..413fd6a 100644 --- a/instant-xml/tests/all.rs +++ b/instant-xml/tests/all.rs @@ -1,32 +1,26 @@ use instant_xml::{FromXml, ToXml}; -#[derive(Debug, Eq, FromXml, PartialEq, ToXml)] -struct Unit; - -#[derive(Debug, Eq, PartialEq, ToXml)] -#[xml(namespace("URI", bar = "BAZ", foo = "BAR"))] -struct StructWithCustomField { - test: Nested, -} - -#[derive(Debug, Eq, PartialEq, ToXml)] +#[derive(Debug, Eq, PartialEq, ToXml, FromXml)] struct Nested { #[xml(namespace(bar))] flag: bool, } -#[derive(Debug, Eq, PartialEq, ToXml)] -#[xml(namespace("URI", bar = "BAZ", foo = "BAR"))] -struct StructWithCustomFieldWrongPrefix { - test: NestedWrongPrefix, -} - #[derive(Debug, Eq, PartialEq, ToXml)] struct NestedWrongPrefix { #[xml(namespace(dar))] flag: bool, } +#[derive(Debug, Eq, PartialEq, ToXml)] +struct Unit; + +#[test] +fn unit() { + assert_eq!(Unit.to_xml().unwrap(), ""); + //assert_eq!(Unit::from_xml("").unwrap(), Unit); +} + #[derive(Debug, Eq, PartialEq, ToXml)] #[xml(namespace("URI", bar = "BAZ", foo = "BAR"))] struct StructWithNamedFields { @@ -37,12 +31,6 @@ struct StructWithNamedFields { number: i32, } -#[test] -fn unit() { - assert_eq!(Unit.to_xml().unwrap(), ""); - assert_eq!(Unit::from_xml("").unwrap(), Unit); -} - #[test] fn struct_with_named_fields() { assert_eq!( @@ -57,6 +45,12 @@ fn struct_with_named_fields() { ); } +#[derive(Debug, Eq, PartialEq, ToXml)] +#[xml(namespace("URI", bar = "BAZ", foo = "BAR"))] +struct StructWithCustomField { + test: Nested, +} + #[test] fn struct_with_custom_field() { assert_eq!( @@ -72,6 +66,12 @@ fn struct_with_custom_field() { ); } +#[derive(Debug, Eq, PartialEq, ToXml)] +#[xml(namespace("URI", bar = "BAZ", foo = "BAR"))] +struct StructWithCustomFieldWrongPrefix { + test: NestedWrongPrefix, +} + #[test] #[should_panic] fn struct_with_custom_field_wrong_prefix() { @@ -81,3 +81,39 @@ fn struct_with_custom_field_wrong_prefix() { .to_xml() .unwrap(); } + +#[derive(Debug, Eq, PartialEq, FromXml)] +#[xml(namespace("URI", bar = "BAZ", foo = "BAR"))] +struct StructWithCustomFieldFromXml { + #[xml(namespace(bar))] + flag: bool, + #[xml(attribute)] + flag_attribute: bool, + test: Nested, +} + +#[test] +fn struct_with_custom_field_from_xml() { + assert_eq!( + StructWithCustomFieldFromXml::from_xml("falsetrue").unwrap(), + StructWithCustomFieldFromXml { + flag: false, + flag_attribute: true, + test: Nested { flag: true } + } + ); + // Different order + assert_eq!( + StructWithCustomFieldFromXml::from_xml("truefalse").unwrap(), + StructWithCustomFieldFromXml { + flag: false, + flag_attribute: true, + test: Nested { flag: true } + } + ); + + assert_eq!( + Nested::from_xml("true").unwrap(), + Nested { flag: true } + ); +}