xso-proc: refactor struct processing

Jonas Schäfer created

If we are going to support structs with fields, it would be good to have
that struct-related code organised a little and less splashed over the
main lib.rs file.

Change summary

xso-proc/src/lib.rs     | 168 ++++----------------------------
xso-proc/src/structs.rs | 218 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 243 insertions(+), 143 deletions(-)

Detailed changes

xso-proc/src/lib.rs 🔗

@@ -21,30 +21,23 @@ return to `xso` for more information**. The documentation of
 // 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 proc_macro2::{Span, TokenStream};
 use quote::quote;
 use syn::*;
 
 mod meta;
+mod structs;
 
 /// 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)> {
+fn parse_struct(item: Item) -> Result<(Visibility, Ident, structs::StructDef)> {
     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))
+            let def = structs::StructDef::new(&item.ident, meta, &item.fields)?;
+            Ok((item.vis, item.ident, def))
         }
         other => Err(Error::new_spanned(other, "cannot derive on this item")),
     }
@@ -53,90 +46,29 @@ fn parse_struct(item: Item) -> Result<(Visibility, meta::XmlCompoundMeta, Ident)
 /// 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 (vis, ident, def) = parse_struct(input)?;
 
-    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 name_ident = Ident::new("name", Span::call_site());
+    let attrs_ident = Ident::new("attrs", Span::call_site());
 
-    let unknown_attr_err = format!(
-        "Unknown attribute in {} element.",
-        xml_name.repr_to_string()
-    );
-    let unknown_child_err = format!("Unknown child in {} element.", xml_name.repr_to_string());
-    let docstr = format!("Build a [`{}`] from XML events", ident);
+    let structs::FromXmlParts {
+        defs,
+        from_events_body,
+        builder_ty_ident,
+    } = def.make_from_events_builder(&vis, &name_ident, &attrs_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)
-                    }
-                }
-            }
-        }
+        #defs
 
         impl ::xso::FromXml for #ident {
-            type Builder = #from_events_builder_ty_name;
+            type Builder = #builder_ty_ident;
 
             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)))
+                #from_events_body
             }
         }
     };
@@ -172,73 +104,23 @@ pub fn from_xml(input: RawTokenStream) -> RawTokenStream {
 /// 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)?;
+    let (vis, ident, def) = 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);
+    let structs::IntoXmlParts {
+        defs,
+        into_event_iter_body,
+        event_iter_ty_ident,
+    } = def.make_into_event_iter(&vis)?;
 
     #[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),
-                                #xml_name.to_owned(),
-                            ),
-                            ::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,
-                }
-            }
-        }
+        #defs
 
         impl ::xso::IntoXml for #ident {
-            type EventIter = #into_events_iter_ty_name;
+            type EventIter = #event_iter_ty_ident;
 
             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)))
+                #into_event_iter_body
             }
         }
     };

xso-proc/src/structs.rs 🔗

@@ -0,0 +1,218 @@
+// 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/.
+
+//! Handling of structs
+
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::*;
+
+use crate::meta::{NameRef, NamespaceRef, XmlCompoundMeta};
+
+/// Parts necessary to construct a `::xso::FromXml` implementation.
+pub(crate) struct FromXmlParts {
+    /// Additional items necessary for the implementation.
+    pub(crate) defs: TokenStream,
+
+    /// The body of the `::xso::FromXml::from_xml` function.
+    pub(crate) from_events_body: TokenStream,
+
+    /// The name of the type which is the `::xso::FromXml::Builder`.
+    pub(crate) builder_ty_ident: Ident,
+}
+
+/// Parts necessary to construct a `::xso::IntoXml` implementation.
+pub(crate) struct IntoXmlParts {
+    /// Additional items necessary for the implementation.
+    pub(crate) defs: TokenStream,
+
+    /// The body of the `::xso::IntoXml::into_event_iter` function.
+    pub(crate) into_event_iter_body: TokenStream,
+
+    /// The name of the type which is the `::xso::IntoXml::EventIter`.
+    pub(crate) event_iter_ty_ident: Ident,
+}
+
+/// Definition of a struct and how to parse it.
+pub(crate) struct StructDef {
+    /// The XML namespace of the element to map the struct to.
+    namespace: NamespaceRef,
+
+    /// The XML name of the element to map the struct to.
+    name: NameRef,
+
+    /// Name of the target type.
+    target_ty_ident: Ident,
+
+    /// Name of the builder type.
+    builder_ty_ident: Ident,
+
+    /// Name of the iterator type.
+    event_iter_ty_ident: Ident,
+}
+
+impl StructDef {
+    /// Create a new struct from its name, meta, and fields.
+    pub(crate) fn new(ident: &Ident, meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
+        let Some(namespace) = meta.namespace else {
+            return Err(Error::new(meta.span, "`namespace` is required on structs"));
+        };
+
+        let Some(name) = meta.name else {
+            return Err(Error::new(meta.span, "`name` is required on structs"));
+        };
+
+        match fields {
+            Fields::Unit => (),
+            other => {
+                return Err(Error::new_spanned(
+                    other,
+                    "cannot derive on non-unit struct (yet!)",
+                ))
+            }
+        }
+
+        Ok(Self {
+            namespace,
+            name,
+            target_ty_ident: ident.clone(),
+            builder_ty_ident: quote::format_ident!("{}FromXmlBuilder", ident),
+            event_iter_ty_ident: quote::format_ident!("{}IntoXmlIterator", ident),
+        })
+    }
+
+    pub(crate) fn make_from_events_builder(
+        &self,
+        vis: &Visibility,
+        name_ident: &Ident,
+        attrs_ident: &Ident,
+    ) -> Result<FromXmlParts> {
+        let xml_namespace = &self.namespace;
+        let xml_name = &self.name;
+
+        let target_ty_ident = &self.target_ty_ident;
+        let builder_ty_ident = &self.builder_ty_ident;
+        let state_ty_name = quote::format_ident!("{}State", builder_ty_ident);
+
+        let unknown_attr_err = format!(
+            "Unknown attribute in {} element.",
+            xml_name.repr_to_string()
+        );
+        let unknown_child_err = format!("Unknown child in {} element.", xml_name.repr_to_string());
+
+        let docstr = format!("Build a [`{}`] from XML events", target_ty_ident);
+
+        Ok(FromXmlParts {
+            defs: quote! {
+                enum #state_ty_name {
+                    Default,
+                }
+
+                #[doc = #docstr]
+                #vis struct #builder_ty_ident(::core::option::Option<#state_ty_name>);
+
+                impl ::xso::FromEventsBuilder for #builder_ty_ident {
+                    type Output = #target_ty_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(#target_ty_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)
+                            }
+                        }
+                    }
+                }
+            },
+            from_events_body: quote! {
+                if #name_ident.0 != #xml_namespace || #name_ident.1 != #xml_name {
+                    return ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch {
+                        name: #name_ident,
+                        attrs: #attrs_ident,
+                    });
+                }
+                if attrs.len() > 0 {
+                    return ::core::result::Result::Err(::xso::error::Error::Other(
+                        #unknown_attr_err,
+                    ).into());
+                }
+                ::core::result::Result::Ok(#builder_ty_ident(::core::option::Option::Some(#state_ty_name::Default)))
+            },
+            builder_ty_ident: builder_ty_ident.clone(),
+        })
+    }
+
+    pub(crate) fn make_into_event_iter(&self, vis: &Visibility) -> Result<IntoXmlParts> {
+        let xml_namespace = &self.namespace;
+        let xml_name = &self.name;
+
+        let target_ty_ident = &self.target_ty_ident;
+        let event_iter_ty_ident = &self.event_iter_ty_ident;
+        let state_ty_name = quote::format_ident!("{}State", event_iter_ty_ident);
+
+        let docstr = format!("Decompose a [`{}`] into XML events", target_ty_ident);
+
+        Ok(IntoXmlParts {
+            defs: quote! {
+                enum #state_ty_name {
+                    Header,
+                    Footer,
+                }
+
+                #[doc = #docstr]
+                #vis struct #event_iter_ty_ident(::core::option::Option<#state_ty_name>);
+
+                impl ::std::iter::Iterator for #event_iter_ty_ident {
+                    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),
+                                        #xml_name.to_owned(),
+                                    ),
+                                    ::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,
+                        }
+                    }
+                }
+            },
+            into_event_iter_body: quote! {
+                ::core::result::Result::Ok(#event_iter_ty_ident(::core::option::Option::Some(#state_ty_name::Header)))
+            },
+            event_iter_ty_ident: event_iter_ty_ident.clone(),
+        })
+    }
+}