@@ -23,7 +23,7 @@ pub const XMLNS_XML: &str = "http://www.w3.org/XML/1998/namespace";
pub const XMLNS_XMLNS: &str = "http://www.w3.org/2000/xmlns/";
macro_rules! reject_key {
- ($key:ident not on $not_allowed_on:literal only on $only_allowed_on:literal) => {
+ ($key:ident not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => {
if let Some($key) = $key {
return Err(Error::new_spanned(
$key,
@@ -32,15 +32,17 @@ macro_rules! reject_key {
stringify!($key),
"` is not allowed on ",
$not_allowed_on,
- " (only on ",
- $only_allowed_on,
- ")"
+ $(
+ " (only on ",
+ $only_allowed_on,
+ ")",
+ )?
),
));
}
};
- ($key:ident flag not on $not_allowed_on:literal only on $only_allowed_on:literal) => {
+ ($key:ident flag not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => {
if let Flag::Present($key) = $key {
return Err(Error::new(
$key,
@@ -49,9 +51,11 @@ macro_rules! reject_key {
stringify!($key),
"` is not allowed on ",
$not_allowed_on,
- " (only on ",
- $only_allowed_on,
- ")"
+ $(
+ " (only on ",
+ $only_allowed_on,
+ ")",
+ )?
),
));
}
@@ -252,6 +256,13 @@ impl Flag {
Self::Present(_) => true,
}
}
+
+ /// Like `Option::take`, but for flags.
+ pub(crate) fn take(&mut self) -> Self {
+ let mut result = Flag::Absent;
+ core::mem::swap(&mut result, self);
+ result
+ }
}
impl<T: Spanned> From<T> for Flag {
@@ -340,6 +351,9 @@ pub(crate) struct XmlCompoundMeta {
/// The exhaustive flag.
pub(crate) exhaustive: Flag,
+
+ /// The transparent flag.
+ pub(crate) transparent: Flag,
}
impl XmlCompoundMeta {
@@ -353,6 +367,7 @@ impl XmlCompoundMeta {
let mut iterator = None;
let mut debug = Flag::Absent;
let mut exhaustive = Flag::Absent;
+ let mut transparent = Flag::Absent;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("debug") {
@@ -379,6 +394,12 @@ impl XmlCompoundMeta {
}
exhaustive = (&meta.path).into();
Ok(())
+ } else if meta.path.is_ident("transparent") {
+ if transparent.is_set() {
+ return Err(Error::new_spanned(meta.path, "duplicate `transparent` key"));
+ }
+ transparent = (&meta.path).into();
+ Ok(())
} else {
match qname.parse_incremental_from_meta(meta)? {
None => Ok(()),
@@ -394,6 +415,7 @@ impl XmlCompoundMeta {
builder,
iterator,
exhaustive,
+ transparent,
})
}
@@ -6,42 +6,59 @@
//! Handling of structs
-use proc_macro2::Span;
+use proc_macro2::{Span, TokenStream};
use quote::quote;
-use syn::*;
+use syn::{spanned::Spanned, *};
use crate::common::{AsXmlParts, FromXmlParts, ItemDef};
use crate::compound::Compound;
+use crate::error_message::ParentRef;
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
-use crate::types::{ref_ty, ty_from_ident};
+use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State};
+use crate::types::{
+ as_xml_iter_fn, feed_fn, from_events_fn, from_xml_builder_ty, item_iter_ty, ref_ty,
+ ty_from_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,
+/// The inner parts of the struct.
+///
+/// This contains all data necessary for the matching logic.
+enum StructInner {
+ /// Single-field struct declared with `#[xml(transparent)]`.
+ ///
+ /// Transparent struct delegate all parsing and serialising to their
+ /// only field, which is why they do not need to store a lot of
+ /// information and come with extra restrictions, such as:
+ ///
+ /// - no XML namespace can be declared (it is determined by inner type)
+ /// - no XML name can be declared (it is determined by inner type)
+ /// - there must be only exactly one field
+ /// - that field has no `#[xml]` attribute
+ Transparent {
+ /// The member identifier of the only field.
+ member: Member,
- /// The field(s) of this struct.
- inner: Compound,
+ /// Type of the only field.
+ ty: Type,
+ },
- /// Name of the target type.
- target_ty_ident: Ident,
+ /// A compound of fields, *not* declared as transparent.
+ ///
+ /// This can be a unit, tuple-like, or named struct.
+ Compound {
+ /// The XML namespace of the element to map the struct to.
+ xml_namespace: NamespaceRef,
- /// Name of the builder type.
- builder_ty_ident: Ident,
-
- /// Name of the iterator type.
- item_iter_ty_ident: Ident,
+ /// The XML name of the element to map the struct to.
+ xml_name: NameRef,
- /// Flag whether debug mode is enabled.
- debug: bool,
+ /// The field(s) of this struct.
+ inner: Compound,
+ },
}
-impl StructDef {
- /// Create a new struct from its name, meta, and fields.
- pub(crate) fn new(ident: &Ident, meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
+impl StructInner {
+ fn new(meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
// We destructure here so that we get informed when new fields are
// added and can handle them, either by processing them or raising
// an error if they are present.
@@ -52,32 +69,268 @@ impl StructDef {
debug,
builder,
iterator,
+ transparent,
} = meta;
+ // These must've been cleared by the caller. Because these being set
+ // is a programming error (in xso-proc) and not a usage error, we
+ // assert here instead of using reject_key!.
+ assert!(builder.is_none());
+ assert!(iterator.is_none());
+ assert!(!debug.is_set());
+
reject_key!(exhaustive flag not on "structs" only on "enums");
- let Some(namespace) = namespace else {
- return Err(Error::new(meta_span, "`namespace` is required on structs"));
- };
+ if let Flag::Present(_) = transparent {
+ reject_key!(namespace not on "transparent structs");
+ reject_key!(name not on "transparent structs");
- let Some(name) = name else {
- return Err(Error::new(meta_span, "`name` is required on structs"));
- };
+ let fields_span = fields.span();
+ let fields = match fields {
+ Fields::Unit => {
+ return Err(Error::new(
+ fields_span,
+ "transparent structs or enum variants must have exactly one field",
+ ))
+ }
+ Fields::Named(FieldsNamed {
+ named: ref fields, ..
+ })
+ | Fields::Unnamed(FieldsUnnamed {
+ unnamed: ref fields,
+ ..
+ }) => fields,
+ };
+
+ if fields.len() != 1 {
+ return Err(Error::new(
+ fields_span,
+ "transparent structs or enum variants must have exactly one field",
+ ));
+ }
+
+ let field = &fields[0];
+ for attr in field.attrs.iter() {
+ if attr.meta.path().is_ident("xml") {
+ return Err(Error::new_spanned(
+ attr,
+ "#[xml(..)] attributes are not allowed inside transparent structs",
+ ));
+ }
+ }
+ let member = match field.ident.as_ref() {
+ Some(v) => Member::Named(v.clone()),
+ None => Member::Unnamed(Index {
+ span: field.ty.span(),
+ index: 0,
+ }),
+ };
+ let ty = field.ty.clone();
+ Ok(Self::Transparent { ty, member })
+ } else {
+ let Some(xml_namespace) = namespace else {
+ return Err(Error::new(
+ meta_span,
+ "`namespace` is required on non-transparent structs",
+ ));
+ };
+
+ let Some(xml_name) = name else {
+ return Err(Error::new(
+ meta_span,
+ "`name` is required on non-transparent structs",
+ ));
+ };
+
+ Ok(Self::Compound {
+ inner: Compound::from_fields(fields, &xml_namespace)?,
+ xml_namespace,
+ xml_name,
+ })
+ }
+ }
+
+ fn make_from_events_statemachine(
+ &self,
+ state_ty_ident: &Ident,
+ output_name: &ParentRef,
+ state_prefix: &str,
+ ) -> Result<FromEventsSubmachine> {
+ match self {
+ Self::Transparent { ty, member } => {
+ let from_xml_builder_ty = from_xml_builder_ty(ty.clone());
+ let from_events_fn = from_events_fn(ty.clone());
+ let feed_fn = feed_fn(from_xml_builder_ty.clone());
+
+ let output_cons = match output_name {
+ ParentRef::Named(ref path) => quote! {
+ #path { #member: result }
+ },
+ ParentRef::Unnamed { .. } => quote! {
+ ( result, )
+ },
+ };
+
+ let state_name = quote::format_ident!("{}Default", state_prefix);
+ let builder_data_ident = quote::format_ident!("__xso_data");
+
+ // Here, we generate a partial statemachine which really only
+ // proxies the FromXmlBuilder implementation of the inner
+ // type.
+ Ok(FromEventsSubmachine {
+ defs: TokenStream::default(),
+ states: vec![
+ State::new_with_builder(
+ state_name.clone(),
+ &builder_data_ident,
+ &from_xml_builder_ty,
+ )
+ .with_impl(quote! {
+ match #feed_fn(&mut #builder_data_ident, ev)? {
+ ::core::option::Option::Some(result) => {
+ ::core::result::Result::Ok(::core::ops::ControlFlow::Continue(#output_cons))
+ }
+ ::core::option::Option::None => {
+ ::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#state_name {
+ #builder_data_ident,
+ }))
+ }
+ }
+ })
+ ],
+ init: quote! {
+ #from_events_fn(name, attrs).map(|#builder_data_ident| Self::#state_name { #builder_data_ident })
+ },
+ })
+ }
+
+ Self::Compound {
+ ref inner,
+ ref xml_namespace,
+ ref xml_name,
+ } => Ok(inner
+ .make_from_events_statemachine(state_ty_ident, output_name, state_prefix)?
+ .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
+ }
+ }
+ })),
+ }
+ }
+
+ fn make_as_item_iter_statemachine(
+ &self,
+ input_name: &ParentRef,
+ state_ty_ident: &Ident,
+ state_prefix: &str,
+ item_iter_ty_lifetime: &Lifetime,
+ ) -> Result<AsItemsSubmachine> {
+ match self {
+ Self::Transparent { ty, member } => {
+ let item_iter_ty = item_iter_ty(ty.clone(), item_iter_ty_lifetime.clone());
+ let as_xml_iter_fn = as_xml_iter_fn(ty.clone());
+
+ let state_name = quote::format_ident!("{}Default", state_prefix);
+ let iter_ident = quote::format_ident!("__xso_data");
- let builder_ty_ident = match builder {
+ let destructure = match input_name {
+ ParentRef::Named(ref path) => quote! {
+ #path { #member: #iter_ident }
+ },
+ ParentRef::Unnamed { .. } => quote! {
+ (#iter_ident, )
+ },
+ };
+
+ // Here, we generate a partial statemachine which really only
+ // proxies the AsXml iterator implementation from the inner
+ // type.
+ Ok(AsItemsSubmachine {
+ defs: TokenStream::default(),
+ states: vec![State::new_with_builder(
+ state_name.clone(),
+ &iter_ident,
+ &item_iter_ty,
+ )
+ .with_mut(&iter_ident)
+ .with_impl(quote! {
+ #iter_ident.next().transpose()?
+ })],
+ destructure,
+ init: quote! {
+ #as_xml_iter_fn(#iter_ident).map(|#iter_ident| Self::#state_name { #iter_ident })?
+ },
+ })
+ }
+
+ Self::Compound {
+ ref inner,
+ ref xml_namespace,
+ ref xml_name,
+ } => Ok(inner
+ .make_as_item_iter_statemachine(
+ input_name,
+ state_ty_ident,
+ state_prefix,
+ 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
+ }
+ })),
+ }
+ }
+}
+
+/// Definition of a struct and how to parse it.
+pub(crate) struct StructDef {
+ /// 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,
+
+ /// The matching logic and contents of the struct.
+ inner: StructInner,
+}
+
+impl StructDef {
+ /// Create a new struct from its name, meta, and fields.
+ pub(crate) fn new(ident: &Ident, mut meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
+ let builder_ty_ident = match meta.builder.take() {
Some(v) => v,
None => quote::format_ident!("{}FromXmlBuilder", ident.to_string()),
};
- let item_iter_ty_ident = match iterator {
+ let item_iter_ty_ident = match meta.iterator.take() {
Some(v) => v,
None => quote::format_ident!("{}AsXmlIterator", ident.to_string()),
};
+ let debug = meta.debug.take();
+
+ let inner = StructInner::new(meta, fields)?;
+
Ok(Self {
- inner: Compound::from_fields(fields, &namespace)?,
- namespace,
- name,
+ inner,
target_ty_ident: ident.clone(),
builder_ty_ident,
item_iter_ty_ident,
@@ -93,9 +346,6 @@ impl ItemDef for StructDef {
name_ident: &Ident,
attrs_ident: &Ident,
) -> Result<FromXmlParts> {
- let xml_namespace = &self.namespace;
- let xml_name = &self.name;
-
let target_ty_ident = &self.target_ty_ident;
let builder_ty_ident = &self.builder_ty_ident;
let state_ty_ident = quote::format_ident!("{}State", builder_ty_ident);
@@ -107,18 +357,6 @@ impl ItemDef for StructDef {
&Path::from(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,
@@ -141,9 +379,6 @@ impl ItemDef for StructDef {
}
fn make_as_xml_iter(&self, vis: &Visibility) -> Result<AsXmlParts> {
- let xml_namespace = &self.namespace;
- let xml_name = &self.name;
-
let target_ty_ident = &self.target_ty_ident;
let item_iter_ty_ident = &self.item_iter_ty_ident;
let item_iter_ty_lifetime = Lifetime {
@@ -183,15 +418,6 @@ impl ItemDef for StructDef {
"Struct",
&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()
.render(
vis,