Detailed changes
@@ -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]
@@ -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.");
}
}
@@ -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.");
}
}
@@ -115,7 +115,7 @@ fn empty_unexpected_attribute() {
};
match parse_str::<Empty>("<foo xmlns='urn:example:ns1' fnord='bar'/>") {
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::<Empty>("<foo xmlns='urn:example:ns1'><coucou/></foo>") {
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),
}
@@ -0,0 +1,155 @@
+// 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 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<Self> {
+ 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<FromEventsSubmachine> {
+ 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<IntoEventsSubmachine> {
+ 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 }
+ },
+ })
+ }
+}
@@ -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.
///
@@ -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<Self> {
if input.peek(syn::LitStr) {
@@ -0,0 +1,656 @@
+// 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/.
+
+//! 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<State>,
+
+ /// 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<F: FnOnce(&TokenStream) -> 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<State>,
+
+ /// 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<rxml::Event>`. 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<F: FnOnce(&TokenStream) -> 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<ControlFlow<State, Output>, 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<FromEventsEntryPoint>,
+}
+
+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<Self>` on the
+ /// builder struct.
+ pub(crate) fn render(
+ self,
+ vis: &Visibility,
+ builder_ty_ident: &Ident,
+ state_ty_ident: &Ident,
+ output_ty: &Type,
+ ) -> Result<TokenStream> {
+ 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<Self, #output_ty>, ::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<Self, ::xso::error::FromEventsError> {
+ #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<Self::Output>, ::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<Self, ::xso::error::FromEventsError> {
+ #init_body
+ { let _ = &mut attrs; }
+ ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs })
+ }
+ }
+ })
+ }
+}
+
+/// # State machine to implement an `Iterator<Item = rxml::Event>`.
+///
+/// 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<State>, Option<Event>), 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<State>`
+ /// field.
+ ///
+ /// If `None` is returned for the `Option<State>`, the iterator
+ /// terminates yielding the `Option<Event>` 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<IntoEventsEntryPoint>,
+}
+
+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<Self>` on the iterator struct.
+ pub(crate) fn render(
+ self,
+ vis: &Visibility,
+ input_ty: &Type,
+ state_ty_ident: &Ident,
+ event_iter_ty_ident: &Ident,
+ ) -> Result<TokenStream> {
+ 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<Self>, ::core::option::Option<::xso::exports::rxml::Event>), ::xso::error::Error> {
+ match self {
+ #advance_match_arms
+ }
+ }
+
+ fn new(
+ value: #input_ty,
+ ) -> ::core::result::Result<Self, ::xso::error::Error> {
+ ::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<Self::Item> {
+ 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<Self, ::xso::error::Error> {
+ #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<String> {
+ 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()),
+ }
+}
@@ -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 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)
- }
- }
+ let state_ty_ident = quote::format_ident!("{}State", builder_ty_ident);
+
+ 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
}
}
- },
- 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());
+ })
+ .compile()
+ .render(
+ vis,
+ &builder_ty_ident,
+ &state_ty_ident,
+ &TypePath {
+ qself: None,
+ path: target_ty_ident.clone().into(),
}
- ::core::result::Result::Ok(#builder_ty_ident(::core::option::Option::Some(#state_ty_name::Default)))
+ .into(),
+ )?;
+
+ Ok(FromXmlParts {
+ defs,
+ from_events_body: quote! {
+ #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 docstr = format!("Decompose a [`{}`] into XML events", target_ty_ident);
-
- Ok(IntoXmlParts {
- defs: quote! {
- enum #state_ty_name {
- Header,
- Footer,
+ let state_ty_ident = quote::format_ident!("{}State", event_iter_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
}
-
- #[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,
- }
- }
+ })
+ .compile()
+ .render(
+ vis,
+ &TypePath {
+ qself: None,
+ path: target_ty_ident.clone().into(),
}
- },
+ .into(),
+ &state_ty_ident,
+ &event_iter_ty_ident,
+ )?;
+
+ Ok(IntoXmlParts {
+ 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(),
})
@@ -0,0 +1,42 @@
+// 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/.
+
+//! 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(),
+ },
+ })
+}