xso-proc: validate XML names against rxml_validation::NcName

Jonas SchΓ€fer created

That way we avoid a fallible conversion at runtime.

Change summary

xso-proc/Cargo.toml     |  1 
xso-proc/src/lib.rs     | 14 ++--------
xso-proc/src/meta.rs    | 55 ++++++++++++++++++++++++++++++++++++++----
xso/src/from_xml_doc.md | 12 +++++++++
4 files changed, 65 insertions(+), 17 deletions(-)

Detailed changes

xso-proc/Cargo.toml πŸ”—

@@ -18,6 +18,7 @@ proc-macro = true
 quote = "^1"
 syn = { version = "^2", features = ["full", "extra-traits"] }
 proc-macro2 = "^1"
+rxml_validation = { version = "0.11.0", default-features = false, features = ["std"] }
 
 [features]
 panicking-into-impl = ["minidom"]

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

@@ -77,8 +77,8 @@ fn from_xml_impl(input: Item) -> Result<TokenStream> {
     let from_events_builder_ty_name = quote::format_ident!("{}FromEvents", ident);
     let state_ty_name = quote::format_ident!("{}FromEventsState", ident);
 
-    let unknown_attr_err = format!("Unknown attribute in {} element.", xml_name.value());
-    let unknown_child_err = format!("Unknown child in {} element.", xml_name.value());
+    let unknown_attr_err = format!("Unknown attribute in {} element.", xml_name.as_str());
+    let unknown_child_err = format!("Unknown child in {} element.", xml_name.as_str());
     let docstr = format!("Build a [`{}`] from XML events", ident);
 
     #[cfg_attr(not(feature = "minidom"), allow(unused_mut))]
@@ -215,15 +215,7 @@ fn into_xml_impl(input: Item) -> Result<TokenStream> {
                             ::xso::exports::rxml::parser::EventMetrics::zero(),
                             (
                                 ::xso::exports::rxml::Namespace::from_str(#xml_namespace),
-                                match ::xso::exports::rxml::NcName::try_from(#xml_name) {
-                                    ::core::result::Result::Ok(v) => v,
-                                    ::core::result::Result::Err(e) => {
-                                        self.0 = ::core::option::Option::None;
-                                        return ::core::option::Option::Some(::core::result::Result::Err(e.into()));
-
-                                    }
-
-                                }
+                                ::xso::exports::rxml::NcName::from(#xml_name),
                             ),
                             ::xso::exports::rxml::AttrMap::new(),
                         )))

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

@@ -9,20 +9,63 @@
 //! This module is concerned with parsing attributes from the Rust "meta"
 //! annotations on structs, enums, enum variants and fields.
 
-use proc_macro2::Span;
+use proc_macro2::{Span, TokenStream};
+use quote::{quote, quote_spanned};
 use syn::{spanned::Spanned, *};
 
+use rxml_validation::NcName;
+
 /// Type alias for a `#[xml(namespace = ..)]` attribute.
 ///
 /// This may, in the future, be replaced by an enum supporting multiple
 /// ways to specify a namespace.
 pub(crate) type NamespaceRef = Path;
 
-/// Type alias for a `#[xml(name = ..)]` attribute.
-///
-/// This may, in the future, be replaced by an enum supporting both `Path` and
-/// `LitStr`.
-pub(crate) type NameRef = LitStr;
+/// Value for the `#[xml(name = .. )]` attribute.
+#[derive(Debug)]
+pub(crate) struct NameRef {
+    value: NcName,
+    span: Span,
+}
+
+impl NameRef {
+    /// Access the XML name as str.
+    ///
+    /// *Note*: This function may vanish in the future if we ever support
+    /// non-literal XML names.
+    pub(crate) fn as_str(&self) -> &str {
+        self.value.as_str()
+    }
+}
+
+impl syn::parse::Parse for NameRef {
+    fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
+        let s: LitStr = input.parse()?;
+        let span = s.span();
+        match NcName::try_from(s.value()) {
+            Ok(value) => Ok(Self { value, span }),
+            Err(e) => Err(Error::new(
+                span,
+                format!("not a valid XML element name: {}", e),
+            )),
+        }
+    }
+}
+
+impl quote::ToTokens for NameRef {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        let value = self.value.as_str();
+        let value = quote_spanned! { self.span=> #value };
+        // SAFETY: self.0 is a known-good NcName, so converting it to an
+        // NcNameStr is known to be safe.
+        // NOTE: we cannot use `quote_spanned! { self.span=> }` for the unsafe
+        // block as that would then in fact trip a `#[deny(unsafe_code)]` lint
+        // at the use site of the macro.
+        tokens.extend(quote! {
+            unsafe { ::xso::exports::rxml::NcNameStr::from_str_unchecked(#value) }
+        })
+    }
+}
 
 /// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
 #[derive(Debug)]

xso/src/from_xml_doc.md πŸ”—

@@ -34,6 +34,18 @@ All key-value pairs interpreted by these derive macros must be wrapped in a
 | `namespace` | *path* | The path to a `&'static str` which holds the XML namespace to match. |
 | `name` | *string literal* | The XML element name to match. |
 
+Note that the `name` value must be a valid XML element name, without colons.
+The namespace prefix, if any, is assigned automatically at serialisation time
+and cannot be overridden. The following will thus not compile:
+
+```compile_fail
+# use xso::FromXml;
+# static MY_NAMESPACE: &'static str = "urn:example";
+#[derive(FromXml, Debug, PartialEq)]
+#[xml(namespace = MY_NAMESPACE, name = "fnord:foo")]  // colon not allowed
+struct Foo;
+```
+
 ## Limitations
 
 Supports only empty structs currently. For example, the following will not