xso-proc: add support for namespaced attributes

This commit is contained in:
Jonas Schäfer 2024-06-24 07:28:04 +02:00
parent 219d682295
commit 84de7fc248
4 changed files with 98 additions and 10 deletions

View file

@ -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'/>",
);
}

View file

@ -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,
);

View file

@ -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,
})
}
}

View file

@ -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(),
});
```