xso-proc: start making derive macros for FromXml and IntoXml

Jonas Schäfer created

For now, these macros only support empty elements. Everything else will
be rejected with a compile-time error.

Change summary

Cargo.toml                      |   2 
parsers/Cargo.toml              |   2 
parsers/src/util/macro_tests.rs | 150 ++++++++++++++++++
parsers/src/util/mod.rs         |   3 
xso-proc/Cargo.toml             |  24 ++
xso-proc/src/lib.rs             | 286 +++++++++++++++++++++++++++++++++++
xso-proc/src/meta.rs            | 120 ++++++++++++++
xso/Cargo.toml                  |   6 
xso/src/from_xml_doc.md         |  50 ++++++
xso/src/lib.rs                  |  20 ++
10 files changed, 662 insertions(+), 1 deletion(-)

Detailed changes

Cargo.toml 🔗

@@ -7,6 +7,7 @@ members = [  # alphabetically sorted
   "tokio-xmpp",
   "xmpp",
   "xso",
+  "xso-proc",
 ]
 resolver = "2"
 
@@ -18,3 +19,4 @@ tokio-xmpp = { path = "tokio-xmpp" }
 xmpp-parsers = { path = "parsers" }
 xmpp = { path = "xmpp" }
 xso = { path = "xso" }
+xso_proc = { path = "xso-proc" }

parsers/Cargo.toml 🔗

@@ -24,7 +24,7 @@ chrono = { version = "0.4.5", default-features = false, features = ["std"] }
 # same repository dependencies
 jid = { version = "0.10", features = ["minidom"], path = "../jid" }
 minidom = { version = "0.15", path = "../minidom" }
-xso = { version = "0.0.2" }
+xso = { version = "0.0.2", features = ["macros", "minidom", "panicking-into-impl"] }
 
 [features]
 # Build xmpp-parsers to make components instead of clients.

parsers/src/util/macro_tests.rs 🔗

@@ -0,0 +1,150 @@
+// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#![deny(
+    non_camel_case_types,
+    non_snake_case,
+    unsafe_code,
+    unused_variables,
+    unused_mut,
+    dead_code
+)]
+
+mod helpers {
+    // we isolate the helpers into a module, because we do not want to have
+    // them in scope below.
+    // this is to ensure that the macros do not have hidden dependencies on
+    // any specific names being imported.
+    use minidom::Element;
+    use xso::{error::FromElementError, transform, try_from_element, FromXml, IntoXml};
+
+    pub(super) fn roundtrip_full<T: IntoXml + FromXml + PartialEq + std::fmt::Debug + Clone>(
+        s: &str,
+    ) {
+        let initial: Element = s.parse().unwrap();
+        let structural: T = match try_from_element(initial.clone()) {
+            Ok(v) => v,
+            Err(e) => panic!("failed to parse from {:?}: {}", s, e),
+        };
+        let recovered =
+            transform(structural.clone()).expect("roundtrip did not produce an element");
+        assert_eq!(initial, recovered);
+        let structural2: T = match try_from_element(recovered) {
+            Ok(v) => v,
+            Err(e) => panic!("failed to parse from serialisation of {:?}: {}", s, e),
+        };
+        assert_eq!(structural, structural2);
+    }
+
+    pub(super) fn parse_str<T: FromXml>(s: &str) -> Result<T, FromElementError> {
+        let initial: Element = s.parse().unwrap();
+        try_from_element(initial)
+    }
+}
+
+use self::helpers::{parse_str, roundtrip_full};
+
+use xso::{FromXml, IntoXml};
+
+// these are adverserial local names in order to trigger any issues with
+// unqualified names in the macro expansions.
+#[allow(dead_code, non_snake_case)]
+fn Err() {}
+#[allow(dead_code, non_snake_case)]
+fn Ok() {}
+#[allow(dead_code, non_snake_case)]
+fn Some() {}
+#[allow(dead_code, non_snake_case)]
+fn None() {}
+#[allow(dead_code)]
+type Option = ((),);
+#[allow(dead_code)]
+type Result = ((),);
+
+static NS1: &str = "urn:example:ns1";
+
+#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "foo")]
+struct Empty;
+
+#[test]
+fn empty_roundtrip() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<Empty>("<foo xmlns='urn:example:ns1'/>");
+}
+
+#[test]
+fn empty_name_mismatch() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<Empty>("<bar xmlns='urn:example:ns1'/>") {
+        Err(xso::error::FromElementError::Mismatch(..)) => (),
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn empty_namespace_mismatch() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<Empty>("<foo xmlns='urn:example:ns2'/>") {
+        Err(xso::error::FromElementError::Mismatch(..)) => (),
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn empty_unexpected_attribute() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<Empty>("<foo xmlns='urn:example:ns1' fnord='bar'/>") {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => {
+            assert_eq!(e, "Unknown attribute in foo element.");
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn empty_unexpected_child() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<Empty>("<foo xmlns='urn:example:ns1'><coucou/></foo>") {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => {
+            assert_eq!(e, "Unknown child in foo element.");
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn empty_qname_check_has_precedence_over_attr_check() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<Empty>("<bar xmlns='urn:example:ns1' fnord='bar'/>") {
+        Err(xso::error::FromElementError::Mismatch(..)) => (),
+        other => panic!("unexpected result: {:?}", other),
+    }
+}

parsers/src/util/mod.rs 🔗

@@ -10,3 +10,6 @@ pub(crate) mod text_node_codecs;
 /// Helper macros to parse and serialise more easily.
 #[macro_use]
 mod macros;
+
+#[cfg(test)]
+mod macro_tests;

xso-proc/Cargo.toml 🔗

@@ -0,0 +1,24 @@
+[package]
+name = "xso_proc"
+version = "0.0.2"
+authors = [
+  "Jonas Schäfer <jonas@zombofant.net>",
+]
+description = "Macro implementation of #[derive(FromXml, IntoXml)]"
+homepage = "https://xmpp.rs"
+repository = "https://gitlab.com/xmpp-rs/xmpp-rs"
+keywords = ["xso", "derive", "serialization"]
+license = "MPL-2.0"
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+quote = "^1"
+syn = { version = "^2", features = ["full", "extra-traits"] }
+proc-macro2 = "^1"
+
+[features]
+panicking-into-impl = ["minidom"]
+minidom = []

xso-proc/src/lib.rs 🔗

@@ -0,0 +1,286 @@
+// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#![forbid(unsafe_code)]
+#![warn(missing_docs)]
+#![allow(rustdoc::private_intra_doc_links)]
+/*!
+# Macros for parsing XML into Rust structs, and vice versa
+
+**If you are a user of `xso_proc` or `xso`, please
+return to `xso` for more information**. The documentation of
+`xso_proc` is geared toward developers of `…_macros` and `…_core`.
+
+**You have been warned.**
+*/
+
+// Wondering about RawTokenStream vs. TokenStream?
+// syn mostly works with proc_macro2, while the proc macros themselves use
+// proc_macro.
+use proc_macro::TokenStream as RawTokenStream;
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::*;
+
+mod meta;
+
+/// Convert an [`syn::Item`] into the parts relevant for us.
+///
+/// If the item is of an unsupported variant, an appropriate error is
+/// returned.
+fn parse_struct(item: Item) -> Result<(Visibility, meta::XmlCompoundMeta, Ident)> {
+    match item {
+        Item::Struct(item) => {
+            match item.fields {
+                Fields::Unit => (),
+                other => {
+                    return Err(Error::new_spanned(
+                        other,
+                        "cannot derive on non-unit struct (yet!)",
+                    ))
+                }
+            }
+            let meta = meta::XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
+            Ok((item.vis, meta, item.ident))
+        }
+        other => Err(Error::new_spanned(other, "cannot derive on this item")),
+    }
+}
+
+/// Generate a `xso::FromXml` implementation for the given item, or fail with
+/// a proper compiler error.
+fn from_xml_impl(input: Item) -> Result<TokenStream> {
+    let (
+        vis,
+        meta::XmlCompoundMeta {
+            namespace,
+            name,
+            span,
+        },
+        ident,
+    ) = parse_struct(input)?;
+
+    // we rebind to a different name here because otherwise some expressions
+    // inside `quote! {}` below get a bit tricky to read (such as
+    // `name.1 == #name`).
+    let Some(xml_namespace) = namespace else {
+        return Err(Error::new(span, "`namespace` key is required"));
+    };
+
+    let Some(xml_name) = name else {
+        return Err(Error::new(span, "`name` key is required"));
+    };
+
+    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 docstr = format!("Build a [`{}`] from XML events", ident);
+
+    #[cfg_attr(not(feature = "minidom"), allow(unused_mut))]
+    let mut result = quote! {
+        enum #state_ty_name {
+            Default,
+        }
+
+        #[doc = #docstr]
+        #vis struct #from_events_builder_ty_name(::core::option::Option<#state_ty_name>);
+
+        impl ::xso::FromEventsBuilder for #from_events_builder_ty_name {
+            type Output = #ident;
+
+            fn feed(
+                &mut self,
+                ev: ::xso::exports::rxml::Event
+            ) -> ::core::result::Result<::core::option::Option<Self::Output>, ::xso::error::Error> {
+                match self.0 {
+                    ::core::option::Option::None => panic!("feed() called after it returned a non-None value"),
+                    ::core::option::Option::Some(#state_ty_name::Default) => match ev {
+                        ::xso::exports::rxml::Event::StartElement(..) => {
+                            ::core::result::Result::Err(::xso::error::Error::Other(#unknown_child_err))
+                        }
+                        ::xso::exports::rxml::Event::EndElement(..) => {
+                            self.0 = ::core::option::Option::None;
+                            ::core::result::Result::Ok(::core::option::Option::Some(#ident))
+                        }
+                        ::xso::exports::rxml::Event::Text(..) => {
+                            ::core::result::Result::Err(::xso::error::Error::Other("Unexpected text content".into()))
+                        }
+                        // we ignore these: a correct parser only generates
+                        // them at document start, and there we want to indeed
+                        // not worry about them being in front of the first
+                        // element.
+                        ::xso::exports::rxml::Event::XmlDeclaration(_, ::xso::exports::rxml::XmlVersion::V1_0) => ::core::result::Result::Ok(::core::option::Option::None)
+                    }
+                }
+            }
+        }
+
+        impl ::xso::FromXml for #ident {
+            type Builder = #from_events_builder_ty_name;
+
+            fn from_events(
+                name: ::xso::exports::rxml::QName,
+                attrs: ::xso::exports::rxml::AttrMap,
+            ) -> ::core::result::Result<Self::Builder, ::xso::error::FromEventsError> {
+                if name.0 != #xml_namespace || name.1 != #xml_name {
+                    return ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs });
+                }
+                if attrs.len() > 0 {
+                    return ::core::result::Result::Err(::xso::error::Error::Other(#unknown_attr_err).into());
+                }
+                ::core::result::Result::Ok(#from_events_builder_ty_name(::core::option::Option::Some(#state_ty_name::Default)))
+            }
+        }
+    };
+
+    #[cfg(feature = "minidom")]
+    result.extend(quote! {
+        impl ::std::convert::TryFrom<::xso::exports::minidom::Element> for #ident {
+            type Error = ::xso::error::FromElementError;
+
+            fn try_from(other: ::xso::exports::minidom::Element) -> ::core::result::Result<Self, Self::Error> {
+                ::xso::try_from_element(other)
+            }
+        }
+    });
+
+    Ok(result)
+}
+
+/// Macro to derive a `xso::FromXml` implementation on a type.
+///
+/// The user-facing documentation for this macro lives in the `xso` crate.
+#[proc_macro_derive(FromXml, attributes(xml))]
+pub fn from_xml(input: RawTokenStream) -> RawTokenStream {
+    // Shim wrapper around `from_xml_impl` which converts any errors into
+    // actual compiler errors within the resulting token stream.
+    let item = syn::parse_macro_input!(input as Item);
+    match from_xml_impl(item) {
+        Ok(v) => v.into(),
+        Err(e) => e.into_compile_error().into(),
+    }
+}
+
+/// Generate a `xso::IntoXml` implementation for the given item, or fail with
+/// a proper compiler error.
+fn into_xml_impl(input: Item) -> Result<TokenStream> {
+    let (
+        vis,
+        meta::XmlCompoundMeta {
+            namespace,
+            name,
+            span,
+        },
+        ident,
+    ) = parse_struct(input)?;
+
+    // we rebind to a different name here to stay consistent with
+    // `from_xml_impl`.
+    let Some(xml_namespace) = namespace else {
+        return Err(Error::new(span, "`namespace` key is required"));
+    };
+
+    let Some(xml_name) = name else {
+        return Err(Error::new(span, "`name` key is required"));
+    };
+
+    let into_events_iter_ty_name = quote::format_ident!("{}IntoEvents", ident);
+    let state_ty_name = quote::format_ident!("{}IntoEventsState", ident);
+
+    let docstr = format!("Decompose a [`{}`] into XML events", ident);
+
+    #[cfg_attr(not(feature = "minidom"), allow(unused_mut))]
+    let mut result = quote! {
+        enum #state_ty_name {
+            Header,
+            Footer,
+        }
+
+        #[doc = #docstr]
+        #vis struct #into_events_iter_ty_name(::core::option::Option<#state_ty_name>);
+
+        impl ::std::iter::Iterator for #into_events_iter_ty_name {
+            type Item = ::core::result::Result<::xso::exports::rxml::Event, ::xso::error::Error>;
+
+            fn next(&mut self) -> ::core::option::Option<Self::Item> {
+                match self.0 {
+                    ::core::option::Option::Some(#state_ty_name::Header) => {
+                        self.0 = ::core::option::Option::Some(#state_ty_name::Footer);
+                        ::core::option::Option::Some(::core::result::Result::Ok(::xso::exports::rxml::Event::StartElement(
+                            ::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::AttrMap::new(),
+                        )))
+                    }
+                    ::core::option::Option::Some(#state_ty_name::Footer) => {
+                        self.0 = ::core::option::Option::None;
+                        ::core::option::Option::Some(::core::result::Result::Ok(::xso::exports::rxml::Event::EndElement(
+                            ::xso::exports::rxml::parser::EventMetrics::zero(),
+                        )))
+                    }
+                    ::core::option::Option::None => ::core::option::Option::None,
+                }
+            }
+        }
+
+        impl ::xso::IntoXml for #ident {
+            type EventIter = #into_events_iter_ty_name;
+
+            fn into_event_iter(self) -> ::core::result::Result<Self::EventIter, ::xso::error::Error> {
+                ::core::result::Result::Ok(#into_events_iter_ty_name(::core::option::Option::Some(#state_ty_name::Header)))
+            }
+        }
+    };
+
+    #[cfg(all(feature = "minidom", feature = "panicking-into-impl"))]
+    result.extend(quote! {
+        impl ::std::convert::From<#ident> for ::xso::exports::minidom::Element {
+            fn from(other: #ident) -> Self {
+                ::xso::transform(other).expect("seamless conversion into minidom::Element")
+            }
+        }
+    });
+
+    #[cfg(all(feature = "minidom", not(feature = "panicking-into-impl")))]
+    result.extend(quote! {
+        impl ::std::convert::TryFrom<#ident> for ::xso::exports::minidom::Element {
+            type Error = ::xso::error::Error;
+
+            fn try_from(other: #ident) -> ::core::result::Result<Self, Self::Error> {
+                ::xso::transform(other)
+            }
+        }
+    });
+
+    Ok(result)
+}
+
+/// Macro to derive a `xso::IntoXml` implementation on a type.
+///
+/// The user-facing documentation for this macro lives in the `xso` crate.
+#[proc_macro_derive(IntoXml, attributes(xml))]
+pub fn into_xml(input: RawTokenStream) -> RawTokenStream {
+    // Shim wrapper around `into_xml_impl` which converts any errors into
+    // actual compiler errors within the resulting token stream.
+    let item = syn::parse_macro_input!(input as Item);
+    match into_xml_impl(item) {
+        Ok(v) => v.into(),
+        Err(e) => e.into_compile_error().into(),
+    }
+}

xso-proc/src/meta.rs 🔗

@@ -0,0 +1,120 @@
+// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+//! # Parse Rust attributes
+//!
+//! This module is concerned with parsing attributes from the Rust "meta"
+//! annotations on structs, enums, enum variants and fields.
+
+use proc_macro2::Span;
+use syn::{spanned::Spanned, *};
+
+/// 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;
+
+/// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
+#[derive(Debug)]
+pub(crate) struct XmlCompoundMeta {
+    /// The span of the `#[xml(..)]` meta from which this was parsed.
+    ///
+    /// This is useful for error messages.
+    pub(crate) span: Span,
+
+    /// The value assigned to `namespace` inside `#[xml(..)]`, if any.
+    pub(crate) namespace: Option<NamespaceRef>,
+
+    /// The value assigned to `name` inside `#[xml(..)]`, if any.
+    pub(crate) name: Option<NameRef>,
+}
+
+impl XmlCompoundMeta {
+    /// Parse the meta values from a `#[xml(..)]` attribute.
+    ///
+    /// Undefined options or options with incompatible values are rejected
+    /// with an appropriate compile-time error.
+    fn parse_from_attribute(attr: &Attribute) -> Result<Self> {
+        let mut namespace = None;
+        let mut name = None;
+
+        attr.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 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"))
+            }
+        })?;
+
+        Ok(Self {
+            span: attr.span(),
+            namespace,
+            name,
+        })
+    }
+
+    /// Search through `attrs` for a single `#[xml(..)]` attribute and parse
+    /// it.
+    ///
+    /// Undefined options or options with incompatible values are rejected
+    /// with an appropriate compile-time error.
+    ///
+    /// If more than one `#[xml(..)]` attribute is found, an error is
+    /// emitted.
+    ///
+    /// If no `#[xml(..)]` attribute is found, `None` is returned.
+    pub(crate) fn try_parse_from_attributes(attrs: &[Attribute]) -> Result<Option<Self>> {
+        let mut result = None;
+        for attr in attrs {
+            if !attr.path().is_ident("xml") {
+                continue;
+            }
+            if result.is_some() {
+                return Err(syn::Error::new_spanned(
+                    attr.path(),
+                    "only one #[xml(..)] per struct or enum variant allowed",
+                ));
+            }
+            result = Some(Self::parse_from_attribute(attr)?);
+        }
+        Ok(result)
+    }
+
+    /// Search through `attrs` for a single `#[xml(..)]` attribute and parse
+    /// it.
+    ///
+    /// Undefined options or options with incompatible values are rejected
+    /// with an appropriate compile-time error.
+    ///
+    /// If more than one or no `#[xml(..)]` attribute is found, an error is
+    /// emitted.
+    pub(crate) fn parse_from_attributes(attrs: &[Attribute]) -> Result<Self> {
+        match Self::try_parse_from_attributes(attrs)? {
+            Some(v) => Ok(v),
+            None => Err(syn::Error::new(
+                Span::call_site(),
+                "#[xml(..)] attribute required on struct or enum variant",
+            )),
+        }
+    }
+}

xso/Cargo.toml 🔗

@@ -12,3 +12,9 @@ license = "MPL-2.0"
 [dependencies]
 rxml = { version = "0.11.0", default-features = false }
 minidom = { version = "^0.15" }
+xso_proc = { version = "0.0.2", optional = true }
+
+[features]
+macros = [ "dep:xso_proc" ]
+minidom = [ "xso_proc/minidom"]
+panicking-into-impl = ["xso_proc/panicking-into-impl"]

xso/src/from_xml_doc.md 🔗

@@ -0,0 +1,50 @@
+# Make a struct or enum parseable from XML
+
+This derives the [`FromXml`] trait on a struct or enum. It is the counterpart
+to [`macro@IntoXml`].
+
+## Example
+
+```rust
+# use xso::FromXml;
+static MY_NAMESPACE: &str = "urn:example";
+
+#[derive(FromXml, Debug, PartialEq)]
+#[xml(namespace = MY_NAMESPACE, name = "foo")]
+struct Foo;
+
+let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example'/>").unwrap();
+assert_eq!(foo, Foo);
+```
+
+## Attributes
+
+The derive macros need to know which XML namespace and name the elements it
+is supposed have. This must be specified via key-value pairs on the type the
+derive macro is invoked on. These are specified as Rust attributes. In order
+to disambiguate between XML attributes and Rust attributes, we are going to
+refer to Rust attributes using the term *meta* instead, which is consistent
+with the Rust language reference calling that syntax construct *meta*.
+
+All key-value pairs interpreted by these derive macros must be wrapped in a
+`#[xml( ... )]` *meta*. The following keys are defined on structs:
+
+| Key | Value type | Description |
+| --- | --- | --- |
+| `namespace` | *path* | The path to a `&'static str` which holds the XML namespace to match. |
+| `name` | *string literal* | The XML element name to match. |
+
+## Limitations
+
+Supports only empty structs currently. For example, the following will not
+work:
+
+```compile_fail
+# use xso::FromXml;
+# static MY_NAMESPACE: &str = "urn:example";
+#[derive(FromXml, Debug, PartialEq)]
+#[xml(namespace = MY_NAMESPACE, name = "foo")]
+struct Foo {
+    some_field: String,
+}
+```

xso/src/lib.rs 🔗

@@ -20,13 +20,32 @@ use of this library in parsing XML streams like specified in RFC 6120.
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 pub mod error;
+#[cfg(feature = "minidom")]
 pub mod minidom_compat;
 
 #[doc(hidden)]
 pub mod exports {
+    #[cfg(feature = "minidom")]
+    pub use minidom;
     pub use rxml;
 }
 
+#[doc = include_str!("from_xml_doc.md")]
+#[doc(inline)]
+#[cfg(feature = "macros")]
+pub use xso_proc::FromXml;
+
+/// # Make a struct or enum serialisable to XML
+///
+/// This derives the [`IntoXml`] trait on a struct or enum. It is the
+/// counterpart to [`macro@FromXml`].
+///
+/// The attributes necessary and available for the derivation to work are
+/// documented on [`macro@FromXml`].
+#[doc(inline)]
+#[cfg(feature = "macros")]
+pub use xso_proc::IntoXml;
+
 /// Trait allowing to consume a struct and iterate its contents as
 /// serialisable [`rxml::Event`] items.
 ///
@@ -145,6 +164,7 @@ pub fn transform<T: FromXml, F: IntoXml>(from: F) -> Result<T, self::error::Erro
 /// Unlike [`transform`] (which can also be used with an element), this
 /// function will return the element unharmed if its element header does not
 /// match the expectations of `T`.
+#[cfg(feature = "minidom")]
 pub fn try_from_element<T: FromXml>(
     from: minidom::Element,
 ) -> Result<T, self::error::FromElementError> {