xso: implement support for enums

This commit is contained in:
Jonas Schäfer 2024-06-30 09:06:10 +02:00
commit 4845715add
9 changed files with 594 additions and 8 deletions

View file

@ -297,7 +297,7 @@ impl Compound {
/// `rxml::QName` is in scope.
pub(crate) fn make_as_item_iter_statemachine(
&self,
input_name: &Path,
input_name: &ParentRef,
state_prefix: &str,
lifetime: &Lifetime,
) -> Result<AsItemsSubmachine> {
@ -430,11 +430,13 @@ impl Compound {
}),
);
let ParentRef::Named(input_path) = input_name;
Ok(AsItemsSubmachine {
defs: TokenStream::default(),
states,
destructure: quote! {
#input_name { #destructure }
#input_path { #destructure }
},
init: quote! {
Self::#element_head_start_state_ident { #dummy_ident: ::std::marker::PhantomData, #name_ident: name.1, #ns_ident: name.0, #start_init }

319
xso-proc/src/enums.rs Normal file
View file

@ -0,0 +1,319 @@
// 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 enums
use std::collections::HashMap;
use proc_macro2::Span;
use quote::quote;
use syn::*;
use crate::common::{AsXmlParts, FromXmlParts, ItemDef};
use crate::compound::Compound;
use crate::error_message::ParentRef;
use crate::meta::{NameRef, NamespaceRef, XmlCompoundMeta};
use crate::state::{AsItemsStateMachine, FromEventsStateMachine};
/// The definition of an enum variant, switched on the XML element's name.
struct NameVariant {
/// The XML name of the element to map the enum variant to.
name: NameRef,
/// The name of the variant
ident: Ident,
/// The field(s) of this struct.
inner: Compound,
}
impl NameVariant {
/// Construct a new name-selected variant from its declaration.
fn new(decl: &Variant) -> Result<Self> {
let meta = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"`namespace` is not allowed on enum variants (only on enums and structs)",
));
}
let Some(name) = meta.name else {
return Err(Error::new(meta.span, "`name` is required on enum variants"));
};
Ok(Self {
name,
ident: decl.ident.clone(),
inner: Compound::from_fields(&decl.fields)?,
})
}
fn make_from_events_statemachine(
&self,
enum_ident: &Ident,
state_ty_ident: &Ident,
) -> Result<FromEventsStateMachine> {
let xml_name = &self.name;
Ok(self
.inner
.make_from_events_statemachine(
state_ty_ident,
&ParentRef::Named(Path {
leading_colon: None,
segments: [
PathSegment::from(enum_ident.clone()),
self.ident.clone().into(),
]
.into_iter()
.collect(),
}),
&self.ident.to_string(),
)?
.with_augmented_init(|init| {
quote! {
if name.1 != #xml_name {
::core::result::Result::Err(::xso::error::FromEventsError::Mismatch {
name,
attrs,
})
} else {
#init
}
}
})
.compile())
}
fn make_as_item_iter_statemachine(
&self,
xml_namespace: &NamespaceRef,
enum_ident: &Ident,
item_iter_ty_lifetime: &Lifetime,
) -> Result<AsItemsStateMachine> {
let xml_name = &self.name;
Ok(self
.inner
.make_as_item_iter_statemachine(
&ParentRef::Named(Path {
leading_colon: None,
segments: [
PathSegment::from(enum_ident.clone()),
self.ident.clone().into(),
]
.into_iter()
.collect(),
}),
&self.ident.to_string(),
&item_iter_ty_lifetime,
)?
.with_augmented_init(|init| {
quote! {
let name = (
::xso::exports::rxml::Namespace::from(#xml_namespace),
::std::borrow::Cow::Borrowed(#xml_name),
);
#init
}
})
.compile())
}
}
/// Definition of an enum and how to parse it.
pub(crate) struct EnumDef {
/// The XML namespace of the element to map the enum to.
namespace: NamespaceRef,
/// The variants of the enum.
variants: Vec<NameVariant>,
/// Name of the target type.
target_ty_ident: Ident,
/// Name of the builder type.
builder_ty_ident: Ident,
/// Name of the iterator type.
item_iter_ty_ident: Ident,
/// Flag whether debug mode is enabled.
debug: bool,
}
impl EnumDef {
/// Create a new enum from its name, meta, and variants.
pub(crate) fn new<'x, I: IntoIterator<Item = &'x Variant>>(
ident: &Ident,
meta: XmlCompoundMeta,
variant_iter: I,
) -> Result<Self> {
if let Some(name) = meta.name {
return Err(Error::new_spanned(
name,
"`name` is not allowed on enums (only on their variants)",
));
}
let Some(namespace) = meta.namespace else {
return Err(Error::new(meta.span, "`namespace` is required on enums"));
};
let mut variants = Vec::new();
let mut seen_names = HashMap::new();
for variant in variant_iter {
let variant = NameVariant::new(variant)?;
if let Some(other) = seen_names.get(&variant.name) {
return Err(Error::new_spanned(
variant.name,
format!(
"duplicate `name` in enum: variants {} and {} have the same XML name",
other, variant.ident
),
));
}
seen_names.insert(variant.name.clone(), variant.ident.clone());
variants.push(variant);
}
let builder_ty_ident = match meta.builder {
Some(v) => v,
None => quote::format_ident!("{}FromXmlBuilder", ident.to_string()),
};
let item_iter_ty_ident = match meta.iterator {
Some(v) => v,
None => quote::format_ident!("{}AsXmlIterator", ident.to_string()),
};
Ok(Self {
namespace,
variants,
target_ty_ident: ident.clone(),
builder_ty_ident,
item_iter_ty_ident,
debug: meta.debug.is_set(),
})
}
}
impl ItemDef for EnumDef {
fn make_from_events_builder(
&self,
vis: &Visibility,
name_ident: &Ident,
attrs_ident: &Ident,
) -> Result<FromXmlParts> {
let xml_namespace = &self.namespace;
let target_ty_ident = &self.target_ty_ident;
let builder_ty_ident = &self.builder_ty_ident;
let state_ty_ident = quote::format_ident!("{}State", builder_ty_ident);
let mut statemachine = FromEventsStateMachine::new();
for variant in self.variants.iter() {
statemachine
.merge(variant.make_from_events_statemachine(target_ty_ident, &state_ty_ident)?);
}
statemachine.set_pre_init(quote! {
if name.0 != #xml_namespace {
return ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch {
name,
attrs,
})
}
});
let defs = statemachine.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! {
#builder_ty_ident::new(#name_ident, #attrs_ident)
},
builder_ty_ident: builder_ty_ident.clone(),
})
}
fn make_as_xml_iter(&self, vis: &Visibility) -> Result<AsXmlParts> {
let target_ty_ident = &self.target_ty_ident;
let item_iter_ty_ident = &self.item_iter_ty_ident;
let item_iter_ty_lifetime = Lifetime {
apostrophe: Span::call_site(),
ident: Ident::new("xso_proc_as_xml_iter_lifetime", Span::call_site()),
};
let item_iter_ty = Type::Path(TypePath {
qself: None,
path: Path {
leading_colon: None,
segments: [PathSegment {
ident: item_iter_ty_ident.clone(),
arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments {
colon2_token: None,
lt_token: token::Lt {
spans: [Span::call_site()],
},
args: [GenericArgument::Lifetime(item_iter_ty_lifetime.clone())]
.into_iter()
.collect(),
gt_token: token::Gt {
spans: [Span::call_site()],
},
}),
}]
.into_iter()
.collect(),
},
});
let state_ty_ident = quote::format_ident!("{}State", item_iter_ty_ident);
let mut statemachine = AsItemsStateMachine::new();
for variant in self.variants.iter() {
statemachine.merge(variant.make_as_item_iter_statemachine(
&self.namespace,
target_ty_ident,
&item_iter_ty_lifetime,
)?);
}
let defs = statemachine.render(
vis,
&TypePath {
qself: None,
path: target_ty_ident.clone().into(),
}
.into(),
&state_ty_ident,
&item_iter_ty_lifetime,
&item_iter_ty,
)?;
Ok(AsXmlParts {
defs,
as_xml_iter_body: quote! {
#item_iter_ty_ident::new(self)
},
item_iter_ty,
item_iter_ty_lifetime,
})
}
fn debug(&self) -> bool {
self.debug
}
}

View file

@ -27,6 +27,7 @@ use syn::*;
mod common;
mod compound;
mod enums;
mod error_message;
mod field;
mod meta;
@ -41,12 +42,17 @@ use common::{AsXmlParts, FromXmlParts, ItemDef};
///
/// If the item is of an unsupported variant, an appropriate error is
/// returned.
fn parse_struct(item: Item) -> Result<(Visibility, Ident, structs::StructDef)> {
fn parse_struct(item: Item) -> Result<(Visibility, Ident, Box<dyn ItemDef>)> {
match item {
Item::Struct(item) => {
let meta = meta::XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let def = structs::StructDef::new(&item.ident, meta, &item.fields)?;
Ok((item.vis, item.ident, def))
Ok((item.vis, item.ident, Box::new(def)))
}
Item::Enum(item) => {
let meta = meta::XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let def = enums::EnumDef::new(&item.ident, meta, &item.variants)?;
Ok((item.vis, item.ident, Box::new(def)))
}
other => Err(Error::new_spanned(other, "cannot derive on this item")),
}

View file

@ -9,6 +9,8 @@
//! This module is concerned with parsing attributes from the Rust "meta"
//! annotations on structs, enums, enum variants and fields.
use std::hash::{Hash, Hasher};
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{meta::ParseNestedMeta, spanned::Spanned, *};
@ -56,7 +58,7 @@ impl quote::ToTokens for NamespaceRef {
}
/// Value for the `#[xml(name = .. )]` attribute.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) enum NameRef {
/// The XML name is specified as a string literal.
Literal {
@ -71,6 +73,38 @@ pub(crate) enum NameRef {
Path(Path),
}
impl Hash for NameRef {
fn hash<H: Hasher>(&self, h: &mut H) {
match self {
Self::Literal { ref value, .. } => value.hash(h),
Self::Path(ref path) => path.hash(h),
}
}
}
impl PartialEq for NameRef {
fn eq(&self, other: &NameRef) -> bool {
match self {
Self::Literal {
value: ref my_value,
..
} => match other {
Self::Literal {
value: ref other_value,
..
} => my_value == other_value,
_ => false,
},
Self::Path(ref my_path) => match other {
Self::Path(ref other_path) => my_path == other_path,
_ => false,
},
}
}
}
impl Eq for NameRef {}
impl syn::parse::Parse for NameRef {
fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
if input.peek(syn::LitStr) {

View file

@ -176,6 +176,7 @@ impl FromEventsSubmachine {
state_defs,
advance_match_arms,
variants: vec![FromEventsEntryPoint { init: self.init }],
pre_init: TokenStream::default(),
}
}
@ -369,6 +370,9 @@ pub(crate) struct FromEventsStateMachine {
/// Extra items which are needed for the state machine implementation.
defs: TokenStream,
/// Extra code run during pre-init phase.
pre_init: TokenStream,
/// A sequence of enum variant declarations, separated and terminated by
/// commas.
state_defs: TokenStream,
@ -390,6 +394,36 @@ pub(crate) struct FromEventsStateMachine {
}
impl FromEventsStateMachine {
/// Create a new, empty state machine.
pub(crate) fn new() -> Self {
Self {
defs: TokenStream::default(),
state_defs: TokenStream::default(),
advance_match_arms: TokenStream::default(),
pre_init: TokenStream::default(),
variants: Vec::new(),
}
}
/// Merge another state machine into this state machine.
///
/// This *discards* the other state machine's pre-init code.
pub(crate) fn merge(&mut self, other: FromEventsStateMachine) {
self.defs.extend(other.defs);
self.state_defs.extend(other.state_defs);
self.advance_match_arms.extend(other.advance_match_arms);
self.variants.extend(other.variants);
}
/// Set additional code to inject at the head of the `new` method for the
/// builder.
///
/// This can be used to do preliminary checks and is commonly used with
/// specifically-formed init codes on the variants.
pub(crate) fn set_pre_init(&mut self, code: TokenStream) {
self.pre_init = code;
}
/// Render the state machine as a token stream.
///
/// The token stream contains the following pieces:
@ -411,9 +445,10 @@ impl FromEventsStateMachine {
state_defs,
advance_match_arms,
variants,
pre_init,
} = self;
let mut init_body = TokenStream::default();
let mut init_body = pre_init;
for variant in variants {
let FromEventsEntryPoint { init } = variant;
init_body.extend(quote! {
@ -550,6 +585,24 @@ pub(crate) struct AsItemsStateMachine {
}
impl AsItemsStateMachine {
/// Create a new, empty state machine.
pub(crate) fn new() -> Self {
Self {
defs: TokenStream::default(),
state_defs: TokenStream::default(),
advance_match_arms: TokenStream::default(),
variants: Vec::new(),
}
}
/// Merge another state machine into this state machine.
pub(crate) fn merge(&mut self, other: AsItemsStateMachine) {
self.defs.extend(other.defs);
self.state_defs.extend(other.state_defs);
self.advance_match_arms.extend(other.advance_match_arms);
self.variants.extend(other.variants);
}
/// Render the state machine as a token stream.
///
/// The token stream contains the following pieces:
@ -588,7 +641,7 @@ impl AsItemsStateMachine {
let mut match_arms = TokenStream::default();
for AsItemsEntryPoint { destructure, init } in variants {
match_arms.extend(quote! {
#destructure => #init,
#destructure => { #init }
});
}

View file

@ -163,7 +163,7 @@ impl ItemDef for StructDef {
let defs = self
.inner
.make_as_item_iter_statemachine(
&target_ty_ident.clone().into(),
&Path::from(target_ty_ident.clone()).into(),
"Struct",
&item_iter_ty_lifetime,
)?