xso-proc: add support for parsing attributes into Strings

Jonas Schäfer created

This is bare-bones and is missing many features which we intend to add
in future commits, such as parsing from attributes whose names differ
from the field names and parsing into non-String types.

Change summary

parsers/src/util/macro_tests.rs |  45 +++++++
xso-proc/src/compound.rs        | 144 ++++++++++++++++++----
xso-proc/src/error_message.rs   |  81 +++++++++++++
xso-proc/src/field.rs           | 215 +++++++++++++++++++++++++++++++++++
xso-proc/src/lib.rs             |   3 
xso-proc/src/meta.rs            | 100 ++++++++++++++++
xso-proc/src/scope.rs           |  73 +++++++++++
xso-proc/src/state.rs           |   7 +
xso-proc/src/structs.rs         |   2 
xso/src/from_xml_doc.md         |  45 ++++--
10 files changed, 665 insertions(+), 50 deletions(-)

Detailed changes

parsers/src/util/macro_tests.rs 🔗

@@ -183,3 +183,48 @@ fn namespace_lit_roundtrip() {
     };
     roundtrip_full::<NamespaceLit>("<baz xmlns='urn:example:ns2'/>");
 }
+
+#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "attr")]
+struct RequiredAttribute {
+    #[xml(attribute)]
+    foo: String,
+}
+
+#[test]
+fn required_attribute_roundtrip() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<RequiredAttribute>("<attr xmlns='urn:example:ns1' foo='bar'/>");
+}
+
+#[test]
+fn required_attribute_positive() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    let data = parse_str::<RequiredAttribute>("<attr xmlns='urn:example:ns1' foo='bar'/>").unwrap();
+    assert_eq!(data.foo, "bar");
+}
+
+#[test]
+fn required_attribute_missing() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<RequiredAttribute>("<attr xmlns='urn:example:ns1'/>") {
+        Err(::xso::error::FromElementError::Invalid(::xso::error::Error::Other(e)))
+            if e.contains("Required attribute field") && e.contains("missing") =>
+        {
+            ()
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}

xso-proc/src/compound.rs 🔗

@@ -7,29 +7,41 @@
 //! Handling of the insides of compound structures (structs and enum variants)
 
 use proc_macro2::{Span, TokenStream};
-use quote::{quote, ToTokens};
+use quote::quote;
 use syn::*;
 
+use crate::error_message::ParentRef;
+use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit};
+use crate::scope::{mangle_member, FromEventsScope, IntoEventsScope};
 use crate::state::{FromEventsSubmachine, IntoEventsSubmachine, State};
 use crate::types::qname_ty;
 
 /// A struct or enum variant's contents.
-pub(crate) struct Compound;
+pub(crate) struct Compound {
+    /// The fields of this compound.
+    fields: Vec<FieldDef>,
+}
 
 impl Compound {
     /// Construct a compound from fields.
     pub(crate) fn from_fields(compound_fields: &Fields) -> Result<Self> {
-        match compound_fields {
-            Fields::Unit => (),
-            other => {
-                return Err(Error::new_spanned(
-                    other,
-                    "cannot derive on non-unit struct (yet!)",
-                ))
-            }
+        let mut fields = Vec::with_capacity(compound_fields.len());
+        for (i, field) in compound_fields.iter().enumerate() {
+            let index = match i.try_into() {
+                Ok(v) => v,
+                // we are converting to u32, are you crazy?!
+                // (u32, because syn::Member::Index needs that.)
+                Err(_) => {
+                    return Err(Error::new_spanned(
+                        field,
+                        "okay, mate, that are way too many fields. get your life together.",
+                    ))
+                }
+            };
+            fields.push(FieldDef::from_field(field, index)?);
         }
 
-        Ok(Self)
+        Ok(Self { fields })
     }
 
     /// Make and return a set of states which is used to construct the target
@@ -40,9 +52,12 @@ impl Compound {
     pub(crate) fn make_from_events_statemachine(
         &self,
         state_ty_ident: &Ident,
-        output_cons: &Path,
+        output_name: &ParentRef,
         state_prefix: &str,
     ) -> Result<FromEventsSubmachine> {
+        let scope = FromEventsScope::new();
+        let FromEventsScope { ref attrs, .. } = scope;
+
         let default_state_ident = quote::format_ident!("{}Default", state_prefix);
         let builder_data_ident = quote::format_ident!("__data");
         let builder_data_ty: Type = TypePath {
@@ -52,9 +67,44 @@ impl Compound {
         .into();
         let mut states = Vec::new();
 
-        let readable_name = output_cons.to_token_stream().to_string();
-        let unknown_attr_err = format!("Unknown attribute in {} element.", readable_name);
-        let unknown_child_err = format!("Unknown child in {} element.", readable_name);
+        let mut builder_data_def = TokenStream::default();
+        let mut builder_data_init = TokenStream::default();
+        let mut output_cons = TokenStream::default();
+
+        for field in self.fields.iter() {
+            let member = field.member();
+            let builder_field_name = mangle_member(member);
+            let part = field.make_builder_part(&scope, &output_name)?;
+
+            match part {
+                FieldBuilderPart::Init {
+                    value: FieldTempInit { ty, init },
+                } => {
+                    builder_data_def.extend(quote! {
+                        #builder_field_name: #ty,
+                    });
+
+                    builder_data_init.extend(quote! {
+                        #builder_field_name: #init,
+                    });
+
+                    output_cons.extend(quote! {
+                        #member: #builder_data_ident.#builder_field_name,
+                    });
+                }
+            }
+        }
+
+        let unknown_attr_err = format!("Unknown attribute in {}.", output_name);
+        let unknown_child_err = format!("Unknown child in {}.", output_name);
+
+        let output_cons = match output_name {
+            ParentRef::Named(ref path) => {
+                quote! {
+                    #path { #output_cons }
+                }
+            }
+        };
 
         states.push(State::new_with_builder(
             default_state_ident.clone(),
@@ -86,18 +136,21 @@ impl Compound {
 
         Ok(FromEventsSubmachine {
             defs: quote! {
-                struct #builder_data_ty;
+                struct #builder_data_ty {
+                    #builder_data_def
+                }
             },
             states,
             init: quote! {
-                if attrs.len() > 0 {
+                let #builder_data_ident = #builder_data_ty {
+                    #builder_data_init
+                };
+                if #attrs.len() > 0 {
                     return ::core::result::Result::Err(::xso::error::Error::Other(
                         #unknown_attr_err,
                     ).into());
                 }
-                ::core::result::Result::Ok(#state_ty_ident::#default_state_ident {
-                    #builder_data_ident: #builder_data_ty,
-                })
+                ::core::result::Result::Ok(#state_ty_ident::#default_state_ident { #builder_data_ident })
             },
         })
     }
@@ -116,23 +169,54 @@ impl Compound {
         input_name: &Path,
         state_prefix: &str,
     ) -> Result<IntoEventsSubmachine> {
+        let scope = IntoEventsScope::new();
+        let IntoEventsScope { ref attrs, .. } = scope;
+
         let start_element_state_ident = quote::format_ident!("{}StartElement", state_prefix);
         let end_element_state_ident = quote::format_ident!("{}EndElement", state_prefix);
         let name_ident = quote::format_ident!("name");
         let mut states = Vec::new();
 
+        let mut init_body = TokenStream::default();
+        let mut destructure = TokenStream::default();
+        let mut start_init = TokenStream::default();
+
         states.push(
             State::new(start_element_state_ident.clone())
-                .with_field(&name_ident, &qname_ty(Span::call_site()))
-                .with_impl(quote! {
-                    ::core::option::Option::Some(::xso::exports::rxml::Event::StartElement(
-                        ::xso::exports::rxml::parser::EventMetrics::zero(),
-                        #name_ident,
-                        ::xso::exports::rxml::AttrMap::new(),
-                    ))
-                }),
+                .with_field(&name_ident, &qname_ty(Span::call_site())),
         );
 
+        for field in self.fields.iter() {
+            let member = field.member();
+            let bound_name = mangle_member(member);
+            let part = field.make_iterator_part(&scope, &bound_name)?;
+
+            match part {
+                FieldIteratorPart::Header { setter } => {
+                    destructure.extend(quote! {
+                        #member: #bound_name,
+                    });
+                    init_body.extend(setter);
+                    start_init.extend(quote! {
+                        #bound_name,
+                    });
+                    states[0].add_field(&bound_name, field.ty());
+                }
+            }
+        }
+
+        states[0].set_impl(quote! {
+            {
+                let mut #attrs = ::xso::exports::rxml::AttrMap::new();
+                #init_body
+                ::core::option::Option::Some(::xso::exports::rxml::Event::StartElement(
+                    ::xso::exports::rxml::parser::EventMetrics::zero(),
+                    #name_ident,
+                    #attrs,
+                ))
+            }
+        });
+
         states.push(
             State::new(end_element_state_ident.clone()).with_impl(quote! {
                 ::core::option::Option::Some(::xso::exports::rxml::Event::EndElement(
@@ -145,10 +229,10 @@ impl Compound {
             defs: TokenStream::default(),
             states,
             destructure: quote! {
-                #input_name
+                #input_name { #destructure }
             },
             init: quote! {
-                Self::#start_element_state_ident { #name_ident }
+                Self::#start_element_state_ident { #name_ident, #start_init }
             },
         })
     }

xso-proc/src/error_message.rs 🔗

@@ -0,0 +1,81 @@
+// 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/.
+
+//! Infrastructure for contextual error messages
+
+use std::fmt;
+
+use syn::*;
+
+/// Reference to a compound field's parent
+///
+/// This reference can be converted to a hopefully-useful human-readable
+/// string via [`std::fmt::Display`].
+#[derive(Clone, Debug)]
+pub(super) enum ParentRef {
+    /// The parent is addressable by a path, e.g. a struct type or enum
+    /// variant.
+    Named(Path),
+}
+
+impl From<Path> for ParentRef {
+    fn from(other: Path) -> Self {
+        Self::Named(other)
+    }
+}
+
+impl From<&Path> for ParentRef {
+    fn from(other: &Path) -> Self {
+        Self::Named(other.clone())
+    }
+}
+
+impl fmt::Display for ParentRef {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Named(name) => {
+                let mut first = true;
+                for segment in name.segments.iter() {
+                    if !first || name.leading_colon.is_some() {
+                        write!(f, "::")?;
+                    }
+                    first = false;
+                    write!(f, "{}", segment.ident)?;
+                }
+                write!(f, " element")
+            }
+        }
+    }
+}
+
+/// Ephemeral struct to create a nice human-readable representation of
+/// [`syn::Member`].
+///
+/// It implements [`std::fmt::Display`] for that purpose and is otherwise of
+/// little use.
+#[repr(transparent)]
+struct FieldName<'x>(&'x Member);
+
+impl fmt::Display for FieldName<'_> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self.0 {
+            Member::Named(v) => write!(f, "field '{}'", v),
+            Member::Unnamed(v) => write!(f, "unnamed field {}", v.index),
+        }
+    }
+}
+
+/// Create a string error message for a missing attribute.
+///
+/// `parent_name` should point at the compound which is being parsed and
+/// `field` should be the field to which the attribute belongs.
+pub(super) fn on_missing_attribute(parent_name: &ParentRef, field: &Member) -> String {
+    format!(
+        "Required attribute {} on {} missing.",
+        FieldName(&field),
+        parent_name
+    )
+}

xso-proc/src/field.rs 🔗

@@ -0,0 +1,215 @@
+// 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/.
+
+//! Compound (struct or enum variant) field types
+
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{spanned::Spanned, *};
+
+use rxml_validation::NcName;
+
+use crate::error_message::{self, ParentRef};
+use crate::meta::{NameRef, XmlFieldMeta};
+use crate::scope::{FromEventsScope, IntoEventsScope};
+
+/// Code slices necessary for declaring and initializing a temporary variable
+/// for parsing purposes.
+pub(crate) struct FieldTempInit {
+    /// The type of the temporary variable.
+    pub(crate) ty: Type,
+
+    /// The initializer for the temporary variable.
+    pub(crate) init: TokenStream,
+}
+
+/// Describe how a struct or enum variant's member is parsed from XML data.
+///
+/// This struct is returned from [`FieldDef::make_builder_part`] and
+/// contains code snippets and instructions for
+/// [`Compound::make_from_events_statemachine`][`crate::compound::Compound::make_from_events_statemachine`]
+/// to parse the field's data from XML.
+pub(crate) enum FieldBuilderPart {
+    /// Parse a field from the item's element's start event.
+    Init {
+        /// Expression and type which extracts the field's data from the
+        /// element's start event.
+        value: FieldTempInit,
+    },
+}
+
+/// Describe how a struct or enum variant's member is converted to XML data.
+///
+/// This struct is returned from [`FieldDef::make_iterator_part`] and
+/// contains code snippets and instructions for
+/// [`Compound::make_into_events_statemachine`][`crate::compound::Compound::make_into_events_statemachine`]
+/// to convert the field's data into XML.
+pub(crate) enum FieldIteratorPart {
+    /// The field is emitted as part of StartElement.
+    Header {
+        /// A sequence of statements which updates the temporary variables
+        /// during the StartElement event's construction, consuming the
+        /// field's value.
+        setter: TokenStream,
+    },
+}
+
+/// Specify how the field is mapped to XML.
+enum FieldKind {
+    /// The field maps to an attribute.
+    Attribute {
+        /// The XML name of the attribute.
+        xml_name: NameRef,
+    },
+}
+
+impl FieldKind {
+    /// Construct a new field implementation from the meta attributes.
+    ///
+    /// `field_ident` is, for some field types, used to infer an XML name if
+    /// it is not specified explicitly.
+    fn from_meta(meta: XmlFieldMeta, field_ident: Option<&Ident>) -> Result<Self> {
+        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),
+                        ))
+                    }
+                };
+
+                Ok(Self::Attribute {
+                    xml_name: NameRef::Literal {
+                        span: field_ident.span(),
+                        value: xml_name,
+                    },
+                })
+            }
+        }
+    }
+}
+
+/// Definition of a single field in a compound.
+///
+/// See [`Compound`][`crate::compound::Compound`] for more information on
+/// compounds in general.
+pub(crate) struct FieldDef {
+    /// The member identifying the field.
+    member: Member,
+
+    /// The type of the field.
+    ty: Type,
+
+    /// The way the field is mapped to XML.
+    kind: FieldKind,
+}
+
+impl FieldDef {
+    /// Create a new field definition from its declaration.
+    ///
+    /// The `index` must be the zero-based index of the field even for named
+    /// fields.
+    pub(crate) fn from_field(field: &syn::Field, index: u32) -> Result<Self> {
+        let field_span = field.span();
+        let meta = XmlFieldMeta::parse_from_attributes(&field.attrs, &field_span)?;
+
+        let (member, ident) = match field.ident.as_ref() {
+            Some(v) => (Member::Named(v.clone()), Some(v)),
+            None => (
+                Member::Unnamed(Index {
+                    index,
+                    span: field_span,
+                }),
+                None,
+            ),
+        };
+
+        let ty = field.ty.clone();
+
+        Ok(Self {
+            member,
+            ty,
+            kind: FieldKind::from_meta(meta, ident)?,
+        })
+    }
+
+    /// Access the [`syn::Member`] identifying this field in the original
+    /// type.
+    pub(crate) fn member(&self) -> &Member {
+        &self.member
+    }
+
+    /// Access the field's type.
+    pub(crate) fn ty(&self) -> &Type {
+        &self.ty
+    }
+
+    /// Construct the builder pieces for this field.
+    ///
+    /// `container_name` must be a reference to the compound's type, so that
+    /// it can be used for error messages.
+    pub(crate) fn make_builder_part(
+        &self,
+        scope: &FromEventsScope,
+        container_name: &ParentRef,
+    ) -> Result<FieldBuilderPart> {
+        match self.kind {
+            FieldKind::Attribute { ref xml_name } => {
+                let FromEventsScope { ref attrs, .. } = scope;
+
+                let missing_msg = error_message::on_missing_attribute(container_name, &self.member);
+
+                return Ok(FieldBuilderPart::Init {
+                    value: FieldTempInit {
+                        ty: self.ty.clone(),
+                        init: quote! {
+                            match #attrs.remove(::xso::exports::rxml::Namespace::none(), #xml_name) {
+                                ::core::option::Option::Some(v) => v,
+                                ::core::option::Option::None => return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into()),
+                            }
+                        },
+                    },
+                });
+            }
+        }
+    }
+
+    /// Construct the iterator pieces for this field.
+    ///
+    /// `bound_name` must be the name to which the field's value is bound in
+    /// the iterator code.
+    pub(crate) fn make_iterator_part(
+        &self,
+        scope: &IntoEventsScope,
+        bound_name: &Ident,
+    ) -> Result<FieldIteratorPart> {
+        match self.kind {
+            FieldKind::Attribute { ref xml_name } => {
+                let IntoEventsScope { ref attrs, .. } = scope;
+
+                return Ok(FieldIteratorPart::Header {
+                    setter: quote! {
+                        #attrs.insert(
+                            ::xso::exports::rxml::Namespace::NONE,
+                            #xml_name.to_owned(),
+                            #bound_name,
+                        );
+                    },
+                });
+            }
+        }
+    }
+}

xso-proc/src/lib.rs 🔗

@@ -26,7 +26,10 @@ use quote::quote;
 use syn::*;
 
 mod compound;
+mod error_message;
+mod field;
 mod meta;
+mod scope;
 mod state;
 mod structs;
 mod types;

xso-proc/src/meta.rs 🔗

@@ -11,7 +11,7 @@
 
 use proc_macro2::{Span, TokenStream};
 use quote::{quote, quote_spanned};
-use syn::{spanned::Spanned, *};
+use syn::{meta::ParseNestedMeta, spanned::Spanned, *};
 
 use rxml_validation::NcName;
 
@@ -194,3 +194,101 @@ impl XmlCompoundMeta {
         }
     }
 }
+
+/// Contents of an `#[xml(..)]` attribute on a struct or enum variant member.
+#[derive(Debug)]
+pub(crate) enum XmlFieldMeta {
+    Attribute {
+        /// The span of the `#[xml(attribute)]` meta from which this was parsed.
+        ///
+        /// This is useful for error messages.
+        span: Span,
+    },
+}
+
+impl XmlFieldMeta {
+    /// Parse a `#[xml(attribute(..))]` meta.
+    fn attribute_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
+        Ok(Self::Attribute {
+            span: meta.path.span(),
+        })
+    }
+
+    /// Parse [`Self`] from a nestd meta, switching on the identifier
+    /// of that nested meta.
+    fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
+        if meta.path.is_ident("attribute") {
+            Self::attribute_from_meta(meta)
+        } else {
+            Err(Error::new_spanned(meta.path, "unsupported field meta"))
+        }
+    }
+
+    /// Parse an `#[xml(..)]` meta on a field.
+    ///
+    /// This switches based on the first identifier within the `#[xml(..)]`
+    /// meta and generates an enum variant accordingly.
+    ///
+    /// Only a single nested meta is allowed; more than one will be
+    /// rejected with an appropriate compile-time error.
+    ///
+    /// If no meta is contained at all, a compile-time error is generated.
+    ///
+    /// Undefined options or options with incompatible values are rejected
+    /// with an appropriate compile-time error.
+    pub(crate) fn parse_from_attribute(attr: &Attribute) -> Result<Self> {
+        let mut result: Option<Self> = None;
+
+        attr.parse_nested_meta(|meta| {
+            if result.is_some() {
+                return Err(Error::new_spanned(
+                    meta.path,
+                    "multiple field type specifiers are not supported",
+                ));
+            }
+
+            result = Some(Self::parse_from_meta(meta)?);
+            Ok(())
+        })?;
+
+        if let Some(result) = result {
+            Ok(result)
+        } else {
+            Err(Error::new_spanned(
+                attr,
+                "missing field type specifier within `#[xml(..)]`",
+            ))
+        }
+    }
+
+    /// Find and parse a `#[xml(..)]` meta on a field.
+    ///
+    /// This invokes [`Self::parse_from_attribute`] internally on the first
+    /// encountered `#[xml(..)]` meta.
+    ///
+    /// If not exactly one `#[xml(..)]` meta is encountered, an error is
+    /// returned. The error is spanned to `err_span`.
+    pub(crate) fn parse_from_attributes(attrs: &[Attribute], err_span: &Span) -> Result<Self> {
+        let mut result: Option<Self> = None;
+        for attr in attrs {
+            if !attr.path().is_ident("xml") {
+                continue;
+            }
+
+            if result.is_some() {
+                return Err(Error::new_spanned(
+                    attr,
+                    "only one #[xml(..)] attribute per field allowed.",
+                ));
+            }
+
+            result = Some(Self::parse_from_attribute(attr)?);
+        }
+
+        if let Some(result) = result {
+            Ok(result)
+        } else {
+            Err(Error::new(*err_span, "missing #[xml(..)] meta on field"))
+        }
+    }
+}

xso-proc/src/scope.rs 🔗

@@ -0,0 +1,73 @@
+// 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/.
+
+//! Identifiers used within generated code.
+
+use proc_macro2::Span;
+use syn::*;
+
+/// Container struct for various identifiers used throughout the parser code.
+///
+/// This struct is passed around from the [`crate::compound::Compound`]
+/// downward to the code generators in order to ensure that everyone is on the
+/// same page about which identifiers are used for what.
+///
+/// The recommended usage is to bind the names which are needed into the local
+/// scope like this:
+///
+/// ```text
+/// # let scope = FromEventsScope::new();
+/// let FromEventsScope {
+///     ref attrs,
+///     ..
+/// } = scope;
+/// ```
+pub(crate) struct FromEventsScope {
+    /// Accesses the `AttrMap` from code in
+    /// [`crate::field::FieldBuilderPart::Init`].
+    pub(crate) attrs: Ident,
+}
+
+impl FromEventsScope {
+    /// Create a fresh scope with all necessary identifiers.
+    pub(crate) fn new() -> Self {
+        // Sadly, `Ident::new` is not `const`, so we have to create even the
+        // well-known identifiers from scratch all the time.
+        Self {
+            attrs: Ident::new("attrs", Span::call_site()),
+        }
+    }
+}
+
+/// Container struct for various identifiers used throughout the generator
+/// code.
+///
+/// This struct is passed around from the [`crate::compound::Compound`]
+/// downward to the code generators in order to ensure that everyone is on the
+/// same page about which identifiers are used for what.
+///
+/// See [`FromEventsScope`] for recommendations on the usage.
+pub(crate) struct IntoEventsScope {
+    /// Accesses the `AttrMap` from code in
+    /// [`crate::field::FieldIteratorPart::Header`].
+    pub(crate) attrs: Ident,
+}
+
+impl IntoEventsScope {
+    /// Create a fresh scope with all necessary identifiers.
+    pub(crate) fn new() -> Self {
+        Self {
+            attrs: Ident::new("attrs", Span::call_site()),
+        }
+    }
+}
+
+pub(crate) fn mangle_member(member: &Member) -> Ident {
+    match member {
+        Member::Named(member) => quote::format_ident!("f{}", member),
+        Member::Unnamed(member) => quote::format_ident!("f_u{}", member.index),
+    }
+}

xso-proc/src/state.rs 🔗

@@ -88,6 +88,13 @@ impl State {
         self.advance_body = body;
         self
     }
+
+    /// Override the current `advance` implementation of this state.
+    ///
+    /// This is an in-place version of [`Self::with_impl`].
+    pub(crate) fn set_impl(&mut self, body: TokenStream) {
+        self.advance_body = body;
+    }
 }
 
 /// A partial [`FromEventsStateMachine`] which only covers the builder for a

xso-proc/src/structs.rs 🔗

@@ -96,7 +96,7 @@ impl StructDef {
             .inner
             .make_from_events_statemachine(
                 &state_ty_ident,
-                &target_ty_ident.clone().into(),
+                &Path::from(target_ty_ident.clone()).into(),
                 "Struct",
             )?
             .with_augmented_init(|init| {

xso/src/from_xml_doc.md 🔗

@@ -17,15 +17,20 @@ 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*.
+The derive macros need additional information, such as XML namespaces and
+names to match. This must be specified via key-value pairs on the type or
+fields the derive macro is invoked on. These key-value pairs 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:
+`#[xml( ... )]` *meta*.
+
+### Struct meta
+
+The following keys are defined on structs:
 
 | Key | Value type | Description |
 | --- | --- | --- |
@@ -43,16 +48,20 @@ and cannot be overridden. The following will thus not compile:
 struct Foo;
 ```
 
-## Limitations
+### Field meta
 
-Supports only empty structs currently. For example, the following will not
-work:
+For fields, the *meta* consists of a nested meta inside the `#[xml(..)]` meta,
+the identifier of which controls *how* the field is mapped to XML, while the
+contents control the parameters of that mapping.
 
-```compile_fail
-# use xso::FromXml;
-#[derive(FromXml, Debug, PartialEq)]
-#[xml(namespace = "urn:example", name = "foo")]
-struct Foo {
-    some_field: String,
-}
-```
+The following mapping types are defined:
+
+| Type | Description |
+| --- | --- |
+| [`attribute`](#attribute-meta) | Map the field to an XML attribute on the struct's element |
+
+#### `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`].