From bf7816d321c65b403180afffc4f3e702a95f91a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sat, 22 Jun 2024 13:55:54 +0200 Subject: [PATCH] xso-proc: refactor struct processing 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. --- xso-proc/src/lib.rs | 168 +++++-------------------------- xso-proc/src/structs.rs | 218 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+), 143 deletions(-) create mode 100644 xso-proc/src/structs.rs diff --git a/xso-proc/src/lib.rs b/xso-proc/src/lib.rs index ac7a48d..e506858 100644 --- a/xso-proc/src/lib.rs +++ b/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 { - 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 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 name_ident = Ident::new("name", Span::call_site()); + let attrs_ident = Ident::new("attrs", Span::call_site()); - 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.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, ::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 { - 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 { - 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 { - 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 { - ::core::result::Result::Ok(#into_events_iter_ty_name(::core::option::Option::Some(#state_ty_name::Header))) + #into_event_iter_body } } }; diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs new file mode 100644 index 0000000..28460b6 --- /dev/null +++ b/xso-proc/src/structs.rs @@ -0,0 +1,218 @@ +// Copyright (c) 2024 Jonas Schäfer +// +// 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 { + 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 { + 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, ::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 { + 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 { + 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(), + }) + } +}