From d4d520e1f65f943d3a7a3e5ddb564c0c9b3442f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Mon, 24 Jun 2024 07:43:51 +0200 Subject: [PATCH] xso-proc: add support for built-in prefixes in attribute names This simplifies the use of built-in XML attributes such as xml:lang. --- 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(-) diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 2a247fc7..e7725e1b 100644 --- a/parsers/src/util/macro_tests.rs +++ b/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::(""); +} diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 5e35eecf..4053dddb 100644 --- a/xso-proc/src/meta.rs +++ b/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 { 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, 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 { 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(()) diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index 01041965..d813a645 100644 --- a/xso/src/from_xml_doc.md +++ b/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"").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(), }); ```