xso-proc: add support for renaming attributes

Jonas SchΓ€fer created

This is akin to `#[serde(rename = ..)]` and thus useful.

Change summary

parsers/src/util/macro_tests.rs | 17 ++++++++++++++
xso-proc/src/field.rs           | 42 +++++++++++++++++-----------------
xso-proc/src/meta.rs            | 42 ++++++++++++++++++++++++++++++++--
xso/src/from_xml_doc.md         | 38 +++++++++++++++++++++++++++++--
4 files changed, 112 insertions(+), 27 deletions(-)

Detailed changes

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

@@ -228,3 +228,20 @@ fn required_attribute_missing() {
         other => panic!("unexpected result: {:?}", other),
     }
 }
+
+#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "attr")]
+struct RenamedAttribute {
+    #[xml(attribute = "a1")]
+    foo: String,
+}
+
+#[test]
+fn renamed_attribute_roundtrip() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<RenamedAttribute>("<attr xmlns='urn:example:ns1' a1='bar'/>");
+}

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

@@ -73,30 +73,30 @@ impl FieldKind {
     /// it is not specified explicitly.
     fn from_meta(meta: XmlFieldMeta, field_ident: Option<&Ident>) -> Result<Self> {
         match meta {
-            XmlFieldMeta::Attribute { span } => {
-                let Some(field_ident) = field_ident else {
-                    return Err(Error::new(
-                        span,
-                        "attribute extraction not supported on unnamed fields",
-                    ));
-                };
-
-                let xml_name = match NcName::try_from(field_ident.to_string()) {
-                    Ok(v) => v,
-                    Err(e) => {
-                        return Err(Error::new(
-                            field_ident.span(),
-                            format!("invalid XML attribute name: {}", e),
-                        ))
+            XmlFieldMeta::Attribute { span, name } => {
+                let xml_name = match name {
+                    Some(v) => v,
+                    None => match field_ident {
+                        None => return Err(Error::new(
+                            span,
+                            "attribute name must be explicitly specified using `#[xml(attribute = ..)] on unnamed fields",
+                        )),
+                        Some(field_ident) => match NcName::try_from(field_ident.to_string()) {
+                            Ok(value) => NameRef::Literal {
+                                span: field_ident.span(),
+                                value,
+                            },
+                            Err(e) => {
+                                return Err(Error::new(
+                                    field_ident.span(),
+                                    format!("invalid XML attribute name: {}", e),
+                                ))
+                            }
+                        },
                     }
                 };
 
-                Ok(Self::Attribute {
-                    xml_name: NameRef::Literal {
-                        span: field_ident.span(),
-                        value: xml_name,
-                    },
-                })
+                Ok(Self::Attribute { xml_name })
             }
         }
     }

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

@@ -203,15 +203,51 @@ pub(crate) enum XmlFieldMeta {
         ///
         /// This is useful for error messages.
         span: Span,
+
+        /// The XML name supplied.
+        name: Option<NameRef>,
     },
 }
 
 impl XmlFieldMeta {
     /// Parse a `#[xml(attribute(..))]` meta.
+    ///
+    /// That meta can have three distinct syntax styles:
+    /// - argument-less: `#[xml(attribute)]`
+    /// - shorthand: `#[xml(attribute = ..)]`
+    /// - full: `#[xml(attribute(..))]`
     fn attribute_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
-        Ok(Self::Attribute {
-            span: meta.path.span(),
-        })
+        if meta.input.peek(Token![=]) {
+            // shorthand syntax
+            Ok(Self::Attribute {
+                span: meta.path.span(),
+                name: Some(meta.value()?.parse()?),
+            })
+        } else if meta.input.peek(syn::token::Paren) {
+            // full syntax
+            let mut name: Option<NameRef> = None;
+            meta.parse_nested_meta(|meta| {
+                if meta.path.is_ident("name") {
+                    if name.is_some() {
+                        return Err(Error::new_spanned(meta.path, "duplicate `name` key"));
+                    }
+                    name = Some(meta.value()?.parse()?);
+                    Ok(())
+                } else {
+                    Err(Error::new_spanned(meta.path, "unsupported key"))
+                }
+            })?;
+            Ok(Self::Attribute {
+                span: meta.path.span(),
+                name,
+            })
+        } else {
+            // argument-less syntax
+            Ok(Self::Attribute {
+                span: meta.path.span(),
+                name: None,
+            })
+        }
     }
 
     /// Parse [`Self`] from a nestd meta, switching on the identifier

xso/src/from_xml_doc.md πŸ”—

@@ -62,6 +62,38 @@ The following mapping types are defined:
 
 #### `attribute` meta
 
-The `attribute` meta does not support additional parameters. The field it is
-used on is mapped to an XML attribute of the same name and must be of type
-[`String`].
+The `attribute` meta causes the field to be mapped to an XML attribute of the
+same name. The field must be of type [`String`].
+
+The following keys can be used inside the `#[xml(attribute(..))]` meta:
+
+| Key | Value type | Description |
+| --- | --- | --- |
+| `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.
+
+##### Example
+
+```rust
+# use xso::FromXml;
+#[derive(FromXml, Debug, PartialEq)]
+#[xml(namespace = "urn:example", name = "foo")]
+struct Foo {
+    #[xml(attribute)]
+    a: String,
+    #[xml(attribute = "bar")]
+    b: String,
+    #[xml(attribute(name = "baz"))]
+    c: String,
+};
+
+let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example' a='1' bar='2' baz='3'/>").unwrap();
+assert_eq!(foo, Foo {
+    a: "1".to_string(),
+    b: "2".to_string(),
+    c: "3".to_string(),
+});
+```