diff --git a/parsers/src/attention.rs b/parsers/src/attention.rs index 0f2de0e..6693377 100644 --- a/parsers/src/attention.rs +++ b/parsers/src/attention.rs @@ -44,7 +44,7 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Unknown child in attention element."); + assert_eq!(message, "Unknown child in Attention element."); } #[cfg(not(feature = "disable-validation"))] @@ -58,7 +58,7 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Unknown attribute in attention element."); + assert_eq!(message, "Unknown attribute in Attention element."); } #[test] diff --git a/parsers/src/blocking.rs b/parsers/src/blocking.rs index f0391b0..5f0aa9c 100644 --- a/parsers/src/blocking.rs +++ b/parsers/src/blocking.rs @@ -168,7 +168,7 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Unknown attribute in blocklist element."); + assert_eq!(message, "Unknown attribute in BlocklistRequest element."); let result_elem = elem.clone(); let error = BlocklistResult::try_from(result_elem).unwrap_err(); @@ -208,6 +208,6 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Unknown child in blocklist element."); + assert_eq!(message, "Unknown child in BlocklistRequest element."); } } diff --git a/parsers/src/ping.rs b/parsers/src/ping.rs index 61c7ab8..d71514d 100644 --- a/parsers/src/ping.rs +++ b/parsers/src/ping.rs @@ -54,7 +54,7 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Unknown child in ping element."); + assert_eq!(message, "Unknown child in Ping element."); } #[cfg(not(feature = "disable-validation"))] @@ -66,6 +66,6 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Unknown attribute in ping element."); + assert_eq!(message, "Unknown attribute in Ping element."); } } diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 51e99b0..2ea1b8d 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -115,7 +115,7 @@ fn empty_unexpected_attribute() { }; match parse_str::("") { Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => { - assert_eq!(e, "Unknown attribute in foo element."); + assert_eq!(e, "Unknown attribute in Empty element."); } other => panic!("unexpected result: {:?}", other), } @@ -130,7 +130,7 @@ fn empty_unexpected_child() { }; match parse_str::("") { Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => { - assert_eq!(e, "Unknown child in foo element."); + assert_eq!(e, "Unknown child in Empty element."); } other => panic!("unexpected result: {:?}", other), } diff --git a/xso-proc/src/compound.rs b/xso-proc/src/compound.rs new file mode 100644 index 0000000..397095f --- /dev/null +++ b/xso-proc/src/compound.rs @@ -0,0 +1,155 @@ +// 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 the insides of compound structures (structs and enum variants) + +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens}; +use syn::*; + +use crate::state::{FromEventsSubmachine, IntoEventsSubmachine, State}; +use crate::types::qname_ty; + +/// A struct or enum variant's contents. +pub(crate) struct Compound; + +impl Compound { + /// Construct a compound from fields. + pub(crate) fn from_fields(compound_fields: &Fields) -> Result { + match compound_fields { + Fields::Unit => (), + other => { + return Err(Error::new_spanned( + other, + "cannot derive on non-unit struct (yet!)", + )) + } + } + + Ok(Self) + } + + /// Make and return a set of states which is used to construct the target + /// type from XML events. + /// + /// The states are returned as partial state machine. See the return + /// type's documentation for details. + pub(crate) fn make_from_events_statemachine( + &self, + state_ty_ident: &Ident, + output_cons: &Path, + state_prefix: &str, + ) -> Result { + let default_state_ident = quote::format_ident!("{}Default", state_prefix); + let builder_data_ident = quote::format_ident!("__data"); + let builder_data_ty: Type = TypePath { + qself: None, + path: quote::format_ident!("{}Data{}", state_ty_ident, state_prefix).into(), + } + .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); + + states.push(State::new_with_builder( + default_state_ident.clone(), + &builder_data_ident, + &builder_data_ty, + ).with_impl(quote! { + match ev { + // EndElement in Default state -> done parsing. + ::xso::exports::rxml::Event::EndElement(_) => { + ::core::result::Result::Ok(::std::ops::ControlFlow::Continue( + #output_cons + )) + } + ::xso::exports::rxml::Event::StartElement(..) => { + ::core::result::Result::Err(::xso::error::Error::Other(#unknown_child_err)) + } + ::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(::std::ops::ControlFlow::Break( + Self::#default_state_ident { #builder_data_ident } + )) + } + })); + + Ok(FromEventsSubmachine { + defs: quote! { + struct #builder_data_ty; + }, + states, + init: quote! { + 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, + }) + }, + }) + } + + /// Make and return a set of states which is used to destructure the + /// target type into XML events. + /// + /// The states are returned as partial state machine. See the return + /// type's documentation for details. + /// + /// **Important:** The returned submachine is not in functional state! + /// It's `init` must be modified so that a variable called `name` of type + /// `rxml::QName` is in scope. + pub(crate) fn make_into_event_iter_statemachine( + &self, + input_name: &Path, + state_prefix: &str, + ) -> Result { + 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(); + + 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(), + )) + }), + ); + + states.push( + State::new(end_element_state_ident.clone()).with_impl(quote! { + ::core::option::Option::Some(::xso::exports::rxml::Event::EndElement( + ::xso::exports::rxml::parser::EventMetrics::zero(), + )) + }), + ); + + Ok(IntoEventsSubmachine { + defs: TokenStream::default(), + states, + destructure: quote! { + #input_name + }, + init: quote! { + Self::#start_element_state_ident { #name_ident } + }, + }) + } +} diff --git a/xso-proc/src/lib.rs b/xso-proc/src/lib.rs index e506858..557a02f 100644 --- a/xso-proc/src/lib.rs +++ b/xso-proc/src/lib.rs @@ -25,8 +25,11 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::*; +mod compound; mod meta; +mod state; mod structs; +mod types; /// Convert an [`syn::Item`] into the parts relevant for us. /// diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 8a393cc..02a70a1 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -9,8 +9,6 @@ //! This module is concerned with parsing attributes from the Rust "meta" //! annotations on structs, enums, enum variants and fields. -use std::borrow::Cow; - use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned}; use syn::{spanned::Spanned, *}; @@ -62,22 +60,6 @@ pub(crate) enum NameRef { Path(Path), } -impl NameRef { - /// Access a representation of the XML name as str. - /// - /// If this name reference is a [`Self::Path`], this will return the name - /// of the rightmost identifier in the path. - /// - /// If this name reference is a [`Self::Literal`], this will return the - /// contents of the literal. - pub(crate) fn repr_to_string(&self) -> Cow<'_, str> { - match self { - Self::Literal { ref value, .. } => Cow::Borrowed(value.as_str()), - Self::Path(ref path) => path.segments.last().unwrap().ident.to_string().into(), - } - } -} - impl syn::parse::Parse for NameRef { fn parse(input: syn::parse::ParseStream<'_>) -> Result { if input.peek(syn::LitStr) { diff --git a/xso-proc/src/state.rs b/xso-proc/src/state.rs new file mode 100644 index 0000000..62cf8e8 --- /dev/null +++ b/xso-proc/src/state.rs @@ -0,0 +1,656 @@ +// 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/. + +//! State machines for parsing and serialising of structs and enums. + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::*; + +/// A single state in a parser or serializer state machine. +pub(crate) struct State { + /// Name of the state enum variant for this state. + name: Ident, + + /// Declaration of members of the state enum in this state. + decl: TokenStream, + + /// Destructuring of members of the state enum in this state. + destructure: TokenStream, + + /// Right-hand-side of the match arm for this state. + advance_body: TokenStream, +} + +impl State { + /// Create a new state with the a builder data field. + /// + /// This is a convenience wrapper around `new()` and `add_field()`. This + /// wrapper, or its equivalent, **must** be used for states used in + /// [`FromEventsStateMachine`] state machines, as those expect that the + /// first field is the builder data at render time. + pub(crate) fn new_with_builder( + name: Ident, + builder_data_ident: &Ident, + builder_data_ty: &Type, + ) -> Self { + let mut result = Self::new(name); + result.add_field(builder_data_ident, builder_data_ty); + result + } + + /// Create a new, empty state. + /// + /// Note that an empty state will generate invalid code. At the very + /// least, a body must be added using [`Self::set_impl`] or + /// [`Self::with_impl`]. The various state machines may also have + /// additional requirements. + pub(crate) fn new(name: Ident) -> Self { + Self { + name, + decl: TokenStream::default(), + destructure: TokenStream::default(), + advance_body: TokenStream::default(), + } + } + + /// Add a field to this state's data. + /// + /// - `name` is the name under which the data will be accessible in the + /// state's implementation. + /// - `ty` must be the data field's type. + pub(crate) fn add_field(&mut self, name: &Ident, ty: &Type) { + self.decl.extend(quote! { #name: #ty, }); + self.destructure.extend(quote! { #name, }); + } + + /// Modify the state to include another field and return the modified + /// state. + /// + /// This is a consume-and-return-style version of [`Self::add_field`]. + pub(crate) fn with_field(mut self, name: &Ident, ty: &Type) -> Self { + self.add_field(name, ty); + self + } + + /// Set the `advance` implementation of this state. + /// + /// `body` must be the body of the right hand side of the match arm for + /// the `advance` implementation of the state machine. + /// + /// See [`FromEventsStateMachine::advance_match_arms`] and + /// [`IntoEventsSubmachine::compile`] for the respective + /// requirements on the implementations. + pub(crate) fn with_impl(mut self, body: TokenStream) -> Self { + self.advance_body = body; + self + } +} + +/// A partial [`FromEventsStateMachine`] which only covers the builder for a +/// single compound. +/// +/// See [`FromEventsStateMachine`] for more information on the state machines +/// in general. +pub(crate) struct FromEventsSubmachine { + /// Additional items necessary for the statemachine. + pub(crate) defs: TokenStream, + + /// States and state transition implementations. + pub(crate) states: Vec, + + /// Initializer expression. + /// + /// This expression must evaluate to a + /// `Result<#state_ty_ident, xso::FromEventsError>`. + pub(crate) init: TokenStream, +} + +impl FromEventsSubmachine { + /// Convert a partial state machine into a full state machine. + /// + /// This converts the abstract [`State`] items into token + /// streams for the respective parts of the state machine (the state + /// definitions and the match arms), rendering them effectively immutable. + pub(crate) fn compile(self) -> FromEventsStateMachine { + let mut state_defs = TokenStream::default(); + let mut advance_match_arms = TokenStream::default(); + + for state in self.states { + let State { + name, + decl, + destructure, + advance_body, + } = state; + + state_defs.extend(quote! { + #name { #decl }, + }); + + // 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 } => { + #advance_body + } + }); + } + + FromEventsStateMachine { + defs: self.defs, + state_defs, + advance_match_arms, + variants: vec![FromEventsEntryPoint { init: self.init }], + } + } + + /// Update the [`init`][`Self::init`] field in-place. + /// + /// The function will receive a reference to the current `init` value, + /// allowing to create "wrappers" around that existing code. + pub(crate) fn with_augmented_init TokenStream>( + mut self, + f: F, + ) -> Self { + let new_init = f(&self.init); + self.init = new_init; + self + } +} + +/// A partial [`IntoEventsStateMachine`] which only covers the builder for a +/// single compound. +/// +/// See [`IntoEventsStateMachine`] for more information on the state machines +/// in general. +pub(crate) struct IntoEventsSubmachine { + /// Additional items necessary for the statemachine. + pub(crate) defs: TokenStream, + + /// States and state transition implementations. + pub(crate) states: Vec, + + /// A pattern match which destructures the target type into its parts, for + /// use by `init`. + pub(crate) destructure: TokenStream, + + /// An expression which uses the names bound in `destructure` to create a + /// an instance of the state enum. + /// + /// The state enum type is available as `Self` in that context. + pub(crate) init: TokenStream, +} + +impl IntoEventsSubmachine { + /// Convert a partial state machine into a full state machine. + /// + /// This converts the abstract [`State`] items into token + /// streams for the respective parts of the state machine (the state + /// definitions and the match arms), rendering them effectively immutable. + /// + /// This requires that the [`State::advance_body`] token streams evaluate + /// to an `Option`. If it evaluates to `Some(.)`, that is + /// emitted from the iterator. If it evaluates to `None`, the `advance` + /// implementation is called again. + /// + /// Each state implementation is augmented to also enter the next state, + /// causing the iterator to terminate eventually. + pub(crate) fn compile(self) -> IntoEventsStateMachine { + let mut state_defs = TokenStream::default(); + let mut advance_match_arms = TokenStream::default(); + + for (i, state) in self.states.iter().enumerate() { + let State { + ref name, + ref decl, + ref destructure, + ref advance_body, + } = state; + + let footer = match self.states.get(i + 1) { + Some(State { + name: ref next_name, + destructure: ref construct_next, + .. + }) => { + quote! { + ::core::result::Result::Ok((::core::option::Option::Some(Self::#next_name { #construct_next }), event)) + } + } + // final state -> exit the state machine + None => { + quote! { + ::core::result::Result::Ok((::core::option::Option::None, event)) + } + } + }; + + state_defs.extend(quote! { + #name { #decl }, + }); + + advance_match_arms.extend(quote! { + Self::#name { #destructure } => { + let event = #advance_body; + #footer + } + }); + } + + IntoEventsStateMachine { + defs: self.defs, + state_defs, + advance_match_arms, + variants: vec![IntoEventsEntryPoint { + init: self.init, + destructure: self.destructure, + }], + } + } + + /// Update the [`init`][`Self::init`] field in-place. + /// + /// The function will receive a reference to the current `init` value, + /// allowing to create "wrappers" around that existing code. + pub(crate) fn with_augmented_init TokenStream>( + mut self, + f: F, + ) -> Self { + let new_init = f(&self.init); + self.init = new_init; + self + } +} + +/// Container for a single entrypoint into a [`FromEventsStateMachine`]. +pub(crate) struct FromEventsEntryPoint { + pub(crate) init: TokenStream, +} + +/// A single variant's entrypoint into the event iterator. +pub(crate) struct IntoEventsEntryPoint { + /// A pattern match which destructures the target type into its parts, for + /// use by `init`. + destructure: TokenStream, + + /// An expression which uses the names bound in `destructure` to create a + /// an instance of the state enum. + /// + /// The state enum type is available as `Self` in that context. + init: TokenStream, +} + +/// # State machine to implement `xso::FromEventsBuilder` +/// +/// This struct represents a state machine consisting of the following parts: +/// +/// - Extra dependencies ([`Self::defs`]) +/// - States ([`Self::state_defs`]) +/// - Transitions ([`Self::advance_match_arms`]) +/// - Entrypoints ([`Self::variants`]) +/// +/// Such a state machine is best constructed by constructing one or +/// more [`FromEventsSubmachine`] structs and converting/merging them using +/// `into()` and [`merge`][`Self::merge`]. +/// +/// A state machine has an output type (corresponding to +/// `xso::FromEventsBuilder::Output`), which is however only implicitly defined +/// by the expressions generated in the `advance_match_arms`. That means that +/// merging submachines with different output types works, but will then generate +/// code which will fail to compile. +/// +/// When converted to Rust code, the state machine will manifest as (among other +/// things) an enum type which contains all states and which has an `advance` +/// method. That method consumes the enum value and returns either a new enum +/// value, an error, or the output type of the state machine. +#[derive(Default)] +pub(crate) struct FromEventsStateMachine { + /// Extra items which are needed for the state machine implementation. + defs: TokenStream, + + /// A sequence of enum variant declarations, separated and terminated by + /// commas. + state_defs: TokenStream, + + /// A sequence of `match self { .. }` arms, where `self` is the state + /// enumeration type. + /// + /// Each match arm must either diverge or evaluate to a + /// `Result, xso::error::Error>`, where `State` + /// is the state enumeration and `Output` is the state machine's output + /// type. + advance_match_arms: TokenStream, + + /// The different entrypoints for the state machine. + /// + /// This may only contain more than one element if an enumeration is being + /// constructed by the resulting state machine. + variants: Vec, +} + +impl FromEventsStateMachine { + /// Render the state machine as a token stream. + /// + /// The token stream contains the following pieces: + /// - Any definitions necessary for the statemachine to operate + /// - The state enum + /// - The builder struct + /// - The `xso::FromEventsBuilder` impl on the builder struct + /// - A `fn new(rxml::QName, rxml::AttrMap) -> Result` on the + /// builder struct. + pub(crate) fn render( + self, + vis: &Visibility, + builder_ty_ident: &Ident, + state_ty_ident: &Ident, + output_ty: &Type, + ) -> Result { + let Self { + defs, + state_defs, + advance_match_arms, + variants, + } = self; + + let mut init_body = TokenStream::default(); + for variant in variants { + let FromEventsEntryPoint { init } = variant; + init_body.extend(quote! { + let (name, mut attrs) = match { { let _ = &mut attrs; } #init } { + ::core::result::Result::Ok(v) => return ::core::result::Result::Ok(v), + ::core::result::Result::Err(::xso::error::FromEventsError::Invalid(e)) => return ::core::result::Result::Err(::xso::error::FromEventsError::Invalid(e)), + ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs }) => (name, attrs), + }; + }) + } + + let output_ty_ref = make_ty_ref(output_ty); + + let docstr = format!("Build a {0} from XML events.\n\nThis type is generated using the [`macro@xso::FromXml`] derive macro and implements [`xso::FromEventsBuilder`] for {0}.", output_ty_ref); + + Ok(quote! { + #defs + + enum #state_ty_ident { + #state_defs + } + + impl #state_ty_ident { + fn advance(mut self, ev: ::xso::exports::rxml::Event) -> ::core::result::Result<::std::ops::ControlFlow, ::xso::error::Error> { + match self { + #advance_match_arms + }.and_then(|__ok| { + match __ok { + ::std::ops::ControlFlow::Break(st) => ::core::result::Result::Ok(::std::ops::ControlFlow::Break(st)), + ::std::ops::ControlFlow::Continue(result) => { + ::core::result::Result::Ok(::std::ops::ControlFlow::Continue(result)) + } + } + }) + } + } + + impl #builder_ty_ident { + fn new( + name: ::xso::exports::rxml::QName, + attrs: ::xso::exports::rxml::AttrMap, + ) -> ::core::result::Result { + #state_ty_ident::new(name, attrs).map(|ok| Self(::core::option::Option::Some(ok))) + } + } + + #[doc = #docstr] + #vis struct #builder_ty_ident(::core::option::Option<#state_ty_ident>); + + impl ::xso::FromEventsBuilder for #builder_ty_ident { + type Output = #output_ty; + + fn feed(&mut self, ev: ::xso::exports::rxml::Event) -> ::core::result::Result<::core::option::Option, ::xso::error::Error> { + let inner = self.0.take().expect("feed called after completion"); + match inner.advance(ev)? { + ::std::ops::ControlFlow::Continue(value) => ::core::result::Result::Ok(::core::option::Option::Some(value)), + ::std::ops::ControlFlow::Break(st) => { + self.0 = ::core::option::Option::Some(st); + ::core::result::Result::Ok(::core::option::Option::None) + } + } + } + } + + impl #state_ty_ident { + fn new( + name: ::xso::exports::rxml::QName, + mut attrs: ::xso::exports::rxml::AttrMap, + ) -> ::core::result::Result { + #init_body + { let _ = &mut attrs; } + ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs }) + } + } + }) + } +} + +/// # State machine to implement an `Iterator`. +/// +/// This struct represents a state machine consisting of the following parts: +/// +/// - Extra dependencies ([`Self::defs`]) +/// - States ([`Self::state_defs`]) +/// - Transitions ([`Self::advance_match_arms`]) +/// - Entrypoints ([`Self::variants`]) +/// +/// Such a state machine is best constructed by constructing one or +/// more [`FromEventsSubmachine`] structs and converting/merging them using +/// `into()` and [`merge`][`Self::merge`]. +/// +/// A state machine has an output type (corresponding to +/// `xso::FromEventsBuilder::Output`), which is however only implicitly defined +/// by the expressions generated in the `advance_match_arms`. That means that +/// merging submachines with different output types works, but will then generate +/// code which will fail to compile. +/// +/// When converted to Rust code, the state machine will manifest as (among other +/// things) an enum type which contains all states and which has an `advance` +/// method. That method consumes the enum value and returns either a new enum +/// value, an error, or the output type of the state machine. +#[derive(Default)] +pub(crate) struct IntoEventsStateMachine { + /// Extra items which are needed for the state machine implementation. + defs: TokenStream, + + /// A sequence of enum variant declarations, separated and terminated by + /// commas. + state_defs: TokenStream, + + /// A sequence of `match self { .. }` arms, where `self` is the state + /// enumeration type. + /// + /// Each match arm must either diverge or evaluate to a + /// `Result<(Option, Option), xso::error::Error>`, where + /// where `State` is the state enumeration. + /// + /// If `Some(.)` is returned for the event, that event is emitted. If + /// `None` is returned for the event, the advance implementation is called + /// again after switching to the state returned in the `Option` + /// field. + /// + /// If `None` is returned for the `Option`, the iterator + /// terminates yielding the `Option` value directly (even if it is + /// `None`). After the iterator has terminated, it yields `None` + /// indefinitely. + advance_match_arms: TokenStream, + + /// The different entrypoints for the state machine. + /// + /// This may only contain more than one element if an enumeration is being + /// serialised by the resulting state machine. + variants: Vec, +} + +impl IntoEventsStateMachine { + /// Render the state machine as a token stream. + /// + /// The token stream contains the following pieces: + /// - Any definitions necessary for the statemachine to operate + /// - The state enum + /// - The iterator struct + /// - The `Iterator` impl on the builder struct + /// - A `fn new(T) -> Result` on the iterator struct. + pub(crate) fn render( + self, + vis: &Visibility, + input_ty: &Type, + state_ty_ident: &Ident, + event_iter_ty_ident: &Ident, + ) -> Result { + let Self { + defs, + state_defs, + advance_match_arms, + mut variants, + } = self; + + let input_ty_ref = make_ty_ref(input_ty); + let docstr = format!("Convert a {0} into XML events.\n\nThis type is generated using the [`macro@xso::IntoXml`] derive macro and implements [`std::iter:Iterator`] for {0}.", input_ty_ref); + + let init_body = if variants.len() == 1 { + let IntoEventsEntryPoint { destructure, init } = variants.remove(0); + quote! { + { + let #destructure = value; + #init + } + } + } else { + let mut match_arms = TokenStream::default(); + for IntoEventsEntryPoint { destructure, init } in variants { + match_arms.extend(quote! { + #destructure => #init, + }); + } + + quote! { + match value { + #match_arms + } + } + }; + + Ok(quote! { + #defs + + enum #state_ty_ident { + #state_defs + } + + impl #state_ty_ident { + fn advance(mut self) -> ::core::result::Result<(::core::option::Option, ::core::option::Option<::xso::exports::rxml::Event>), ::xso::error::Error> { + match self { + #advance_match_arms + } + } + + fn new( + value: #input_ty, + ) -> ::core::result::Result { + ::core::result::Result::Ok(#init_body) + } + } + + #[doc = #docstr] + #vis struct #event_iter_ty_ident(::core::option::Option<#state_ty_ident>); + + 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 { + let mut state = self.0.take()?; + loop { + let (next_state, ev) = match state.advance() { + ::core::result::Result::Ok(v) => v, + ::core::result::Result::Err(e) => return ::core::option::Option::Some(::core::result::Result::Err(e)), + }; + if let ::core::option::Option::Some(ev) = ev { + self.0 = next_state; + return ::core::option::Option::Some(::core::result::Result::Ok(ev)); + } + // no event, do we have a state? + if let ::core::option::Option::Some(st) = next_state { + // we do: try again! + state = st; + continue; + } else { + // we don't: end of iterator! + self.0 = ::core::option::Option::None; + return ::core::option::Option::None; + } + } + } + } + + impl #event_iter_ty_ident { + fn new(value: #input_ty) -> ::core::result::Result { + #state_ty_ident::new(value).map(|ok| Self(::core::option::Option::Some(ok))) + } + } + }) + } +} + +/// Construct a path for an intradoc link from a given type. +fn doc_link_path(ty: &Type) -> Option { + match ty { + Type::Path(ref ty) => { + let (mut buf, offset) = match ty.qself { + Some(ref qself) => { + let mut buf = doc_link_path(&qself.ty)?; + buf.push_str("::"); + (buf, qself.position) + } + None => { + let mut buf = String::new(); + if ty.path.leading_colon.is_some() { + buf.push_str("::"); + } + (buf, 0) + } + }; + let last = ty.path.segments.len() - 1; + for i in offset..ty.path.segments.len() { + let segment = &ty.path.segments[i]; + buf.push_str(&segment.ident.to_string()); + if i < last { + buf.push_str("::"); + } + } + Some(buf) + } + _ => None, + } +} + +/// Create a markdown snippet which references the given type as cleanly as +/// possible. +/// +/// This is used in documentation generation functions. +/// +/// Not all types can be linked to; those which cannot be linked to will +/// simply be wrapped in backticks. +fn make_ty_ref(ty: &Type) -> String { + match doc_link_path(ty) { + Some(mut path) => { + path.reserve(4); + path.insert_str(0, "[`"); + path.push_str("`]"); + path + } + None => format!("`{}`", ty.to_token_stream()), + } +} diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index 28460b6..8503b38 100644 --- a/xso-proc/src/structs.rs +++ b/xso-proc/src/structs.rs @@ -10,6 +10,7 @@ use proc_macro2::TokenStream; use quote::quote; use syn::*; +use crate::compound::Compound; use crate::meta::{NameRef, NamespaceRef, XmlCompoundMeta}; /// Parts necessary to construct a `::xso::FromXml` implementation. @@ -44,6 +45,9 @@ pub(crate) struct StructDef { /// The XML name of the element to map the struct to. name: NameRef, + /// The field(s) of this struct. + inner: Compound, + /// Name of the target type. target_ty_ident: Ident, @@ -65,19 +69,10 @@ impl StructDef { 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, + inner: Compound::from_fields(fields)?, target_ty_ident: ident.clone(), builder_ty_ident: quote::format_ident!("{}FromXmlBuilder", ident), event_iter_ty_ident: quote::format_ident!("{}IntoXmlIterator", ident), @@ -95,68 +90,43 @@ impl StructDef { 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 state_ty_ident = 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) - } - } + let defs = self + .inner + .make_from_events_statemachine( + &state_ty_ident, + &target_ty_ident.clone().into(), + "Struct", + )? + .with_augmented_init(|init| { + quote! { + if name.0 != #xml_namespace || name.1 != #xml_name { + ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { + name, + attrs, + }) + } else { + #init } } - }, + }) + .compile() + .render( + vis, + &builder_ty_ident, + &state_ty_ident, + &TypePath { + qself: None, + path: target_ty_ident.clone().into(), + } + .into(), + )?; + + Ok(FromXmlParts { + defs, 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::new(#name_ident, #attrs_ident) }, builder_ty_ident: builder_ty_ident.clone(), }) @@ -168,49 +138,36 @@ impl StructDef { 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 state_ty_ident = quote::format_ident!("{}State", event_iter_ty_ident); - let docstr = format!("Decompose a [`{}`] into XML events", target_ty_ident); + let defs = self + .inner + .make_into_event_iter_statemachine(&target_ty_ident.clone().into(), "Struct")? + .with_augmented_init(|init| { + quote! { + let name = ( + ::xso::exports::rxml::Namespace::from(#xml_namespace), + #xml_name.into(), + ); + #init + } + }) + .compile() + .render( + vis, + &TypePath { + qself: None, + path: target_ty_ident.clone().into(), + } + .into(), + &state_ty_ident, + &event_iter_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, - } - } - } - }, + defs, 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::new(self) }, event_iter_ty_ident: event_iter_ty_ident.clone(), }) diff --git a/xso-proc/src/types.rs b/xso-proc/src/types.rs new file mode 100644 index 0000000..d39d92d --- /dev/null +++ b/xso-proc/src/types.rs @@ -0,0 +1,42 @@ +// 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/. + +//! Module with specific [`syn::Type`] constructors. + +use proc_macro2::Span; +use syn::*; + +/// Construct a [`syn::Type`] referring to `::xso::exports::rxml::QName`. +pub(crate) fn qname_ty(span: Span) -> Type { + Type::Path(TypePath { + qself: None, + 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("exports", span), + arguments: PathArguments::None, + }, + PathSegment { + ident: Ident::new("rxml", span), + arguments: PathArguments::None, + }, + PathSegment { + ident: Ident::new("QName", span), + arguments: PathArguments::None, + }, + ] + .into_iter() + .collect(), + }, + }) +}