xso-proc: add support for child elements

Jonas Schรคfer created

Change summary

parsers/src/util/macro_tests.rs |  30 +++++
xso-proc/ChangeLog              |   5 
xso-proc/src/compound.rs        |  99 +++++++++++++++
xso-proc/src/error_message.rs   |   8 +
xso-proc/src/field.rs           | 115 ++++++++++++++++++
xso-proc/src/meta.rs            |  10 +
xso-proc/src/scope.rs           |  12 ++
xso-proc/src/state.rs           |  60 +++++++++-
xso-proc/src/types.rs           | 210 +++++++++++++++++++++++++++++++++++
xso/ChangeLog                   |   2 
xso/src/from_xml_doc.md         |  38 ++++++
11 files changed, 576 insertions(+), 13 deletions(-)

Detailed changes

parsers/src/util/macro_tests.rs ๐Ÿ”—

@@ -491,3 +491,33 @@ fn text_with_codec_roundtrip_non_empty() {
     };
     roundtrip_full::<TextWithCodec>("<text xmlns='urn:example:ns1'>hello</text>");
 }
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct Parent {
+    #[xml(child)]
+    child: RequiredAttribute,
+}
+
+#[test]
+fn parent_roundtrip() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<Parent>("<parent xmlns='urn:example:ns1'><attr foo='hello world!'/></parent>")
+}
+
+#[test]
+fn parent_positive() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    let v =
+        parse_str::<Parent>("<parent xmlns='urn:example:ns1'><attr foo='hello world!'/></parent>")
+            .unwrap();
+    assert_eq!(v.child.foo, "hello world!");
+}

xso-proc/ChangeLog ๐Ÿ”—

@@ -1,3 +1,8 @@
+Version NEXT:
+0000-00-00 Jonas Schรคfer <jonas@zombofant.net>
+    * Please see the `xso` crate for the changelog of `xso-proc`.
+      For discoverability, the changes to the derive macros are listed there.
+
 Version 0.1.0:
 2024-07-25 Jonas Schรคfer <jonas@zombofant.net>
     * Initial release of this crate

xso-proc/src/compound.rs ๐Ÿ”—

@@ -14,7 +14,7 @@ use crate::error_message::ParentRef;
 use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit};
 use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
 use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State};
-use crate::types::{namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty};
+use crate::types::{feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty};
 
 /// A struct or enum variant's contents.
 pub(crate) struct Compound {
@@ -78,6 +78,8 @@ impl Compound {
             ref attrs,
             ref builder_data_ident,
             ref text,
+            ref substate_data,
+            ref substate_result,
             ..
         } = scope;
 
@@ -92,12 +94,14 @@ impl Compound {
         let mut builder_data_def = TokenStream::default();
         let mut builder_data_init = TokenStream::default();
         let mut output_cons = TokenStream::default();
+        let mut child_matchers = TokenStream::default();
         let mut text_handler = None;
 
-        for field in self.fields.iter() {
+        for (i, field) in self.fields.iter().enumerate() {
             let member = field.member();
             let builder_field_name = mangle_member(member);
             let part = field.make_builder_part(&scope, output_name)?;
+            let state_name = quote::format_ident!("{}Field{}", state_prefix, i);
 
             match part {
                 FieldBuilderPart::Init {
@@ -143,6 +147,65 @@ impl Compound {
                         #member: #finalize,
                     });
                 }
+
+                FieldBuilderPart::Nested {
+                    value: FieldTempInit { ty, init },
+                    matcher,
+                    builder,
+                    collect,
+                    finalize,
+                } => {
+                    let feed = feed_fn(builder.clone());
+
+                    states.push(State::new_with_builder(
+                        state_name.clone(),
+                        &builder_data_ident,
+                        &builder_data_ty,
+                    ).with_field(
+                        substate_data,
+                        &builder,
+                    ).with_mut(substate_data).with_impl(quote! {
+                        match #feed(&mut #substate_data, ev)? {
+                            ::std::option::Option::Some(#substate_result) => {
+                                #collect
+                                ::std::result::Result::Ok(::std::ops::ControlFlow::Break(Self::#default_state_ident {
+                                    #builder_data_ident,
+                                }))
+                            }
+                            ::std::option::Option::None => {
+                                ::std::result::Result::Ok(::std::ops::ControlFlow::Break(Self::#state_name {
+                                    #builder_data_ident,
+                                    #substate_data,
+                                }))
+                            }
+                        }
+                    }));
+
+                    builder_data_def.extend(quote! {
+                        #builder_field_name: #ty,
+                    });
+
+                    builder_data_init.extend(quote! {
+                        #builder_field_name: #init,
+                    });
+
+                    child_matchers.extend(quote! {
+                        let (name, attrs) = match #matcher {
+                            ::std::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs }) => (name, attrs),
+                            ::std::result::Result::Err(::xso::error::FromEventsError::Invalid(e)) => return ::std::result::Result::Err(e),
+                            ::std::result::Result::Ok(#substate_data) => {
+                                return ::std::result::Result::Ok(::std::ops::ControlFlow::Break(Self::#state_name {
+                                    #builder_data_ident,
+                                    #substate_data,
+                                }))
+                            }
+                        };
+                    });
+
+                    output_cons.extend(quote! {
+                        #member: #finalize,
+                    });
+                }
             }
         }
 
@@ -184,7 +247,9 @@ impl Compound {
                         #output_cons
                     ))
                 }
-                ::xso::exports::rxml::Event::StartElement(..) => {
+                ::xso::exports::rxml::Event::StartElement(_, name, attrs) => {
+                    #child_matchers
+                    let _ = (name, attrs);
                     ::core::result::Result::Err(::xso::error::Error::Other(#unknown_child_err))
                 }
                 ::xso::exports::rxml::Event::Text(_, #text) => {
@@ -270,7 +335,7 @@ impl Compound {
         for (i, field) in self.fields.iter().enumerate() {
             let member = field.member();
             let bound_name = mangle_member(member);
-            let part = field.make_iterator_part(&bound_name)?;
+            let part = field.make_iterator_part(&scope, &bound_name)?;
             let state_name = quote::format_ident!("{}Field{}", state_prefix, i);
             let ty = scope.borrow(field.ty().clone());
 
@@ -321,6 +386,32 @@ impl Compound {
                         #bound_name,
                     });
                 }
+
+                FieldIteratorPart::Content {
+                    value: FieldTempInit { ty, init },
+                    generator,
+                } => {
+                    // we have to make sure that we carry our data around in
+                    // all the previous states.
+                    for state in states.iter_mut() {
+                        state.add_field(&bound_name, &ty);
+                    }
+
+                    states.push(
+                        State::new(state_name.clone())
+                            .with_field(&bound_name, &ty)
+                            .with_mut(&bound_name)
+                            .with_impl(quote! {
+                                #generator?
+                            }),
+                    );
+                    destructure.extend(quote! {
+                        #member: #bound_name,
+                    });
+                    start_init.extend(quote! {
+                        #bound_name: #init,
+                    });
+                }
             }
         }
 

xso-proc/src/error_message.rs ๐Ÿ”—

@@ -79,3 +79,11 @@ pub(super) fn on_missing_attribute(parent_name: &ParentRef, field: &Member) -> S
         parent_name
     )
 }
+
+/// Create a string error message for a missing child element.
+///
+/// `parent_name` should point at the compound which is being parsed and
+/// `field` should be the field to which the child belongs.
+pub(super) fn on_missing_child(parent_name: &ParentRef, field: &Member) -> String {
+    format!("Missing child {} in {}.", FieldName(&field), parent_name)
+}

xso-proc/src/field.rs ๐Ÿ”—

@@ -14,9 +14,10 @@ use rxml_validation::NcName;
 
 use crate::error_message::{self, ParentRef};
 use crate::meta::{Flag, NameRef, NamespaceRef, XmlFieldMeta};
-use crate::scope::FromEventsScope;
+use crate::scope::{AsItemsScope, FromEventsScope};
 use crate::types::{
-    as_optional_xml_text_fn, as_xml_text_fn, default_fn, from_xml_text_fn, string_ty,
+    as_optional_xml_text_fn, as_xml_iter_fn, as_xml_text_fn, default_fn, from_events_fn,
+    from_xml_builder_ty, from_xml_text_fn, item_iter_ty, option_ty, string_ty,
     text_codec_decode_fn, text_codec_encode_fn,
 };
 
@@ -58,6 +59,38 @@ pub(crate) enum FieldBuilderPart {
         /// temporary value.
         finalize: TokenStream,
     },
+
+    /// Parse a field from child element events.
+    Nested {
+        /// Expression and type which initializes a buffer to use during
+        /// parsing.
+        value: FieldTempInit,
+
+        /// Expression which evaluates to `Result<T, FromEventsError>`,
+        /// consuming `name: rxml::QName` and `attrs: rxml::AttrMap`.
+        ///
+        /// `T` must be the type specified in the
+        /// [`Self::Nested::builder`]  field.
+        matcher: TokenStream,
+
+        /// Type implementing `xso::FromEventsBuilder` which parses the child
+        /// element.
+        ///
+        /// This type is returned by the expressions in
+        /// [`matcher`][`Self::Nested::matcher`].
+        builder: Type,
+
+        /// Expression which consumes the value stored in the identifier
+        /// [`crate::common::FromEventsScope::substate_result`][`FromEventsScope::substate_result`]
+        /// and somehow collects it into the field declared with
+        /// [`value`][`Self::Nested::value`].
+        collect: TokenStream,
+
+        /// Expression which consumes the data from the field declared with
+        /// [`value`][`Self::Nested::value`] and converts it into the field's
+        /// type.
+        finalize: TokenStream,
+    },
 }
 
 /// Describe how a struct or enum variant's member is converted to XML data.
@@ -80,6 +113,21 @@ pub(crate) enum FieldIteratorPart {
         /// String, which is then emitted as text data.
         generator: TokenStream,
     },
+
+    /// The field is emitted as series of items which form a child element.
+    Content {
+        /// Expression and type which initializes the nested iterator.
+        ///
+        /// Note that this is evaluated at construction time of the iterator.
+        /// Fields of this variant do not get access to their original data,
+        /// unless they carry it in the contents of this `value`.
+        value: FieldTempInit,
+
+        /// An expression which uses the value (mutably) and evaluates to
+        /// a Result<Option<Item>, Error>. Once the state returns None, the
+        /// processing will advance to the next state.
+        generator: TokenStream,
+    },
 }
 
 /// Specify how the field is mapped to XML.
@@ -102,6 +150,9 @@ enum FieldKind {
         /// Optional codec to use
         codec: Option<Type>,
     },
+
+    /// The field maps to a child
+    Child,
 }
 
 impl FieldKind {
@@ -147,6 +198,8 @@ impl FieldKind {
             }
 
             XmlFieldMeta::Text { codec } => Ok(Self::Text { codec }),
+
+            XmlFieldMeta::Child => Ok(Self::Child),
         }
     }
 }
@@ -287,6 +340,39 @@ impl FieldDef {
                     finalize,
                 })
             }
+
+            FieldKind::Child => {
+                let FromEventsScope {
+                    ref substate_result,
+                    ..
+                } = scope;
+                let field_access = scope.access_field(&self.member);
+
+                let missing_msg = error_message::on_missing_child(container_name, &self.member);
+
+                let from_events = from_events_fn(self.ty.clone());
+                let from_xml_builder = from_xml_builder_ty(self.ty.clone());
+
+                Ok(FieldBuilderPart::Nested {
+                    value: FieldTempInit {
+                        init: quote! { ::std::option::Option::None },
+                        ty: option_ty(self.ty.clone()),
+                    },
+                    matcher: quote! {
+                        #from_events(name, attrs)
+                    },
+                    builder: from_xml_builder,
+                    collect: quote! {
+                        #field_access = ::std::option::Option::Some(#substate_result);
+                    },
+                    finalize: quote! {
+                        match #field_access {
+                            ::std::option::Option::Some(value) => value,
+                            ::std::option::Option::None => return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into()),
+                        }
+                    },
+                })
+            }
         }
     }
 
@@ -294,7 +380,11 @@ impl FieldDef {
     ///
     /// `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, bound_name: &Ident) -> Result<FieldIteratorPart> {
+    pub(crate) fn make_iterator_part(
+        &self,
+        scope: &AsItemsScope,
+        bound_name: &Ident,
+    ) -> Result<FieldIteratorPart> {
         match self.kind {
             FieldKind::Attribute {
                 ref xml_name,
@@ -335,6 +425,25 @@ impl FieldDef {
 
                 Ok(FieldIteratorPart::Text { generator })
             }
+
+            FieldKind::Child => {
+                let AsItemsScope { ref lifetime, .. } = scope;
+
+                let as_xml_iter = as_xml_iter_fn(self.ty.clone());
+                let item_iter = item_iter_ty(self.ty.clone(), lifetime.clone());
+
+                Ok(FieldIteratorPart::Content {
+                    value: FieldTempInit {
+                        init: quote! {
+                            #as_xml_iter(#bound_name)?
+                        },
+                        ty: item_iter,
+                    },
+                    generator: quote! {
+                        #bound_name.next().transpose()
+                    },
+                })
+            }
         }
     }
 

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

@@ -316,6 +316,9 @@ pub(crate) enum XmlFieldMeta {
         /// The path to the optional codec type.
         codec: Option<Type>,
     },
+
+    /// `#[xml(child)`
+    Child,
 }
 
 impl XmlFieldMeta {
@@ -420,6 +423,11 @@ impl XmlFieldMeta {
         }
     }
 
+    /// Parse a `#[xml(child)]` meta.
+    fn child_from_meta(_: ParseNestedMeta<'_>) -> Result<Self> {
+        Ok(Self::Child)
+    }
+
     /// Parse [`Self`] from a nestd meta, switching on the identifier
     /// of that nested meta.
     fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
@@ -427,6 +435,8 @@ impl XmlFieldMeta {
             Self::attribute_from_meta(meta)
         } else if meta.path.is_ident("text") {
             Self::text_from_meta(meta)
+        } else if meta.path.is_ident("child") {
+            Self::child_from_meta(meta)
         } else {
             Err(Error::new_spanned(meta.path, "unsupported field meta"))
         }

xso-proc/src/scope.rs ๐Ÿ”—

@@ -42,6 +42,16 @@ pub(crate) struct FromEventsScope {
     /// the time, using [`Self::access_field`] is the correct way to access
     /// the builder data.
     pub(crate) builder_data_ident: Ident,
+
+    /// Accesses the result produced by a nested state's builder type.
+    ///
+    /// See [`crate::field::FieldBuilderPart::Nested`].
+    pub(crate) substate_data: Ident,
+
+    /// Accesses the result produced by a nested state's builder type.
+    ///
+    /// See [`crate::field::FieldBuilderPart::Nested`].
+    pub(crate) substate_result: Ident,
 }
 
 impl FromEventsScope {
@@ -53,6 +63,8 @@ impl FromEventsScope {
             attrs: Ident::new("attrs", Span::call_site()),
             text: Ident::new("__xso_proc_macro_text_data", Span::call_site()),
             builder_data_ident: Ident::new("__xso_proc_macro_builder_data", Span::call_site()),
+            substate_data: Ident::new("__xso_proc_macro_substate_data", Span::call_site()),
+            substate_result: Ident::new("__xso_proc_macro_substate_result", Span::call_site()),
         }
     }
 

xso-proc/src/state.rs ๐Ÿ”—

@@ -23,6 +23,9 @@ pub(crate) struct State {
 
     /// Right-hand-side of the match arm for this state.
     advance_body: TokenStream,
+
+    /// If set, that identifier will be bound mutably.
+    uses_mut: Option<Ident>,
 }
 
 impl State {
@@ -54,6 +57,7 @@ impl State {
             decl: TokenStream::default(),
             destructure: TokenStream::default(),
             advance_body: TokenStream::default(),
+            uses_mut: None,
         }
     }
 
@@ -95,6 +99,14 @@ impl State {
     pub(crate) fn set_impl(&mut self, body: TokenStream) {
         self.advance_body = body;
     }
+
+    /// Modify the state to mark the given field as mutable and return the
+    /// modified state.
+    pub(crate) fn with_mut(mut self, ident: &Ident) -> Self {
+        assert!(self.uses_mut.is_none());
+        self.uses_mut = Some(ident.clone());
+        self
+    }
 }
 
 /// A partial [`FromEventsStateMachine`] which only covers the builder for a
@@ -132,18 +144,28 @@ impl FromEventsSubmachine {
                 decl,
                 destructure,
                 advance_body,
+                uses_mut,
             } = state;
 
             state_defs.extend(quote! {
                 #name { #decl },
             });
 
+            let binding = if let Some(uses_mut) = uses_mut.as_ref() {
+                quote! {
+                    let mut #uses_mut = #uses_mut;
+                }
+            } else {
+                TokenStream::default()
+            };
+
             // XXX: nasty hack, but works: the first member of the enum always
             // exists and it always is the builder data, which we always need
             // mutably available. So we can just prefix the destructuring
             // token stream with `mut` to make that first member mutable.
             advance_match_arms.extend(quote! {
                 Self::#name { mut #destructure } => {
+                    #binding
                     #advance_body
                 }
             });
@@ -218,6 +240,7 @@ impl AsItemsSubmachine {
                 ref decl,
                 ref destructure,
                 ref advance_body,
+                ref uses_mut,
             } = state;
 
             let footer = match self.states.get(i + 1) {
@@ -242,12 +265,37 @@ impl AsItemsSubmachine {
                 #name { #decl },
             });
 
-            advance_match_arms.extend(quote! {
-                Self::#name { #destructure } => {
-                    let item = #advance_body;
-                    #footer
-                }
-            });
+            if let Some(uses_mut) = uses_mut.as_ref() {
+                // the variant is non-consuming, meaning it can be called
+                // multiple times and it uses the identifier in `uses_mut`
+                // mutably.
+                // the transition is only triggered when it emits a None
+                // item
+                // (we cannot do this at the place the `State` is constructed,
+                // because we don't yet know all its fields then; it must be
+                // done here.)
+                advance_match_arms.extend(quote! {
+                    Self::#name { #destructure } => {
+                        let mut #uses_mut = #uses_mut;
+                        match #advance_body {
+                            ::std::option::Option::Some(item) => {
+                                ::std::result::Result::Ok((::std::option::Option::Some(Self::#name { #destructure }), ::std::option::Option::Some(item)))
+                            },
+                            item => { #footer },
+                        }
+                    }
+                });
+            } else {
+                // if the variant is consuming, it can only be called once.
+                // it may or may not emit an event, but the transition is
+                // always triggered
+                advance_match_arms.extend(quote! {
+                    Self::#name { #destructure } => {
+                        let item = #advance_body;
+                        #footer
+                    }
+                });
+            }
         }
 
         AsItemsStateMachine {

xso-proc/src/types.rs ๐Ÿ”—

@@ -422,3 +422,213 @@ pub(crate) fn phantom_lifetime_ty(lifetime: Lifetime) -> Type {
         },
     })
 }
+
+/// Construct a [`syn::TypePath`] referring to
+/// `<#of_ty as ::xso::FromXml>`.
+fn from_xml_of(of_ty: Type) -> (Span, TypePath) {
+    let span = of_ty.span();
+    (
+        span,
+        TypePath {
+            qself: Some(QSelf {
+                lt_token: syn::token::Lt { spans: [span] },
+                ty: Box::new(of_ty),
+                position: 2,
+                as_token: Some(syn::token::As { span }),
+                gt_token: syn::token::Gt { spans: [span] },
+            }),
+            path: Path {
+                leading_colon: Some(syn::token::PathSep {
+                    spans: [span, span],
+                }),
+                segments: [
+                    PathSegment {
+                        ident: Ident::new("xso", span),
+                        arguments: PathArguments::None,
+                    },
+                    PathSegment {
+                        ident: Ident::new("FromXml", span),
+                        arguments: PathArguments::None,
+                    },
+                ]
+                .into_iter()
+                .collect(),
+            },
+        },
+    )
+}
+
+/// Construct a [`syn::Type`] referring to
+/// `<#of_ty as ::xso::FromXml>::Builder`.
+pub(crate) fn from_xml_builder_ty(of_ty: Type) -> Type {
+    let (span, mut ty) = from_xml_of(of_ty);
+    ty.path.segments.push(PathSegment {
+        ident: Ident::new("Builder", span),
+        arguments: PathArguments::None,
+    });
+    Type::Path(ty)
+}
+
+/// Construct a [`syn::Expr`] referring to
+/// `<#of_ty as ::xso::FromXml>::from_events`.
+pub(crate) fn from_events_fn(of_ty: Type) -> Expr {
+    let (span, mut ty) = from_xml_of(of_ty);
+    ty.path.segments.push(PathSegment {
+        ident: Ident::new("from_events", span),
+        arguments: PathArguments::None,
+    });
+    Expr::Path(ExprPath {
+        attrs: Vec::new(),
+        qself: ty.qself,
+        path: ty.path,
+    })
+}
+
+/// Construct a [`syn::Type`] which wraps the given `ty` in
+/// `::std::option::Option<_>`.
+pub(crate) fn option_ty(ty: Type) -> Type {
+    let span = ty.span();
+    Type::Path(TypePath {
+        qself: None,
+        path: Path {
+            leading_colon: Some(syn::token::PathSep {
+                spans: [span, span],
+            }),
+            segments: [
+                PathSegment {
+                    ident: Ident::new("std", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("option", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("Option", span),
+                    arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments {
+                        colon2_token: None,
+                        lt_token: syn::token::Lt { spans: [span] },
+                        args: [GenericArgument::Type(ty)].into_iter().collect(),
+                        gt_token: syn::token::Gt { spans: [span] },
+                    }),
+                },
+            ]
+            .into_iter()
+            .collect(),
+        },
+    })
+}
+
+/// Construct a [`syn::TypePath`] referring to
+/// `<#of_ty as ::xso::FromEventsBuilder>`.
+fn from_events_builder_of(of_ty: Type) -> (Span, TypePath) {
+    let span = of_ty.span();
+    (
+        span,
+        TypePath {
+            qself: Some(QSelf {
+                lt_token: syn::token::Lt { spans: [span] },
+                ty: Box::new(of_ty),
+                position: 2,
+                as_token: Some(syn::token::As { span }),
+                gt_token: syn::token::Gt { spans: [span] },
+            }),
+            path: Path {
+                leading_colon: Some(syn::token::PathSep {
+                    spans: [span, span],
+                }),
+                segments: [
+                    PathSegment {
+                        ident: Ident::new("xso", span),
+                        arguments: PathArguments::None,
+                    },
+                    PathSegment {
+                        ident: Ident::new("FromEventsBuilder", span),
+                        arguments: PathArguments::None,
+                    },
+                ]
+                .into_iter()
+                .collect(),
+            },
+        },
+    )
+}
+
+/// Construct a [`syn::Expr`] referring to
+/// `<#of_ty as ::xso::FromEventsBuilder>::feed`.
+pub(crate) fn feed_fn(of_ty: Type) -> Expr {
+    let (span, mut ty) = from_events_builder_of(of_ty);
+    ty.path.segments.push(PathSegment {
+        ident: Ident::new("feed", span),
+        arguments: PathArguments::None,
+    });
+    Expr::Path(ExprPath {
+        attrs: Vec::new(),
+        qself: ty.qself,
+        path: ty.path,
+    })
+}
+
+fn as_xml_of(of_ty: Type) -> (Span, TypePath) {
+    let span = of_ty.span();
+    (
+        span,
+        TypePath {
+            qself: Some(QSelf {
+                lt_token: syn::token::Lt { spans: [span] },
+                ty: Box::new(of_ty),
+                position: 2,
+                as_token: Some(syn::token::As { span }),
+                gt_token: syn::token::Gt { spans: [span] },
+            }),
+            path: Path {
+                leading_colon: Some(syn::token::PathSep {
+                    spans: [span, span],
+                }),
+                segments: [
+                    PathSegment {
+                        ident: Ident::new("xso", span),
+                        arguments: PathArguments::None,
+                    },
+                    PathSegment {
+                        ident: Ident::new("AsXml", span),
+                        arguments: PathArguments::None,
+                    },
+                ]
+                .into_iter()
+                .collect(),
+            },
+        },
+    )
+}
+
+/// Construct a [`syn::Expr`] referring to
+/// `<#of_ty as ::xso::AsXml>::as_xml_iter`.
+pub(crate) fn as_xml_iter_fn(of_ty: Type) -> Expr {
+    let (span, mut ty) = as_xml_of(of_ty);
+    ty.path.segments.push(PathSegment {
+        ident: Ident::new("as_xml_iter", span),
+        arguments: PathArguments::None,
+    });
+    Expr::Path(ExprPath {
+        attrs: Vec::new(),
+        qself: ty.qself,
+        path: ty.path,
+    })
+}
+
+/// Construct a [`syn::Type`] referring to
+/// `<#of_ty as ::xso::AsXml>::ItemIter`.
+pub(crate) fn item_iter_ty(of_ty: Type, lifetime: Lifetime) -> Type {
+    let (span, mut ty) = as_xml_of(of_ty);
+    ty.path.segments.push(PathSegment {
+        ident: Ident::new("ItemIter", span),
+        arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments {
+            colon2_token: None,
+            lt_token: token::Lt { spans: [span] },
+            args: [GenericArgument::Lifetime(lifetime)].into_iter().collect(),
+            gt_token: token::Gt { spans: [span] },
+        }),
+    });
+    Type::Path(ty)
+}

xso/ChangeLog ๐Ÿ”—

@@ -13,6 +13,8 @@ Version NEXT:
 
         All this is to avoid triggering the camel case lint on the types we
         generate.
+    * Added
+      - Support for child elements in derive macros.
 
 Version 0.1.2:
 2024-07-26 Jonas Schรคfer <jonas@zombofant.net>

xso/src/from_xml_doc.md ๐Ÿ”—

@@ -69,6 +69,7 @@ The following mapping types are defined:
 | Type | Description |
 | --- | --- |
 | [`attribute`](#attribute-meta) | Map the field to an XML attribute on the struct's element |
+| [`child`](#child-meta) | Map the field to a child element |
 | [`text`](#text-meta) | Map the field to the text content of the struct's element |
 
 #### `attribute` meta
@@ -135,6 +136,43 @@ assert_eq!(foo, Foo {
 });
 ```
 
+#### `child` meta
+
+The `child` meta causes the field to be mapped to a child element of the
+element. It supports no options. The field's type must implement [`FromXml`]
+in order to derive `FromXml` and [`AsXml`] in order to derive `AsXml`.
+
+##### Example
+
+```rust
+# use xso::FromXml;
+#[derive(FromXml, Debug, PartialEq)]
+#[xml(namespace = "urn:example", name = "child")]
+struct Child {
+    #[xml(attribute = "some-attr")]
+    some_attr: String,
+}
+
+#[derive(FromXml, Debug, PartialEq)]
+#[xml(namespace = "urn:example", name = "parent")]
+struct Parent {
+    #[xml(attribute)]
+    foo: String,
+
+    #[xml(child)]
+    bar: Child,
+}
+
+let parent: Parent = xso::from_bytes(b"<parent
+    xmlns='urn:example'
+    foo='hello world!'
+><child some-attr='within'/></parent>").unwrap();
+assert_eq!(parent, Parent {
+    foo: "hello world!".to_owned(),
+    bar: Child { some_attr: "within".to_owned() },
+});
+```
+
 #### `text` meta
 
 The `text` meta causes the field to be mapped to the text content of the