xso-proc: add support for built-in prefixes in attribute names

Jonas Schรคfer created

This simplifies the use of built-in XML attributes such as xml:lang.

Change summary

parsers/src/util/macro_tests.rs | 17 +++++++
xso-proc/src/meta.rs            | 83 +++++++++++++++++++++++++++++++---
xso/src/from_xml_doc.md         | 13 +++++
3 files changed, 104 insertions(+), 9 deletions(-)

Detailed changes

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

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

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

@@ -15,6 +15,11 @@ use syn::{meta::ParseNestedMeta, spanned::Spanned, *};
 
 use rxml_validation::NcName;
 
+/// XML core namespace URI (for the `xml:` prefix)
+pub const XMLNS_XML: &'static str = "http://www.w3.org/XML/1998/namespace";
+/// XML namespace URI (for the `xmlns:` prefix)
+pub const XMLNS_XMLNS: &'static str = "http://www.w3.org/2000/xmlns/";
+
 /// Value for the `#[xml(namespace = ..)]` attribute.
 #[derive(Debug)]
 pub(crate) enum NamespaceRef {
@@ -25,6 +30,12 @@ pub(crate) enum NamespaceRef {
     Path(Path),
 }
 
+impl NamespaceRef {
+    fn fudge(value: &str, span: Span) -> Self {
+        Self::LitStr(LitStr::new(value, span))
+    }
+}
+
 impl syn::parse::Parse for NamespaceRef {
     fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
         if input.peek(syn::LitStr) {
@@ -67,10 +78,7 @@ impl syn::parse::Parse for NameRef {
             let span = s.span();
             match NcName::try_from(s.value()) {
                 Ok(value) => Ok(Self::Literal { value, span }),
-                Err(e) => Err(Error::new(
-                    span,
-                    format!("not a valid XML element name: {}", e),
-                )),
+                Err(e) => Err(Error::new(span, format!("not a valid XML name: {}", e))),
             }
         } else {
             let p: Path = input.parse()?;
@@ -195,6 +203,44 @@ impl XmlCompoundMeta {
     }
 }
 
+/// Parse an XML name while resolving built-in namespace prefixes.
+fn parse_prefixed_name(
+    value: syn::parse::ParseStream<'_>,
+) -> Result<(Option<NamespaceRef>, NameRef)> {
+    let name: LitStr = value.parse()?;
+    let name_span = name.span();
+    let (prefix, name) = match name
+        .value()
+        .try_into()
+        .and_then(|name: rxml_validation::Name| name.split_name())
+    {
+        Ok(v) => v,
+        Err(e) => {
+            return Err(Error::new(
+                name_span,
+                format!("not a valid XML name: {}", e),
+            ))
+        }
+    };
+    let name = NameRef::Literal {
+        value: name,
+        span: name_span,
+    };
+    if let Some(prefix) = prefix {
+        let namespace_uri = match prefix.as_str() {
+            "xml" => XMLNS_XML,
+            "xmlns" => XMLNS_XMLNS,
+            other => return Err(Error::new(
+                name_span,
+                format!("prefix `{}` is not a built-in prefix and cannot be used. specify the desired namespace using the `namespace` key instead.", other)
+            )),
+        };
+        Ok((Some(NamespaceRef::fudge(namespace_uri, name_span)), name))
+    } else {
+        Ok((None, name))
+    }
+}
+
 /// Contents of an `#[xml(..)]` attribute on a struct or enum variant member.
 #[derive(Debug)]
 pub(crate) enum XmlFieldMeta {
@@ -222,10 +268,11 @@ impl XmlFieldMeta {
     fn attribute_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
         if meta.input.peek(Token![=]) {
             // shorthand syntax
+            let (namespace, name) = parse_prefixed_name(meta.value()?)?;
             Ok(Self::Attribute {
                 span: meta.path.span(),
-                name: Some(meta.value()?.parse()?),
-                namespace: None,
+                name: Some(name),
+                namespace,
             })
         } else if meta.input.peek(syn::token::Paren) {
             // full syntax
@@ -236,11 +283,31 @@ impl XmlFieldMeta {
                     if name.is_some() {
                         return Err(Error::new_spanned(meta.path, "duplicate `name` key"));
                     }
-                    name = Some(meta.value()?.parse()?);
+                    let value = meta.value()?;
+                    name = if value.peek(LitStr) {
+                        let name_span = value.span();
+                        let (new_namespace, name) = parse_prefixed_name(value)?;
+                        if let Some(new_namespace) = new_namespace {
+                            if namespace.is_some() {
+                                return Err(Error::new(
+                                    name_span,
+                                    "cannot combine `namespace` key with prefixed `name`",
+                                ));
+                            }
+                            namespace = Some(new_namespace);
+                        }
+                        Some(name)
+                    } else {
+                        // just use the normal parser
+                        Some(value.parse()?)
+                    };
                     Ok(())
                 } else if meta.path.is_ident("namespace") {
                     if namespace.is_some() {
-                        return Err(Error::new_spanned(meta.path, "duplicate `namespace` key"));
+                        return Err(Error::new_spanned(
+                            meta.path,
+                            "duplicate `namespace` key or `name` key has prefix",
+                        ));
                     }
                     namespace = Some(meta.value()?.parse()?);
                     Ok(())

xso/src/from_xml_doc.md ๐Ÿ”—

@@ -72,9 +72,16 @@ The following keys can be used inside the `#[xml(attribute(..))]` meta:
 | `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`. |
 
+If the `name` key contains a namespace prefix, it must be one of the prefixes
+defined as built-in in the XML specifications. That prefix will then be
+expanded to the corresponding namespace URI and the value for the `namespace`
+key is implied. Mixing a prefixed name with an explicit `namespace` key is
+not allowed.
+
 The `attribute` meta also supports a shorthand syntax,
 `#[xml(attribute = ..)]`, where the value is treated as the value for the
-`name` key and the `namespace` is unset.
+`name` key (with optional prefix as described above, and unnamespaced
+otherwise).
 
 ##### Example
 
@@ -91,17 +98,21 @@ struct Foo {
     c: String,
     #[xml(attribute(namespace = "urn:example", name = "fnord"))]
     d: String,
+    #[xml(attribute = "xml:lang")]
+    e: String,
 };
 
 let foo: Foo = xso::from_bytes(b"<foo
     xmlns='urn:example'
     a='1' bar='2' baz='3'
     xmlns:tns0='urn:example' tns0:fnord='4'
+    xml:lang='5'
 />").unwrap();
 assert_eq!(foo, Foo {
     a: "1".to_string(),
     b: "2".to_string(),
     c: "3".to_string(),
     d: "4".to_string(),
+    e: "5".to_string(),
 });
 ```