xso-proc: add support for namespaced attributes

Jonas SchΓ€fer created

Change summary

parsers/src/util/macro_tests.rs | 37 ++++++++++++++++++++++++++++
xso-proc/src/field.rs           | 46 ++++++++++++++++++++++++++++------
xso-proc/src/meta.rs            | 13 +++++++++
xso/src/from_xml_doc.md         | 12 +++++++-
4 files changed, 98 insertions(+), 10 deletions(-)

Detailed changes

parsers/src/util/macro_tests.rs πŸ”—

@@ -245,3 +245,40 @@ fn renamed_attribute_roundtrip() {
     };
     roundtrip_full::<RenamedAttribute>("<attr xmlns='urn:example:ns1' a1='bar'/>");
 }
+
+#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "attr")]
+struct NamespacedAttribute {
+    #[xml(attribute(namespace = "urn:example:ns1", name = "foo"))]
+    foo: String,
+    #[xml(attribute(namespace = "urn:example:ns2", name = "foo"))]
+    bar: String,
+}
+
+#[test]
+fn namespaced_attribute_roundtrip_a() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<NamespacedAttribute>(
+        "<attr xmlns='urn:example:ns1'
+          xmlns:tns0='urn:example:ns1' tns0:foo='a1'
+          xmlns:tns1='urn:example:ns2' tns1:foo='a2'/>",
+    );
+}
+
+#[test]
+fn namespaced_attribute_roundtrip_b() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<NamespacedAttribute>(
+        "<tns0:attr
+          xmlns:tns0='urn:example:ns1' tns0:foo='bar'
+          xmlns:tns1='urn:example:ns2' tns1:foo='a2'/>",
+    );
+}

xso-proc/src/field.rs πŸ”—

@@ -7,13 +7,13 @@
 //! Compound (struct or enum variant) field types
 
 use proc_macro2::TokenStream;
-use quote::quote;
+use quote::{quote, ToTokens};
 use syn::{spanned::Spanned, *};
 
 use rxml_validation::NcName;
 
 use crate::error_message::{self, ParentRef};
-use crate::meta::{NameRef, XmlFieldMeta};
+use crate::meta::{NameRef, NamespaceRef, XmlFieldMeta};
 use crate::scope::{FromEventsScope, IntoEventsScope};
 
 /// Code slices necessary for declaring and initializing a temporary variable
@@ -61,6 +61,9 @@ pub(crate) enum FieldIteratorPart {
 enum FieldKind {
     /// The field maps to an attribute.
     Attribute {
+        /// The optional XML namespace of the attribute.
+        xml_namespace: Option<NamespaceRef>,
+
         /// The XML name of the attribute.
         xml_name: NameRef,
     },
@@ -73,7 +76,11 @@ impl FieldKind {
     /// it is not specified explicitly.
     fn from_meta(meta: XmlFieldMeta, field_ident: Option<&Ident>) -> Result<Self> {
         match meta {
-            XmlFieldMeta::Attribute { span, name } => {
+            XmlFieldMeta::Attribute {
+                span,
+                namespace,
+                name,
+            } => {
                 let xml_name = match name {
                     Some(v) => v,
                     None => match field_ident {
@@ -96,7 +103,10 @@ impl FieldKind {
                     }
                 };
 
-                Ok(Self::Attribute { xml_name })
+                Ok(Self::Attribute {
+                    xml_name,
+                    xml_namespace: namespace,
+                })
             }
         }
     }
@@ -167,16 +177,26 @@ impl FieldDef {
         container_name: &ParentRef,
     ) -> Result<FieldBuilderPart> {
         match self.kind {
-            FieldKind::Attribute { ref xml_name } => {
+            FieldKind::Attribute {
+                ref xml_name,
+                ref xml_namespace,
+            } => {
                 let FromEventsScope { ref attrs, .. } = scope;
 
                 let missing_msg = error_message::on_missing_attribute(container_name, &self.member);
 
+                let xml_namespace = match xml_namespace {
+                    Some(v) => v.to_token_stream(),
+                    None => quote! {
+                        ::xso::exports::rxml::Namespace::none()
+                    },
+                };
+
                 return Ok(FieldBuilderPart::Init {
                     value: FieldTempInit {
                         ty: self.ty.clone(),
                         init: quote! {
-                            match #attrs.remove(::xso::exports::rxml::Namespace::none(), #xml_name) {
+                            match #attrs.remove(#xml_namespace, #xml_name) {
                                 ::core::option::Option::Some(v) => v,
                                 ::core::option::Option::None => return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into()),
                             }
@@ -197,13 +217,23 @@ impl FieldDef {
         bound_name: &Ident,
     ) -> Result<FieldIteratorPart> {
         match self.kind {
-            FieldKind::Attribute { ref xml_name } => {
+            FieldKind::Attribute {
+                ref xml_name,
+                ref xml_namespace,
+            } => {
                 let IntoEventsScope { ref attrs, .. } = scope;
 
+                let xml_namespace = match xml_namespace {
+                    Some(v) => quote! { ::xso::exports::rxml::Namespace::from(#v) },
+                    None => quote! {
+                        ::xso::exports::rxml::Namespace::NONE
+                    },
+                };
+
                 return Ok(FieldIteratorPart::Header {
                     setter: quote! {
                         #attrs.insert(
-                            ::xso::exports::rxml::Namespace::NONE,
+                            #xml_namespace,
                             #xml_name.to_owned(),
                             #bound_name,
                         );

xso-proc/src/meta.rs πŸ”—

@@ -204,6 +204,9 @@ pub(crate) enum XmlFieldMeta {
         /// This is useful for error messages.
         span: Span,
 
+        /// The XML namespace supplied.
+        namespace: Option<NamespaceRef>,
+
         /// The XML name supplied.
         name: Option<NameRef>,
     },
@@ -222,10 +225,12 @@ impl XmlFieldMeta {
             Ok(Self::Attribute {
                 span: meta.path.span(),
                 name: Some(meta.value()?.parse()?),
+                namespace: None,
             })
         } else if meta.input.peek(syn::token::Paren) {
             // full syntax
             let mut name: Option<NameRef> = None;
+            let mut namespace: Option<NamespaceRef> = None;
             meta.parse_nested_meta(|meta| {
                 if meta.path.is_ident("name") {
                     if name.is_some() {
@@ -233,6 +238,12 @@ impl XmlFieldMeta {
                     }
                     name = Some(meta.value()?.parse()?);
                     Ok(())
+                } else if meta.path.is_ident("namespace") {
+                    if namespace.is_some() {
+                        return Err(Error::new_spanned(meta.path, "duplicate `namespace` key"));
+                    }
+                    namespace = Some(meta.value()?.parse()?);
+                    Ok(())
                 } else {
                     Err(Error::new_spanned(meta.path, "unsupported key"))
                 }
@@ -240,12 +251,14 @@ impl XmlFieldMeta {
             Ok(Self::Attribute {
                 span: meta.path.span(),
                 name,
+                namespace,
             })
         } else {
             // argument-less syntax
             Ok(Self::Attribute {
                 span: meta.path.span(),
                 name: None,
+                namespace: None,
             })
         }
     }

xso/src/from_xml_doc.md πŸ”—

@@ -69,11 +69,12 @@ The following keys can be used inside the `#[xml(attribute(..))]` meta:
 
 | Key | Value type | Description |
 | --- | --- | --- |
+| `namespace` | *string literal* or *path* | The optional namespace of the XML attribute to match. If it is a *path*, it must point at a `&'static str`. Note that attributes, unlike elements, are unnamespaced by default. |
 | `name` | *string literal* or *path* | The name of the XML attribute to match. If it is a *path*, it must point at a `&'static NcNameStr`. |
 
 The `attribute` meta also supports a shorthand syntax,
 `#[xml(attribute = ..)]`, where the value is treated as the value for the
-`name` key.
+`name` key and the `namespace` is unset.
 
 ##### Example
 
@@ -88,12 +89,19 @@ struct Foo {
     b: String,
     #[xml(attribute(name = "baz"))]
     c: String,
+    #[xml(attribute(namespace = "urn:example", name = "fnord"))]
+    d: String,
 };
 
-let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example' a='1' bar='2' baz='3'/>").unwrap();
+let foo: Foo = xso::from_bytes(b"<foo
+    xmlns='urn:example'
+    a='1' bar='2' baz='3'
+    xmlns:tns0='urn:example' tns0:fnord='4'
+/>").unwrap();
 assert_eq!(foo, Foo {
     a: "1".to_string(),
     b: "2".to_string(),
     c: "3".to_string(),
+    d: "4".to_string(),
 });
 ```