diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 5eaff2d..242dc40 100644 --- a/parsers/src/util/macro_tests.rs +++ b/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::(""); +} diff --git a/xso-proc/src/field.rs b/xso-proc/src/field.rs index adadd7d..04ccb48 100644 --- a/xso-proc/src/field.rs +++ b/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 { 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 }) } } } diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index ebbfb74..b941047 100644 --- a/xso-proc/src/meta.rs +++ b/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, }, } 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 { - 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 = 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 diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index fd14be1..f98b043 100644 --- a/xso/src/from_xml_doc.md +++ b/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"").unwrap(); +assert_eq!(foo, Foo { + a: "1".to_string(), + b: "2".to_string(), + c: "3".to_string(), +}); +```