xso_proc: refactor element matching and parsing once more

The Compound abstraction was incorrect, as can be seen by the huge
amount of edge cases handled in obscure match statements on the
`namesace` field.

Attempting to generalize this to allow building enums which switch on an
attribute value instead of a XML element name turned out to be horror
trip.

This new abstraction has much more clear-cut responsibilities:

- Compound (de-)structures Rust fields into/from Elements.
- StructInner handles everything related to struct-like things (enum
  variants and structs), with the exception of validation/preparation.
- StructDef handles validation and preparation on top of StructInner.

And then enums get the differentiation within the type system they
deserve: Those which are fully dynamic and those which are switched on
the XML element's name. That abstraction turns out to be so useful that
it probably very straightforwardly will generalize into matching on
attribute names instead.

All in all, this should be good.
This commit is contained in:
Jonas Schäfer 2024-03-29 09:31:08 +01:00
parent 1a33deef3f
commit 517339e375
11 changed files with 1469 additions and 1024 deletions

View File

@ -453,6 +453,16 @@ fn enum_fallback_parse_as_fallback() {
}
}
#[test]
fn enum_fallback_matching_namespace() {
match crate::util::test::parse_str::<EnumFallback>(
"<variant-3 xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f'/>",
) {
Err(Error::TypeMismatch(_, _, _)) => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(namespace = self::TEST_NS1, name = "flag-container")]
pub struct Flag {

View File

@ -53,3 +53,34 @@ pub(crate) fn bake_generics(generics: Generics) -> (TokenStream, TokenStream, Op
(quote! {}, quote! {}, where_clause)
}
}
/// Build a statement calling the validator function at `validate`, if any.
///
/// This assumes that the argument for `validate` is called `result`.
pub(crate) fn build_validate(validate: Option<Path>) -> Stmt {
syn::parse2(if let Some(validate) = validate {
quote! {
#validate(&mut result)?;
}
} else {
quote! {
{ let _ = &mut result; };
}
})
.expect("failed to build validation code")
}
/// Build a statement calling the preparation function at `prepare`, if any.
///
/// The argument passed to `prepare` is `value_ident`.
pub(crate) fn build_prepare(prepare: Option<Path>, value_ident: &Ident) -> TokenStream {
if let Some(prepare) = prepare {
quote! {
#prepare(&mut #value_ident);
}
} else {
quote! {
{ let _ = &mut #value_ident; };
}
}
}

View File

@ -12,171 +12,25 @@ use syn::{spanned::Spanned, *};
use crate::error_message::ParentRef;
use crate::field::{FieldDef, FieldParsePart};
use crate::meta::{Flag, Name, NamespaceRef, StaticNamespace, XmlCompoundMeta};
/// A XML namespace as declared on a compound.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum CompoundNamespace {
/// The namespace is a static string.
Static(
/// The namespace as [`Path`] pointing at the static string.
StaticNamespace,
),
/// Instead of a fixed namespace, the namespace is dynamic. The allowed
/// values are determined by a field with
/// [`FieldKind::Namespace`][`crate::field::FieldKind::Namespace`] kind
/// (declared using `#[xml(namespace)]`).
Dyn {
/// The `dyn` token from the `#[xml(namespace = dyn)]` meta.
dyn_tok: Token![dyn],
/// The type of the namespace field.
ty: Type,
/// The member of the namespace field.
member: Member,
},
/// Instead of a fixed namespace, the namespace is the same as the parent
/// element.
///
/// This is only ever generated internally: structs, enums, and enum
/// variants cannot be declared with a `super` namespace (it doesn't make
/// sense).
Super(Token![super]),
}
/// An struct or enum variant's contents.
///
/// This struct is used to generate the parsing/serialisation loop, but most
/// notably it is *not* responsible for matching the incoming element; that
/// is the responsibility of the caller of the corresponding functions.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum Compound {
/// Single-field tuple-like compound declared with `#[xml(transparent)]`.
///
/// Transparent compounds delegate all parsing and serialising to their
/// single 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)
/// - the fields must be unnamed
/// - there must be only exactly one field
/// - that field has no `#[xml]` attribute
Transparent {
/// The type of the only field of the compound.
///
/// As the field is unnamed, there is no identifier associated with
/// it. Transparent fields also cannot have attributes.
ty: Type,
},
pub(crate) struct Compound {
/// The fields, in declaration order.
fields: Vec<FieldDef>,
/// A compound of fields, *not* declared as transparent.
///
/// This can be a unit, tuple-like, or struct-like struct or enum variant.
/// This is also constructed inside for extracted fields (i.e. fields
/// using `#[child(.., extract(..))]`) in order to extract the contents
/// of the children without duplicating much code.
Struct {
/// The XML namespace of the compound.
///
/// This is optional only for enum variants.
namespace: Option<CompoundNamespace>,
/// The XML name of the compound.
///
/// This is not to be confused with it's Rust identifier, which is not
/// stored here (but in the respective
/// [`crate::enums::EnumVariant`]; for structs, it is not necessary to
/// store at all).
name: Name,
/// The fields, in declaration order.
fields: Vec<FieldDef>,
},
/// Information about the field annotated with `#[xml(namespace)]`, if
/// any.
namespace_field: Option<(Span, Type, Member)>,
}
impl Compound {
/// Construct a [`Self::Transparent`] variant from a
/// [`XmlCompoundMeta`] and the fields of that compound.
///
/// This enforces the requirements described in that variant.
fn new_transparent(meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"namespace option not allowed on transparent structs or enum variants",
));
}
if let Some(name) = meta.name {
return Err(Error::new_spanned(
name,
"name option not allowed on transparent structs or enum variants",
));
}
let field = match fields {
Fields::Unit => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
))
}
Fields::Named(_) => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must be tuple-like",
))
}
Fields::Unnamed(fields) => {
if fields.unnamed.len() == 0 {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
));
} else if fields.unnamed.len() > 1 {
return Err(Error::new_spanned(
&fields.unnamed[1],
"transparent structs or enum variants must have exactly one field",
));
}
&fields.unnamed[0]
}
};
for attr in field.attrs.iter() {
if attr.path().is_ident("xml") {
return Err(Error::new_spanned(
attr.path(),
"the field inside a #[xml(transparent)] struct or enum variant cannot have an #[xml(..)] attribute."
));
}
}
Ok(Self::Transparent {
ty: field.ty.clone(),
})
}
/// Construct a new [`Self::Struct`] from its [`XmlCompoundMeta`] and an
/// iterator of [`FieldDef`] structs.
///
/// This constructor works different than the [`new_transparent`] internal
/// constructor because it is also used for extracted child fields, which
/// do not have access to a full [`Fields`] struct.
///
/// The constructor enforces the requirements of a `Struct` variant.
pub(crate) fn new_struct<T: Iterator<Item = Result<FieldDef>>>(
meta: XmlCompoundMeta,
input: T,
) -> Result<Self> {
let name: Name = match meta.name {
Some(v) => v.into(),
None => {
return Err(Error::new(
Span::call_site(),
"#[xml(name = ..)] is required on non-transparent structs or enum variants",
))
}
};
/// Construct a new compound from an iterator of [`FieldDef`] structs.
pub(crate) fn new<T: Iterator<Item = Result<FieldDef>>>(input: T) -> Result<Self> {
let mut fields = Vec::with_capacity(input.size_hint().1.unwrap_or(0));
let mut text_field: Option<Span> = None;
let mut namespace_field: Option<(Span, Type, Member)> = None;
@ -217,416 +71,187 @@ impl Compound {
fields.push(field);
}
let namespace = match meta.namespace {
None => {
if let Some((span, ..)) = namespace_field {
return Err(Error::new(
span,
"struct or enum variant must be declared with #[xml(namespace = dyn, ..)] to use a #[xml(namespace)] field."
));
}
None
}
Some(NamespaceRef::Static(namespace)) => {
if let Some((span, ..)) = namespace_field {
return Err(Error::new(
span,
"struct or enum variant must be declared with #[xml(namespace = dyn, ..)] to use a #[xml(namespace)] field."
));
}
Some(CompoundNamespace::Static(namespace))
}
Some(NamespaceRef::Dyn(namespace)) => {
if let Some((_, ty, member)) = namespace_field {
Some(CompoundNamespace::Dyn {
dyn_tok: namespace,
ty,
member,
})
} else {
return Err(Error::new_spanned(
namespace,
"enum variant or struct declared with #[xml(namespace = dyn)] must have a field annotated with #[xml(namespace)]"
));
}
}
Some(NamespaceRef::Super(ns)) => Some(CompoundNamespace::Super(ns)),
};
Ok(Self::Struct {
namespace,
name,
Ok(Self {
fields,
namespace_field,
})
}
/// Construct a [`Compound`] based on its [`XmlCompoundMeta`] and its
/// fields.
/// Construct a compound from [`syn::Fields`].
///
/// This delegates to the other constructors based on the flags inside
/// the `meta`.
pub(crate) fn new(meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
if let Some(validate) = meta.validate {
// the caller should take the validate out for a more specific error message or proper handling
return Err(syn::Error::new_spanned(
validate,
"validate is not allowed here",
));
}
if let Some(prepare) = meta.prepare {
// the caller should take the validate out for a more specific error message or proper handling
return Err(syn::Error::new_spanned(
prepare,
"prepare is not allowed here",
));
}
if let Flag::Present(fallback) = meta.fallback {
// the caller should take the fallback out for a more specific error message or proper handling
return Err(syn::Error::new_spanned(
fallback,
"fallback is not allowed here",
));
}
if let Flag::Present(exhaustive) = meta.exhaustive {
// the caller should take the exhaustive out for a more specific error message or proper handling
return Err(syn::Error::new_spanned(
exhaustive,
"exhaustive is not allowed here",
));
}
if meta.transparent.is_set() {
Self::new_transparent(meta, fields)
} else {
Self::new_struct(
meta,
fields.iter().enumerate().map(|(i, field)| {
FieldDef::from_field(field, i.try_into().expect("too many fields"))
}),
)
}
/// This a convenience wrapper around [`Self::new`], converting the
/// [`syn::Field`] structs to [`FieldDef`].
pub(crate) fn from_fields(fields: &Fields) -> Result<Self> {
Self::new(
fields.iter().enumerate().map(|(i, field)| {
FieldDef::from_field(field, i.try_into().expect("too many fields"))
}),
)
}
/// Number of fields, if this is a [`Self::Struct`] variant.
pub(crate) fn field_count(&self) -> Option<usize> {
match self {
Self::Transparent { .. } => None,
Self::Struct { fields, .. } => Some(fields.len()),
}
/// Obtain references to the information about the
/// `#[xml(namespace)]`-annotated field, if this compound has one.
pub(crate) fn namespace_field(&self) -> Option<(Span, &Type, &Member)> {
self.namespace_field.as_ref().map(|(a, b, c)| (*a, b, c))
}
/// Indicate whether this compound, when used as an enum variant, may use
/// a different namespace than the enum.
///
/// This is used by the enum implementation to decide whether it can be
/// `exhaustive` or contain a `fallback` variant.
pub(crate) fn may_not_be_enum_namespaced(&self) -> bool {
match self {
Self::Transparent { .. } => true,
Self::Struct { namespace, .. } => match namespace {
None => false,
Some(_) => true,
},
}
/// Number of fields.
pub(crate) fn field_count(&self) -> usize {
self.fields.len()
}
/// Combine `a` and `b` to obtain a valid `CompoundNamespace` or return an
/// error.
/// Construct a token stream which contains an expression which parses
/// the contents `minidom::Element` at `residual` into the compound.
///
/// The `name` argument is used to produce a nice error message.
fn need_namespace<'x>(
name: &ParentRef,
a: Option<&'x CompoundNamespace>,
b: Option<&'x CompoundNamespace>,
) -> Result<&'x CompoundNamespace> {
if let Some(namespace) = a.or(b) {
Ok(namespace)
} else {
Err(Error::new(name.span(), format!("cannot determine namespace for struct or enum variant {}. use #[xml(namespace = ..)] on the variant or enum.", name)))
}
}
/// Construct a token stream containing an expression to parse this
/// compound from a `minidom::Element` in `#residual`.
/// - `container_name` is used both for error messages and to construct
/// the resulting compound. If it directly refers to a path, that path
/// is used as constructor. Otherwise, the compound is constructed as
/// tuple.
///
/// The expression consumes the `#residual` and returns a
/// `Result<T, minidom::Element>`.
/// - `container_namespace_expr` must be an expression which evaluates to
/// the parsed namespace of the parent element. If
/// [`Self::namespace_field`] is not `None`, this must be an expression
/// which can be moved out of and which matches the type, as that
/// expression will be used to initialize that field.
///
/// If `name` is a l`ParentRef::Named`], the `Path` inside that is used as
/// a type constructor and the fields from the compound are fed into it.
/// The result type `T` is thus the type identified by the path in `name`.
/// In all other cases, this expression only needs to be usable in a
/// `std::cmp::PartialEq<str>` context.
///
/// Otherwise, `T` is a tuple of the fields inside the compound in
/// declaration order.
///
/// If the compound's namespace is `super`, the `parent_namespace_expr`
/// must be an expression which can be evaluated in order to obtain a
/// value implementing `::xso::DynNamespaceEnum`. That value
/// must reflect the parent element's namespace, as it is used to check if
/// the element at hand is in the parent's namespace.
/// - `residual` must be the identifier at which the element is found.
pub(crate) fn build_try_from_element(
self,
name: &ParentRef,
item_namespace: Option<&CompoundNamespace>,
container_name: &ParentRef,
container_namespace_expr: &Expr,
residual: &Ident,
parent_namespace_expr: Option<&Expr>,
) -> Result<TokenStream> {
match self {
Self::Transparent { ty } => Ok(quote! {
match <#ty as ::xso::FromXml>::from_tree(#residual) {
Ok(v) => Ok(Self(v)),
Err(::xso::error::Error::TypeMismatch(_, _, e)) => Err(e),
Err(other) => return Err(other),
}
}),
Self::Struct {
namespace: xml_namespace,
name: xml_name,
fields,
} => {
let namespace = Self::need_namespace(name, xml_namespace.as_ref(), item_namespace)?;
let mut init = quote! {};
let mut tupinit = quote! {};
let mut attrcheck = quote! {};
let mut tempinit = quote! {};
let mut childiter = quote! {};
let mut childfallback = quote! {
return Err(::xso::error::Error::ParseError(concat!("Unknown child in ", #xml_name, " element.")));
};
let namespace_tempname = quote::format_ident!("__namespace_field");
let namespace_expr = match namespace {
CompoundNamespace::Static(ns) => Some(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: ns.clone().into(),
})),
CompoundNamespace::Dyn { .. } => Some(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: namespace_tempname.clone().into(),
})),
CompoundNamespace::Super(_) => parent_namespace_expr.cloned(),
};
let mut had_fallback: bool = false;
for field in fields {
let ident = field.ident.clone();
let FieldParsePart {
tempinit: field_tempinit,
childiter: field_childiter,
attrcheck: field_attrcheck,
value,
childfallback: field_childfallback,
} = field.build_try_from_element(
name,
&namespace_tempname,
namespace_expr.as_ref(),
)?;
attrcheck = quote! { #attrcheck #field_attrcheck };
tempinit = quote! { #tempinit #field_tempinit };
childiter = quote! { #childiter #field_childiter };
if let Some(field_childfallback) = field_childfallback {
if had_fallback {
panic!("internal error: multiple fields attempting to collect all child elements.");
}
had_fallback = true;
childfallback = field_childfallback;
}
init = quote! {
#init
#ident: #value,
};
tupinit = quote! {
#tupinit
#value,
};
}
let construct = match name {
ParentRef::Named(ref path) => quote! {
#path {
#init
}
},
ParentRef::Unnamed { .. } => quote! {
( #tupinit )
},
};
let body = quote! {
for (key, _) in #residual.attrs() {
#attrcheck
return Err(::xso::error::Error::ParseError(concat!("Unknown attribute in ", #xml_name, " element.")));
}
#tempinit
for mut residual in #residual.take_contents_as_children() {
#childiter
#childfallback
}
Ok(#construct)
};
match namespace {
CompoundNamespace::Static(ns) => Ok(quote! {
if #residual.is(#xml_name, #ns) {
#body
} else {
Err(#residual)
}
}),
CompoundNamespace::Dyn { ty, .. } => Ok(quote! {
match <#ty as ::xso::DynNamespaceEnum>::from_xml_text(&#residual.ns()) {
Ok(#namespace_tempname) => if #residual.name() == #xml_name {
#body
} else {
Err(#residual)
}
Err(::xso::error::DynNamespaceError::Invalid) => {
return Err(::xso::error::Error::ParseError(
"Invalid namespace"
));
}
Err(::xso::error::DynNamespaceError::Mismatch) => Err(#residual),
}
}),
CompoundNamespace::Super(ns) => match parent_namespace_expr {
Some(expr) => Ok(quote! {
if ::std::cmp::PartialEq::eq(&#expr, #residual.ns().as_str()) && #residual.name() == #xml_name {
#body
} else {
Err(#residual)
}
}),
None => {
return Err(Error::new_spanned(
ns,
"#[xml(namespace = super)] cannot be used here.",
))
}
},
let readable_name = container_name.to_string();
let mut init = quote! {};
let mut tupinit = quote! {};
let mut attrcheck = quote! {};
let mut tempinit = quote! {};
let mut childiter = quote! {};
let mut childfallback = quote! {
return Err(::xso::error::Error::ParseError(concat!("Unknown child in ", #readable_name, ".")));
};
let mut had_fallback: bool = false;
for field in self.fields {
let ident = field.ident.clone();
let FieldParsePart {
tempinit: field_tempinit,
childiter: field_childiter,
attrcheck: field_attrcheck,
value,
childfallback: field_childfallback,
} = field.build_try_from_element(container_name, container_namespace_expr)?;
attrcheck = quote! { #attrcheck #field_attrcheck };
tempinit = quote! { #tempinit #field_tempinit };
childiter = quote! { #childiter #field_childiter };
if let Some(field_childfallback) = field_childfallback {
if had_fallback {
panic!(
"internal error: multiple fields attempting to collect all child elements."
);
}
had_fallback = true;
childfallback = field_childfallback;
}
init = quote! {
#init
#ident: #value,
};
tupinit = quote! {
#tupinit
#value,
};
}
let construct = match container_name {
ParentRef::Named(ref path) => quote! {
#path {
#init
}
},
ParentRef::Unnamed { .. } => quote! {
( #tupinit )
},
};
Ok(quote! {
{
for (key, _) in #residual.attrs() {
#attrcheck
return Err(::xso::error::Error::ParseError(concat!("Unknown attribute in ", #readable_name, ".")));
}
#tempinit
for mut residual in #residual.take_contents_as_children() {
#childiter
#childfallback
}
#construct
}
})
}
/// Construct an expression evaluating to a `minidom::Element` containing
/// all data from the compound.
/// Construct an expression consuming the `builder` and returning it,
/// filled with the data from the compound.
///
/// The compound is accessed through `access_field`: That function will
/// be called for each field and the expression is used exactly once to
/// obtain the data of the field inside the compound.
/// - `container_name` is used for error messages.
///
/// This indirection is necessary to be able to handle both structs (where
/// fields are accessed as struct members) and enumeration variants
/// (where fields are bound to local names).
/// - `container_namespace_expr` must be an expression which evaluates to
/// the parsed namespace of the parent element. This needs to implement
/// `::xso::DynNamespaceEnum` if any fields use
/// `#[xml(namespace = super)]`.
///
/// If the compound has no namespace assigned (e.g. enum variants), the
/// `item_namespace` must be `Some`; otherwise, a compile-time error is
/// generated pointing out that enum variants must have a namespace set
/// if their enum has no namespace set.
/// - `builder` must be an expression which can be moved out from and
/// which is the `minidom::Builder` into which the element should be
/// constructed.
///
/// If the compound's namespace is `super`, the `parent_namespace_expr`
/// must be an expression which can be evaluated in order to obtain a
/// value implementing `::xso::DynNamespaceEnum`. That value
/// must reflect the parent element's namespace, as it is used to obtain
/// the namespace of the compound's own element.
/// - `access_field`: Accessor function for fields within the compound.
/// That function will be called for each field and the expression is
/// used exactly once to obtain the data of the field inside the
/// compound.
///
/// This indirection is necessary to be able to handle both structs
/// (where fields are accessed as struct members) and enumeration
/// variants (where fields are bound to local names).
///
/// Note that the field referenced by [`Self::namespace_field`] is not
/// accessed by this function.
pub(crate) fn build_into_element(
self,
item_name: &ParentRef,
item_namespace: Option<&CompoundNamespace>,
container_name: &ParentRef,
container_namespace_expr: &Expr,
builder: &Ident,
mut access_field: impl FnMut(Member) -> Expr,
parent_namespace_expr: Option<&Expr>,
) -> Result<TokenStream> {
match self {
Self::Transparent { ty: _ } => {
let ident = access_field(Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
}));
Ok(quote! {
::xso::exports::minidom::Element::from(#ident)
})
}
Self::Struct {
namespace,
name,
fields,
} => {
let xml_name = name;
let (builder_init, namespace_expr) = match Self::need_namespace(
item_name,
namespace.as_ref(),
item_namespace,
)? {
CompoundNamespace::Static(xml_namespace) => (
quote! {
::xso::exports::minidom::Element::builder(#xml_name, #xml_namespace)
},
Some(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: xml_namespace.clone(),
})),
),
CompoundNamespace::Dyn { member, ty, .. } => {
let ident = access_field(member.clone());
(
quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
<#ty as ::xso::DynNamespaceEnum>::into_xml_text(#ident.clone()),
)
},
Some(syn::parse2(quote! { #ident.clone() })?),
)
}
CompoundNamespace::Super(ns) => match parent_namespace_expr {
Some(expr) => (
quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
::xso::DynNamespaceEnum::into_xml_text(#expr.clone()),
)
},
Some(expr.clone()),
),
None => {
return Err(Error::new_spanned(
ns,
"#[xml(namespace = super)] cannot be used here.",
))
}
},
};
let mut build = quote! {};
for field in fields {
let field_build = field.build_into_element(
item_name,
&mut access_field,
namespace_expr.as_ref(),
)?;
build = quote! {
#build
builder = #field_build;
};
}
Ok(quote! {
{
let mut builder = #builder_init;
#build
builder.build()
}
})
}
let mut build = quote! {};
for field in self.fields {
let field_build = field.build_into_element(
container_name,
container_namespace_expr,
&mut access_field,
)?;
build = quote! {
#build
builder = #field_build;
};
}
Ok(quote! {
{
let mut builder = #builder;
#build
builder
}
})
}
/// Return an iterator which returns the [`syn::Member`] structs to access
@ -635,17 +260,16 @@ impl Compound {
/// For tuple-like compounds that's basically counting up from 0, for
/// named compounds this emits the field names in declaration order.
pub(crate) fn iter_members(&self) -> Box<dyn Iterator<Item = Member> + '_> {
match self {
Self::Transparent { .. } => Box::new(
[Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
})]
.into_iter(),
),
Self::Struct { ref fields, .. } => {
Box::new(fields.iter().map(|x| x.ident.clone().into()))
}
Box::new(self.fields.iter().map(|x| x.ident.clone().into()))
}
/// If and only if this compound has exactly one field, return a reference
/// to that field's type.
pub(crate) fn single_type(&self) -> Option<&Type> {
if self.fields.len() != 1 {
None
} else {
Some(&self.fields[0].ty)
}
}
}

View File

@ -7,18 +7,57 @@ this crate on `enum` items.
It is thus the counterpart to [`crate::structs`].
*/
use proc_macro2::Span;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::*;
use crate::common::bake_generics;
use crate::compound::{Compound, CompoundNamespace};
use crate::meta::{Flag, NamespaceRef, XmlCompoundMeta};
use crate::common::{bake_generics, build_prepare, build_validate};
use crate::compound::Compound;
use crate::meta::{Flag, Name, NamespaceRef, StaticNamespace, XmlCompoundMeta};
use crate::structs::StructInner;
/// Represent a single enum variant.
/// Build the necessary objects to rebind all enum fields to collision-free
/// identifiers.
///
/// Takes an iterator over members of the enum's compound and returns, in
/// order:
///
/// - A vector of these identifiers.
/// - A vector with expressions containing the rebound idenifiers.
/// - A function which maps the original identifiers to their rebound
/// versions.
fn build_ident_mapping<I: Iterator<Item = Member>>(
refs: I,
) -> (Vec<Member>, Vec<Expr>, Box<dyn FnMut(Member) -> Expr>) {
let map_ident = |ident: Member| -> Expr {
Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Path {
leading_colon: None,
segments: [PathSegment::from(quote::format_ident!("__field_{}", ident))]
.into_iter()
.collect(),
},
})
};
let mut orig_names = Vec::with_capacity(refs.size_hint().1.unwrap_or(0));
let mut mapped_names = Vec::with_capacity(orig_names.capacity());
for field_ref in refs {
orig_names.push(field_ref.clone());
mapped_names.push(map_ident(field_ref));
}
(orig_names, mapped_names, Box::new(map_ident))
}
/// Variant of an enum which switches on the XML element's name.
#[cfg_attr(feature = "debug", derive(Debug))]
struct EnumVariant {
struct XmlNameVariant {
/// The XML name to match against.
xml_name: Name,
/// The identifier of the enum variant.
ident: Ident,
@ -29,92 +68,132 @@ struct EnumVariant {
inner: Compound,
}
impl EnumVariant {
/// Construct an enum variant definition.
///
/// `meta` must be the processed `#[xml(..)]` attribute on the variant.
/// `ident` must be variant's identifier and `fields` must be the
/// variant's fields.
///
/// This constructor does additional checks on top of the checks done by
/// [`crate::compound::Compound::new`].
///
/// - `validate`, `prepare` and `exhaustive` are rejected.
/// - `fallback` is rejected if the variant is not a unit variant.
fn new(mut meta: XmlCompoundMeta, ident: Ident, fields: &Fields) -> Result<Self> {
if let Some(validate) = meta.validate.take() {
return Err(syn::Error::new_spanned(
validate,
"validate is not allowed on enum variants (but on enums)",
impl XmlNameVariant {
/// Parse a [`syn::Variant`] as XML name switched variant.
fn new(variant: &Variant) -> Result<Self> {
let meta = XmlCompoundMeta::parse_from_attributes(&variant.attrs)?;
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"`namespace` not allowed on enum variants.",
));
}
if let Some(prepare) = meta.prepare.take() {
return Err(syn::Error::new_spanned(
prepare,
"prepare is not allowed on enum variants (but on enums)",
let name = if let Some(name) = meta.name {
name.into()
} else {
return Err(Error::new(
meta.span,
"`name` is required on enum variants in enums with a fixed namespace.",
));
};
let fallback = meta.fallback;
if let Flag::Present(transparent) = meta.transparent {
return Err(Error::new(
transparent,
"`transparent` not allowed on enum variants in enums with a fixed namespace.",
));
}
if let Flag::Present(exhaustive) = meta.exhaustive.take() {
return Err(syn::Error::new_spanned(
if let Flag::Present(exhaustive) = meta.exhaustive {
return Err(Error::new(
exhaustive,
"exhaustive is not allowed on enum variants (but on enums)",
"`exhaustive` not allowed on enum variants.",
));
}
let fallback = meta.fallback.take();
let inner = Compound::new(meta, fields)?;
if let Flag::Present(fallback) = &fallback {
match fields {
Fields::Unit => (),
_ => {
return Err(syn::Error::new_spanned(
fallback,
"fallback is only allowed on unit variants",
))
}
}
if let Some(validate) = meta.validate {
return Err(Error::new_spanned(
validate,
"`validate` not allowed on enum variants.",
));
}
if let Some(prepare) = meta.prepare {
return Err(Error::new_spanned(
prepare,
"`validate` not allowed on enum variants.",
));
}
if let Flag::Present(debug) = meta.debug {
return Err(Error::new(debug, "`debug` not allowed on enum variants."));
}
Ok(Self {
xml_name: name,
ident: variant.ident.clone(),
fallback,
ident,
inner,
inner: Compound::from_fields(&variant.fields)?,
})
}
}
/// Represent an enum.
/// Variant of an enum where each variant has completely independent matching.
#[cfg_attr(feature = "debug", derive(Debug))]
struct EnumDef {
/// The `validate` if set on the enum.
///
/// This is called after the enum has been otherwise parsed successfully
/// with the enum value as mutable reference as only argument. It is
/// expected to return `Result<(), Error>`, the `Err(..)` variant of which
/// is forwarded correctly.
validate: Option<Path>,
struct DynamicVariant {
/// The identifier of the enum variant.
ident: Ident,
/// The `prepare` if set on the enum.
/// The enum variant's definition.
///
/// This is called before the enum will be converted back into an XML
/// element with the enum value as mutable reference as only argument.
prepare: Option<Path>,
/// The implementation of [`StructInner`] can be reused completely, as the
/// variant has to cover all of its matching logic.
inner: StructInner,
}
/// The XML namespace of the enum, if any was set inside the `#[xml(..)]`
/// meta.
///
/// In contrast to structs, it is not mandatory to specify a namespace on
/// enumerations if and only if all their variants are transparent or have
/// a namespace specified.
///
/// The constructor verifies this.
namespace: Option<CompoundNamespace>,
impl DynamicVariant {
/// Parse a [`syn::Variant`] as dynamic enum variant.
fn new(variant: &Variant) -> Result<Self> {
let ident = variant.ident.clone();
let mut meta = XmlCompoundMeta::parse_from_attributes(&variant.attrs)?;
if let Flag::Present(fallback) = meta.fallback.take() {
return Err(Error::new(
fallback,
"`fallback` cannot be used in dynamic enums.",
));
}
/// The enum variants.
variants: Vec<EnumVariant>,
if let Flag::Present(exhaustive) = meta.exhaustive.take() {
return Err(Error::new(
exhaustive,
"`exhaustive` not allowed on enum variants.",
));
}
/// The exhaustive flag if set on the enum.
if let Some(validate) = meta.validate.take() {
return Err(Error::new_spanned(
validate,
"`validate` not allowed on enum variants.",
));
}
if let Some(prepare) = meta.prepare.take() {
return Err(Error::new_spanned(
prepare,
"`validate` not allowed on enum variants.",
));
}
if let Flag::Present(debug) = meta.debug.take() {
return Err(Error::new(debug, "`debug` not allowed on enum variants."));
}
let inner = StructInner::new(meta, &variant.fields)?;
Ok(Self { ident, inner })
}
}
/// An enum which switches on the XML element's name, with a fixed namespace.
#[cfg_attr(feature = "debug", derive(Debug))]
struct XmlNameSwitched {
/// The exhaustive flag, if set on the enum.
///
/// If this flag is set, the enum considers itself authoritative for its
/// namespace, and [`Self::namespace`] must in fact not be `None`.
/// namespace.
///
/// Then, if an XML element matching the namespace but none of the
/// variants is encountered, a hard parse error with an appropriate error
@ -123,138 +202,87 @@ struct EnumDef {
///
/// This can be useful in some circumstances.
exhaustive: Flag,
/// The namespace the enum's XML element resides in.
namespace: StaticNamespace,
/// The enum's variants.
variants: Vec<XmlNameVariant>,
}
impl EnumDef {
/// Construct a new enum from its `#[xml(..)]` attribute and the variants.
impl XmlNameSwitched {
/// Construct a new XML name switched enum from parts.
///
/// The constructor validates all arguments extensively:
/// - Only one `fallback` variant is accepted.
/// - A `fallback` variant cannot be combined with an `exhaustive` enum.
/// - `fallback` or `exhaustive` can only be used if `namespace` is
/// set on the enum and no variant specifies its own namespace.
/// - At least one variant must exist.
/// - `exhaustive` must be the exhaustive flag (see [`Self::exhaustive`]
/// for details).
///
/// - `namespace` must be the XML namespace of the enum.
///
/// - `input` must be an iterator emitting borrowed [`syn::Variant`]
/// structs to process.
fn new<'x, I: Iterator<Item = &'x Variant>>(
mut meta: XmlCompoundMeta,
exhaustive: Flag,
namespace: StaticNamespace,
input: I,
) -> Result<Self> {
if let Flag::Present(fallback) = meta.fallback.take() {
return Err(syn::Error::new_spanned(
fallback,
"fallback is not allowed on enums (only on enum variants)",
));
}
let namespace = match meta.namespace {
None => None,
Some(NamespaceRef::Static(namespace)) => Some(CompoundNamespace::Static(namespace)),
Some(NamespaceRef::Dyn(namespace)) => {
return Err(syn::Error::new_spanned(
namespace,
"namespace = dyn can only be used on enum variants, not on enums.",
))
}
Some(NamespaceRef::Super(namespace)) => {
return Err(syn::Error::new_spanned(
namespace,
"only fields can refer to the parent namespace",
))
}
};
let mut variants = Vec::with_capacity(input.size_hint().1.unwrap_or(0));
let mut had_fallback = false;
let mut non_enum_namespaced: Option<Span> = None;
for variant in input {
let meta = XmlCompoundMeta::parse_from_attributes(&variant.attrs)?;
let span = meta.span;
let variant = EnumVariant::new(meta, variant.ident.clone(), &variant.fields)?;
if had_fallback {
if let Flag::Present(fallback) = &variant.fallback {
return Err(syn::Error::new_spanned(
let variant = XmlNameVariant::new(variant)?;
if let Flag::Present(fallback) = variant.fallback {
if had_fallback {
return Err(syn::Error::new(
fallback,
"only one variant may be a fallback variant",
));
}
had_fallback = true;
}
if variant.inner.may_not_be_enum_namespaced() {
non_enum_namespaced = Some(span);
}
variants.push(variant);
}
if had_fallback {
if let Some(non_enum_namespaced) = non_enum_namespaced {
return Err(syn::Error::new(non_enum_namespaced, "transparent variants or variants with explicit #[xml(namespace = ..)] can not be combined with fallback variants"));
}
match namespace {
Some(CompoundNamespace::Static(_)) => (),
None => return Err(syn::Error::new(meta.span, "fallback variants can only be used on enums with a static namespace set. use #[xml(namespace = ..)] on the enum itself.")),
Some(CompoundNamespace::Super(_)) => unreachable!(),
Some(CompoundNamespace::Dyn { dyn_tok, .. })=> return Err(syn::Error::new_spanned(dyn_tok, "fallback variants cannot be combined with dynamic namespaces")),
}
}
if let Flag::Present(ref exhaustive) = meta.exhaustive {
if let Flag::Present(exhaustive) = exhaustive {
if had_fallback {
return Err(syn::Error::new_spanned(exhaustive, "exhaustive cannot be sensibly combined with a fallback variant. choose one or the other."));
}
match namespace {
Some(CompoundNamespace::Static(_)) => (),
None => return Err(syn::Error::new_spanned(exhaustive, "exhaustive enums must have a static namespace set. use #[xml(namespace = ..)] on the enum itself.")),
Some(CompoundNamespace::Super(_)) => unreachable!(),
Some(CompoundNamespace::Dyn{ dyn_tok, .. }) => return Err(syn::Error::new_spanned(dyn_tok, "exhaustive enums cannot be combined with dynamic namespaces")),
return Err(syn::Error::new(exhaustive, "exhaustive cannot be sensibly combined with a fallback variant. choose one or the other."));
}
}
if variants.len() == 0 {
return Err(syn::Error::new(
meta.span,
"empty enumerations are not supported",
));
}
Ok(Self {
validate: meta.validate,
prepare: meta.prepare,
exhaustive,
namespace,
variants,
exhaustive: meta.exhaustive,
})
}
/// Construct the entire implementation of
/// `TryFrom<minidom::Element>::try_from`.
/// Construct an expression which consumes the `minidom::Element` at
/// `residual` and returns a `Result<T, Error>`.
///
/// - `enum_ident` must be the identifier of the enum's type.
/// - `validate` must be a statement which takes a mutable reference to
/// the identifier `result` and `return`'s with an error if it is not
/// acceptable.
/// - `residual` must be the identifier at which the element is found.
fn build_try_from_element(
self,
enum_ident: &Ident,
validate: Stmt,
residual: &Ident,
) -> Result<proc_macro2::TokenStream> {
) -> Result<TokenStream> {
let xml_namespace = self.namespace;
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: xml_namespace.clone(),
});
let validate = if let Some(validate) = self.validate {
quote! {
#validate(&mut result)?;
}
} else {
quote! {
{ let _ = &mut result; };
}
let mut fallback = quote! {
_ => return Err(::xso::error::Error::TypeMismatch("", "", #residual)),
};
let mut fallback = quote! {};
let mut iter = quote! {};
for variant in self.variants {
let ident = variant.ident;
if variant.fallback.is_set() {
let Some(CompoundNamespace::Static(ns)) = &xml_namespace else {
panic!("invariant violated: non-static ns on enum with fallback variant")
};
fallback = quote! {
if #residual.has_ns(#ns) {
let mut result = Self::#ident;
#validate
return Ok(result);
}
}
}
let xml_name = variant.xml_name;
let variant_impl = variant.inner.build_try_from_element(
&(Path {
@ -267,52 +295,63 @@ impl EnumDef {
.collect(),
}
.into()),
xml_namespace.as_ref(),
&namespace_expr,
residual,
None,
)?;
let variant_impl = quote! {
let mut result = #variant_impl;
#validate
Ok(result)
};
if variant.fallback.is_set() {
fallback = quote! {
_ => { #variant_impl },
}
}
iter = quote! {
#iter
let mut #residual = match #variant_impl {
Ok(mut result) => {
#validate
return Ok(result);
},
Err(residual) => residual,
};
}
#xml_name => { #variant_impl },
};
}
if self.exhaustive.is_set() {
let Some(CompoundNamespace::Static(ns)) = &xml_namespace else {
panic!("invariant violated: non-static ns on exhaustive enum");
};
// deliberately overriding fallback fields here --- the constructor
// already rejects this combination
fallback = quote! {
if #residual.has_ns(#ns) {
return Err(::xso::error::Error::ParseError(concat!("This is not a ", stringify!(#enum_ident), " element.")));
}
_ => Err(::xso::error::Error::ParseError(concat!("This is not a ", stringify!(#enum_ident), " element."))),
};
}
Ok(quote! {
#iter
#fallback
Err(::xso::error::Error::TypeMismatch("", "", #residual))
if #residual.ns() == #xml_namespace {
match #residual.name() {
#iter
#fallback
}
} else {
Err(::xso::error::Error::TypeMismatch("", "", #residual))
}
})
}
/// Construct the entire implementation of
/// `<From<T> for minidom::Element>::from`.
fn build_into_element(
self,
ty_ident: &Ident,
value_ident: &Ident,
) -> Result<proc_macro2::TokenStream> {
/// Construct a token stream which contains the arms of a `match`
/// expression dissecting the enum and building `minidom::Element` objects
/// for each variant.
///
/// `ty_ident` must be the identifier of the enum's type.
fn build_into_element(self, ty_ident: &Ident) -> Result<TokenStream> {
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: self.namespace,
});
let builder = Ident::new("builder", Span::call_site());
let mut matchers = quote! {};
for variant in self.variants {
let xml_name = variant.xml_name;
let path = Path {
leading_colon: None,
segments: [
@ -322,46 +361,325 @@ impl EnumDef {
.into_iter()
.collect(),
};
let map_ident = |ident: Member| -> Expr {
Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Path {
leading_colon: None,
segments: [PathSegment::from(quote::format_ident!("__field_{}", ident))]
.into_iter()
.collect(),
},
})
};
let refs = variant.inner.iter_members();
let mut orig_names = Vec::with_capacity(refs.size_hint().1.unwrap_or(0));
let mut mapped_names = Vec::with_capacity(orig_names.capacity());
for field_ref in refs {
orig_names.push(field_ref.clone());
mapped_names.push(map_ident(field_ref));
}
let (orig_names, mapped_names, map_ident) =
build_ident_mapping(variant.inner.iter_members());
let into_element = variant.inner.build_into_element(
&(Path::from(ty_ident.clone()).into()),
self.namespace.as_ref(),
&namespace_expr,
&builder,
map_ident,
None,
)?;
matchers = quote! {
#matchers
#path { #( #orig_names: #mapped_names ),* } => {
#into_element
let #builder = ::xso::exports::minidom::Element::builder(
#xml_name,
#namespace_expr,
);
let #builder = #into_element;
#builder.build()
},
};
}
Ok(matchers)
}
}
/// An enum where each variant has completely independent matching.
#[cfg_attr(feature = "debug", derive(Debug))]
struct Dynamic {
/// Variants of the enum.
variants: Vec<DynamicVariant>,
}
impl Dynamic {
/// Construct a dynamic enum from an iterable of variants.
fn new<'x, I: Iterator<Item = &'x Variant>>(input: I) -> Result<Self> {
let mut variants = Vec::with_capacity(input.size_hint().1.unwrap_or(0));
for variant in input {
let variant = DynamicVariant::new(variant)?;
variants.push(variant);
}
Ok(Self { variants })
}
/// Construct an expression which consumes the `minidom::Element` at
/// `residual` and returns a `Result<T, Error>`.
///
/// - `enum_ident` must be the identifier of the enum's type.
/// - `validate` must be a statement which takes a mutable reference to
/// the identifier `result` and `return`'s with an error if it is not
/// acceptable.
/// - `residual` must be the identifier at which the element is found.
fn build_try_from_element(
self,
enum_ident: &Ident,
validate: Stmt,
residual: &Ident,
) -> Result<TokenStream> {
let mut matchers = quote! {};
for variant in self.variants {
let ident = variant.ident;
let try_from_impl = variant.inner.build_try_from_element(
&(Path {
leading_colon: None,
segments: [
PathSegment::from(enum_ident.clone()),
PathSegment::from(ident.clone()),
]
.into_iter()
.collect(),
}
.into()),
residual,
)?;
matchers = quote! {
#matchers
let mut #residual = match #try_from_impl {
Ok(mut result) => {
#validate
return Ok(result);
},
Err(#residual) => #residual,
};
}
}
Ok(quote! {
{
#matchers
Err(::xso::error::Error::TypeMismatch("", "", #residual))
}
})
}
/// Construct a token stream which contains the arms of a `match`
/// expression dissecting the enum and building `minidom::Element` objects
/// for each variant.
///
/// `ty_ident` must be the identifier of the enum's type.
fn build_into_element(self, enum_ident: &Ident) -> Result<TokenStream> {
let mut matchers = quote! {};
for variant in self.variants {
let ident = variant.ident;
let path = Path {
leading_colon: None,
segments: [
PathSegment::from(enum_ident.clone()),
PathSegment::from(ident.clone()),
]
.into_iter()
.collect(),
};
let (orig_names, mapped_names, map_ident) =
build_ident_mapping(variant.inner.iter_members());
let into_impl = variant
.inner
.build_into_element(&(path.into()), map_ident)?;
matchers = quote! {
#matchers
#enum_ident::#ident { #( #orig_names: #mapped_names ),* } => { #into_impl },
}
}
Ok(matchers)
}
}
/// Inner part of [`EnumDef`], supporting the different styles of
/// enumerations.
#[cfg_attr(feature = "debug", derive(Debug))]
enum EnumInner {
/// Enum item where the variants switch on the XML element's name.
XmlNameSwitched(XmlNameSwitched),
/// Enum item which has no matcher on the enum itself, where each variant
/// may be an entirely different XML element.
Dynamic(Dynamic),
}
impl EnumInner {
/// Construct an expression which consumes the `minidom::Element` at
/// `residual` and returns a `Result<T, Error>`.
///
/// - `enum_ident` must be the identifier of the enum's type.
/// - `validate` must be a statement which takes a mutable reference to
/// the identifier `result` and `return`'s with an error if it is not
/// acceptable.
/// - `residual` must be the identifier at which the element is found.
fn build_try_from_element(
self,
enum_ident: &Ident,
validate: Stmt,
residual: &Ident,
) -> Result<TokenStream> {
match self {
Self::XmlNameSwitched(inner) => {
inner.build_try_from_element(enum_ident, validate, residual)
}
Self::Dynamic(inner) => inner.build_try_from_element(enum_ident, validate, residual),
}
}
/// Construct a token stream which contains the arms of a `match`
/// expression dissecting the enum and building `minidom::Element` objects
/// for each variant.
///
/// `ty_ident` must be the identifier of the enum's type.
fn build_into_element(self, ty_ident: &Ident) -> Result<TokenStream> {
match self {
Self::XmlNameSwitched(inner) => inner.build_into_element(ty_ident),
Self::Dynamic(inner) => inner.build_into_element(ty_ident),
}
}
}
/// Represent an enum.
#[cfg_attr(feature = "debug", derive(Debug))]
struct EnumDef {
/// The `validate` value, if set on the enum.
///
/// This is called after the enum has been otherwise parsed successfully
/// with the enum value as mutable reference as only argument. It is
/// expected to return `Result<(), Error>`, the `Err(..)` variant of which
/// is forwarded correctly.
validate: Option<Path>,
/// The `prepare` value, if set on the enum.
///
/// This is called before the enum will be converted back into an XML
/// element with the enum value as mutable reference as only argument.
prepare: Option<Path>,
/// The actual contents of the enum.
inner: EnumInner,
/// The `debug` flag, if set on the enum.
#[cfg_attr(not(feature = "debug"), allow(dead_code))]
debug: Flag,
}
impl EnumDef {
/// Construct a new enum from its `#[xml(..)]` attribute and the variants.
fn new<'x, I: Iterator<Item = &'x Variant>>(meta: XmlCompoundMeta, input: I) -> Result<Self> {
let prepare = meta.prepare;
let validate = meta.validate;
let debug = meta.debug;
if let Flag::Present(fallback) = meta.fallback {
return Err(syn::Error::new(
fallback,
"`fallback` is not allowed on enums (only on enum variants)",
));
}
if let Flag::Present(transparent) = meta.transparent {
return Err(syn::Error::new(
transparent,
"`transparent` cannot be set set on enums (only on enum variants)",
));
}
let namespace = match meta.namespace {
None => {
// no namespace -> must be dynamic variant. ensure the other
// stuff isn't there.
if let Some(name) = meta.name {
return Err(syn::Error::new_spanned(
name,
"`name` cannot be set without `namespace` or on dynamic enums",
));
}
if let Flag::Present(exhaustive) = meta.exhaustive {
return Err(syn::Error::new(
exhaustive,
"`transparent` cannot be set set on dynamic enums",
));
}
return Ok(Self {
validate,
prepare,
debug,
inner: EnumInner::Dynamic(Dynamic::new(input)?),
});
}
Some(NamespaceRef::Static(ns)) => ns,
Some(NamespaceRef::Dyn(ns)) => {
return Err(syn::Error::new_spanned(
ns,
"`#[xml(namespace = dyn)]` cannot be used on enums.",
))
}
Some(NamespaceRef::Super(ns)) => {
return Err(syn::Error::new_spanned(
ns,
"`#[xml(namespace = super)]` cannot be used on enums.",
))
}
};
if let Some(name) = meta.name {
return Err(syn::Error::new_spanned(
name,
"`name` cannot be set on enums (only on variants).",
));
}
let exhaustive = meta.exhaustive;
Ok(Self {
prepare,
validate,
debug,
inner: EnumInner::XmlNameSwitched(XmlNameSwitched::new(exhaustive, namespace, input)?),
})
}
/// Construct the entire implementation of
/// `TryFrom<minidom::Element>::try_from`.
fn build_try_from_element(self, enum_ident: &Ident, residual: &Ident) -> Result<TokenStream> {
let validate = build_validate(self.validate);
let result = self
.inner
.build_try_from_element(enum_ident, validate, residual)?;
#[cfg(feature = "debug")]
if self.debug.is_set() {
println!("{}", result);
}
Ok(result)
}
/// Construct the entire implementation of
/// `<From<T> for minidom::Element>::from`.
fn build_into_element(self, ty_ident: &Ident, value_ident: &Ident) -> Result<TokenStream> {
let prepare = build_prepare(self.prepare, value_ident);
let matchers = self.inner.build_into_element(ty_ident)?;
let result = quote! {
#prepare
match #value_ident {
#matchers
}
})
};
#[cfg(feature = "debug")]
if self.debug.is_set() {
println!("{}", result);
}
Ok(result)
}
}

View File

@ -18,8 +18,7 @@ pub(super) enum ParentRef {
/// The parent is not addressable.
///
/// This is typically the case for compounds created for
/// [`crate::field::child::ExtractDef`].
/// This is typically the case for compounds created for `ExtractDef`.
Unnamed {
/// The parent's ref.
///
@ -63,6 +62,7 @@ impl ParentRef {
///
/// This points at the closest [`Self::Named`] variant in the parent
/// chain.
#[allow(dead_code)]
pub(crate) fn span(&self) -> Span {
match self {
Self::Named(p) => p.span(),

View File

@ -7,7 +7,7 @@ use syn::*;
use crate::compound::Compound;
use crate::error_message::{self, ParentRef};
use crate::meta::{ExtractMeta, Flag, NameRef, NamespaceRef, XmlCompoundMeta};
use crate::meta::{ExtractMeta, Flag, NameRef, NamespaceRef};
use super::{ChildMode, FieldDef, FieldNamespace, FieldParsePart};
@ -17,6 +17,10 @@ use super::{ChildMode, FieldDef, FieldNamespace, FieldParsePart};
/// `#[xml(child(.., extract(..))]` or `#[xml(children(.., extract(..)))]`.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(super) struct ExtractDef {
namespace: FieldNamespace,
name: NameRef,
/// Compound which contains the arguments of the `extract(..)` attribute,
/// transformed into a struct with unnamed fields.
///
@ -29,6 +33,17 @@ pub(super) struct ExtractDef {
}
impl ExtractDef {
fn expand_namespace(&self, container_namespace_expr: &Expr) -> Expr {
match self.namespace {
FieldNamespace::Static(ref ns) => Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: ns.clone().into(),
}),
FieldNamespace::Super(_) => container_namespace_expr.clone(),
}
}
/// Construct an `ExtractDef`.
///
/// The `namespace` and `name` identify the XML element this `ExtractDef`
@ -49,26 +64,17 @@ impl ExtractDef {
parts: Vec<ExtractMeta>,
mut single_extract_type: Option<Type>,
) -> Result<Self> {
let meta = XmlCompoundMeta {
span: span.clone(),
namespace: Some(namespace.clone().into()),
name: Some(name.clone()),
fallback: Flag::Absent,
transparent: Flag::Absent,
exhaustive: Flag::Absent,
validate: None,
prepare: None,
};
if parts.len() != 1 {
single_extract_type = None;
}
let parts = Compound::new_struct(
meta,
parts.into_iter().enumerate().map(|(i, x)| {
FieldDef::from_extract(span.clone(), x, i as u32, single_extract_type.take())
}),
)?;
Ok(Self { parts })
let parts = Compound::new(parts.into_iter().enumerate().map(|(i, x)| {
FieldDef::from_extract(span.clone(), x, i as u32, single_extract_type.take())
}))?;
Ok(Self {
namespace,
name,
parts,
})
}
/// Construct a token stream containing an expression which tries to
@ -91,13 +97,14 @@ impl ExtractDef {
/// passed on to the inner call to [`Compound::build_into_element`].
fn build_extract(
self,
name: &ParentRef,
container_name: &ParentRef,
container_namespace_expr: &Expr,
residual: &Ident,
parent_namespace_expr: Option<&Expr>,
) -> Result<TokenStream> {
let nfields = self.parts.field_count().unwrap();
let namespace_expr = self.expand_namespace(container_namespace_expr);
let xml_name = self.name;
let ns_ident = Ident::new("__extract_namespace", Span::call_site());
let nfields = self.parts.field_count();
let repack = if nfields == 1 {
quote! { data.0.into() }
@ -105,38 +112,25 @@ impl ExtractDef {
quote! { data }
};
// we reassign and wrap the parent namespace here in order to avoid
// any issues with (not yet implemented) nested extractions.
let (ns_rename, parent_namespace_expr) =
if let Some(parent_namespace_expr) = parent_namespace_expr {
(
quote! {
let #ns_ident = (#parent_namespace_expr).clone();
},
Some(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: ns_ident.into(),
})),
)
} else {
(quote! {}, None)
};
let parse = self
.parts
.build_try_from_element(container_name, &namespace_expr, residual)?;
let build = self.parts.build_try_from_element(
name,
None,
residual,
parent_namespace_expr.as_ref(),
)?;
let test_expr = match self.namespace {
FieldNamespace::Static(xml_namespace) => quote! {
#residual.is(#xml_name, #xml_namespace)
},
FieldNamespace::Super(_) => quote! {
::std::cmp::PartialEq::eq(&#namespace_expr, #residual.ns().as_str()) && #residual.name() == #xml_name
},
};
Ok(quote! {
{
#ns_rename
match #build {
Ok(data) => Ok(#repack),
Err(e) => Err(e),
}
if #test_expr {
let data = #parse;
Ok(#repack)
} else {
Err(#residual)
}
})
}
@ -156,61 +150,64 @@ impl ExtractDef {
/// passed on to the inner call to [`Compound::build_into_element`].
fn build_assemble(
self,
name: &ParentRef,
container_name: &ParentRef,
container_namespace_expr: &Expr,
field: &Expr,
parent_namespace_expr: Option<&Expr>,
) -> Result<TokenStream> {
let nfields = self.parts.field_count().unwrap();
let xml_namespace = self.expand_namespace(container_namespace_expr);
let xml_name = self.name;
let nfields = self.parts.field_count();
let ident = Ident::new("__extract_data", Span::call_site());
let ns_ident = Ident::new("__extract_namespace", Span::call_site());
let mut repack = if nfields == 1 {
let repack = if nfields == 1 {
quote! { let #ident = (#ident,); }
} else {
quote! { let #ident = #ident; }
};
// we reassign and wrap the parent namespace here in order to avoid
// any issues with (not yet implemented) nested extractions.
let parent_namespace_expr = if let Some(parent_namespace_expr) = parent_namespace_expr {
repack = quote! {
#repack
let #ns_ident = (#parent_namespace_expr).clone();
};
Some(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: ns_ident.into(),
}))
} else {
None
let builder = Ident::new("builder", Span::call_site());
let builder_init = match self.namespace {
FieldNamespace::Static(xml_namespace) => quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
#xml_namespace
)
},
FieldNamespace::Super(_) => quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
::xso::DynNamespaceEnum::into_xml_text(#xml_namespace.clone()),
)
},
};
let build = self.parts.build_into_element(
name,
None,
|member| {
Expr::Field(ExprField {
attrs: Vec::new(),
dot_token: syn::token::Dot {
spans: [Span::call_site()],
},
base: Box::new(Expr::Path(ExprPath {
let build =
self.parts
.build_into_element(container_name, &xml_namespace, &builder, |member| {
Expr::Field(ExprField {
attrs: Vec::new(),
qself: None,
path: Path::from(ident.clone()),
})),
member,
})
},
parent_namespace_expr.as_ref(),
)?;
dot_token: syn::token::Dot {
spans: [Span::call_site()],
},
base: Box::new(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Path::from(ident.clone()),
})),
member,
})
})?;
Ok(quote! {
{
let #ident = #field;
#repack
Some(#build)
let #builder = #builder_init;
let #builder = #build;
#builder.build()
}
})
}
@ -218,16 +215,7 @@ impl ExtractDef {
/// Return the type of the only field, if this extract has exactly one
/// field, or None otherwise.
fn inner_type(&self) -> Option<&Type> {
match self.parts {
Compound::Transparent { .. } => unreachable!(),
Compound::Struct { ref fields, .. } => {
if fields.len() != 1 {
None
} else {
Some(&fields[0].ty)
}
}
}
self.parts.single_type()
}
}
@ -377,7 +365,7 @@ impl ChildField {
tempname: Ident,
ident: Member,
ty: Type,
parent_namespace_expr: Option<&Expr>,
container_namespace_expr: &Expr,
) -> Result<FieldParsePart> {
match self.mode {
ChildMode::Single => {
@ -396,8 +384,8 @@ impl ChildField {
Some(extract) => {
let extract = extract.build_extract(
&name.child(ident),
container_namespace_expr,
&Ident::new("residual", Span::call_site()),
parent_namespace_expr,
)?;
Ok(FieldParsePart {
@ -460,8 +448,8 @@ impl ChildField {
Some(extract) => {
let extract = extract.build_extract(
&name.child(ident),
container_namespace_expr,
&Ident::new("residual", Span::call_site()),
parent_namespace_expr,
)?;
let item_ty: Type = syn::parse2(quote! {
<#ty as IntoIterator>::Item
@ -528,7 +516,7 @@ impl ChildField {
member: Member,
ident: Expr,
ty: Type,
parent_namespace_expr: Option<&Expr>,
container_namespace_expr: &Expr,
) -> Result<TokenStream> {
match self.mode {
ChildMode::Single => match self.extract {
@ -545,15 +533,12 @@ impl ChildField {
.clone();
let assemble = extract.build_assemble(
&name.child(member),
container_namespace_expr,
&temp_expr,
parent_namespace_expr,
)?;
Ok(quote! {
match Option::<#inner_ty>::from(#ident) {
Some(#temp_ident) => match #assemble {
Some(el) => builder.append(::xso::exports::minidom::Node::Element(el)),
None => builder,
}
Some(#temp_ident) => builder.append(::xso::exports::minidom::Node::Element(#assemble)),
None => builder,
}
})
@ -569,16 +554,16 @@ impl ChildField {
Some(extract) => {
let assemble = extract.build_assemble(
&name.child(member),
container_namespace_expr,
&Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Path::from(Ident::new("data", Span::call_site())),
}),
parent_namespace_expr,
)?;
Ok(quote! {
builder.append_all(
#ident.into_iter().filter_map(|data| {
#ident.into_iter().map(|data| {
#assemble
})
)

View File

@ -79,9 +79,9 @@ pub(crate) enum FieldNamespace {
/// Instead of a fixed namespace, the namespace of the parent is used.
///
/// This is only allowed inside compounds with a `#[xml(namespace = dyn)]`
/// declaration, i.e. using the
/// [`crate::compound::CompoundNamespace::Dyn`] variant.
/// This is only allowed inside enum variants or structs with a
/// `#[xml(namespace = dyn)]` declaration, i.e. using the
/// [`crate::structs::StructNamespace::Dyn`] variant.
Super(
/// The `super` token from the `#[xml(namespace = super)]` meta.
Token![super],
@ -116,7 +116,7 @@ pub(crate) enum FieldKind {
/// (`#[xml(namespace)]`).
///
/// See also
/// [`CompoundNamespace::Dyn`][`crate::compound::CompoundNamespace::Dyn`].
/// [`StructNamespace::Dyn`][`crate::structs::StructNamespace::Dyn`].
Namespace(NamespaceField),
/// This field is parsed from an XML child using the `FromXml` /
@ -386,25 +386,42 @@ impl FieldDef {
/// XML.
pub(crate) fn build_try_from_element(
self,
name: &ParentRef,
namespace_tempname: &Ident,
parent_namespace_expr: Option<&Expr>,
container_name: &ParentRef,
container_namespace_expr: &Expr,
) -> Result<FieldParsePart> {
let ident = self.ident;
let ty = self.ty;
let tempname = quote::format_ident!("__field_init_{}", ident);
match self.kind {
FieldKind::Attribute(field) => field.build_try_from_element(name, tempname, ident, ty),
FieldKind::Child(field) => {
field.build_try_from_element(name, tempname, ident, ty, parent_namespace_expr)
FieldKind::Attribute(field) => {
field.build_try_from_element(container_name, tempname, ident, ty)
}
FieldKind::Text(field) => field.build_try_from_element(name, tempname, ident, ty),
FieldKind::Element(field) => field.build_try_from_element(name, tempname, ident, ty),
FieldKind::Elements(field) => field.build_try_from_element(name, tempname, ident, ty),
FieldKind::Flag(field) => field.build_try_from_element(name, tempname, ident, ty),
FieldKind::Namespace(field) => {
field.build_try_from_element(name, tempname, ident, ty, namespace_tempname)
FieldKind::Child(field) => field.build_try_from_element(
container_name,
tempname,
ident,
ty,
container_namespace_expr,
),
FieldKind::Text(field) => {
field.build_try_from_element(container_name, tempname, ident, ty)
}
FieldKind::Element(field) => {
field.build_try_from_element(container_name, tempname, ident, ty)
}
FieldKind::Elements(field) => {
field.build_try_from_element(container_name, tempname, ident, ty)
}
FieldKind::Flag(field) => {
field.build_try_from_element(container_name, tempname, ident, ty)
}
FieldKind::Namespace(field) => field.build_try_from_element(
container_name,
container_namespace_expr,
tempname,
ident,
ty,
),
FieldKind::Ignore => Ok(FieldParsePart {
value: quote! { <#ty as ::std::default::Default>::default() },
..FieldParsePart::default()
@ -423,9 +440,9 @@ impl FieldDef {
/// `access_field` and the `builder` ident are accessible.
pub(crate) fn build_into_element(
self,
name: &ParentRef,
container_name: &ParentRef,
container_namespace_expr: &Expr,
mut access_field: impl FnMut(Member) -> Expr,
parent_namespace_expr: Option<&Expr>,
) -> Result<TokenStream> {
let member = self.ident.clone();
let ident = access_field(self.ident);
@ -433,9 +450,13 @@ impl FieldDef {
match self.kind {
FieldKind::Attribute(field) => field.build_into_element(ident, ty),
FieldKind::Text(field) => field.build_into_element(ident, ty),
FieldKind::Child(field) => {
field.build_into_element(name, member, ident, ty, parent_namespace_expr)
}
FieldKind::Child(field) => field.build_into_element(
container_name,
member,
ident,
ty,
container_namespace_expr,
),
FieldKind::Element(field) => field.build_into_element(ident, ty),
FieldKind::Elements(field) => field.build_into_element(ident, ty),
FieldKind::Flag(field) => field.build_into_element(ident, ty),

View File

@ -29,14 +29,14 @@ impl NamespaceField {
/// [`Compound::build_try_from_element`][`crate::compound::Compound::build_try_from_element`].
pub(super) fn build_try_from_element(
self,
_name: &ParentRef,
_container_name: &ParentRef,
container_namespace_expr: &Expr,
_tempname: Ident,
_ident: Member,
_ty: Type,
namespace_tempname: &Ident,
) -> Result<FieldParsePart> {
Ok(FieldParsePart {
value: quote! { #namespace_tempname },
value: quote! { #container_namespace_expr },
..FieldParsePart::default()
})
}

View File

@ -19,6 +19,40 @@ use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use syn::{spanned::Spanned, *};
/// Helper macro to chain multiple [`MetaParse`] structs in a series.
///
/// This attempts to parse one `option` after the other, each of which must
/// be an expression evaluating to a thing implementing [`MetaParse`]. If any
/// matches, `Ok(())` is returned. If none matches, an error message containing
/// the allowed option names by calling `name()` on the `option` values is
/// returned.
macro_rules! parse_meta {
($meta:ident, $($option:expr,)+) => {
#[allow(unused_assignments)]
{
let meta = $meta;
$(
let meta = match $option.parse_at_meta(meta) {
Ok(ControlFlow::Continue(meta)) => meta,
Ok(ControlFlow::Break(())) => return Ok(()),
Err(e) => return Err(e),
};
)+
let mut error = format!("unsupported option. supported options are: ");
let mut first = true;
$(
error.reserve($option.name().len() + 2);
if !first {
error.push_str(", ");
}
first = false;
error.push_str($option.name());
)+
Err(Error::new_spanned(meta.path, error))
}
}
}
/// Helper trait to parse a value of some kind from a
/// [`syn::meta::ParseNestedMeta`].
///
@ -63,7 +97,7 @@ impl<'a> MetaParse for ParseFlag<'a> {
format!("flag {} is already set", self.name()),
));
}
*self.1 = Flag::Present(meta.path.clone());
*self.1 = Flag::Present(meta.path.span());
Ok(())
}
}
@ -104,41 +138,8 @@ impl<'a> MetaParse for ParseExtracts<'a> {
}
}
/// Helper macro to chain multiple [`MetaParse`] structs in a series.
///
/// This attempts to parse one `option` after the other, each of which must
/// be an expression evaluating to a thing implementing [`MetaParse`]. If any
/// matches, `Ok(())` is returned. If none matches, an error message containing
/// the allowed option names by calling `name()` on the `option` values is
/// returned.
macro_rules! parse_meta {
($meta:ident, $($option:expr,)+) => {
#[allow(unused_assignments)]
{
let meta = $meta;
$(
let meta = match $option.parse_at_meta(meta) {
Ok(ControlFlow::Continue(meta)) => meta,
Ok(ControlFlow::Break(())) => return Ok(()),
Err(e) => return Err(e),
};
)+
let mut error = format!("unsupported option. supported options are: ");
let mut first = true;
$(
error.reserve($option.name().len() + 2);
if !first {
error.push_str(", ");
}
first = false;
error.push_str($option.name());
)+
Err(Error::new_spanned(meta.path, error))
}
}
}
/// Represents a boolean flag from a `#[xml(..)]` attribute meta.
#[derive(Clone, Copy)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum Flag {
/// The flag is not set.
@ -146,11 +147,11 @@ pub(crate) enum Flag {
/// The flag was set.
Present(
/// The [`syn::meta::ParseNestedMeta::path`] which enabled the flag.
/// The span of the syntax element which enabled the flag.
///
/// This is used to generate useful error messages by pointing at the
/// specific place the flag was activated.
Path,
Span,
),
}
@ -171,6 +172,12 @@ impl Flag {
}
}
impl<T: Spanned> From<T> for Flag {
fn from(other: T) -> Flag {
Flag::Present(other.span())
}
}
/// Type alias for a XML namespace setting.
///
/// This may in the future be replaced by an enum supporting both `Path` and
@ -185,7 +192,7 @@ pub(crate) type StaticNamespace = Path;
pub(crate) enum NamespaceRef {
/// A dynamic namespace, expressed as `#[xml(namespace = dyn)]`.
///
/// See [`crate::compound::CompoundNamespace::Dyn`] for details.
/// See [`crate::structs::StructNamespace::Dyn`] for details.
Dyn(
/// The original `dyn` token for better error messages.
Token![dyn],
@ -337,6 +344,9 @@ pub(crate) struct XmlCompoundMeta {
/// The value assigned to `prepare` inside `#[xml(..)]`, if any.
pub(crate) prepare: Option<Path>,
/// Flag indicating the presence of `debug` inside `#[xml(..)]`
pub(crate) debug: Flag,
}
impl XmlCompoundMeta {
@ -350,6 +360,7 @@ impl XmlCompoundMeta {
let mut fallback = Flag::Absent;
let mut transparent = Flag::Absent;
let mut exhaustive = Flag::Absent;
let mut debug = Flag::Absent;
let mut validate: Option<Path> = None;
let mut prepare: Option<Path> = None;
@ -361,11 +372,20 @@ impl XmlCompoundMeta {
ParseFlag("fallback", &mut fallback),
ParseFlag("transparent", &mut transparent),
ParseFlag("exhaustive", &mut exhaustive),
ParseFlag("debug", &mut debug),
ParseValue("validate", &mut validate),
ParseValue("prepare", &mut prepare),
)
})?;
#[cfg(not(feature = "debug"))]
if let Flag::Present(debug) = debug {
return Err(Error::new(
debug,
"`debug` is only allowed if the macros were built with --feature debug",
));
}
Ok(Self {
span: attr.span(),
namespace,
@ -375,6 +395,7 @@ impl XmlCompoundMeta {
validate,
exhaustive,
prepare,
debug,
})
}
@ -384,9 +405,11 @@ impl XmlCompoundMeta {
/// Undefined options or options with incompatible values are rejected
/// with an appropriate compile-time error.
///
/// If more than one or no `#[xml(..)]` attribute is found, an error is
/// If more than one `#[xml(..)]` attribute is found, an error is
/// emitted.
pub(crate) fn parse_from_attributes(attrs: &[Attribute]) -> Result<Self> {
///
/// If no `#[xml(..)]` attribute is found, `None` is returned.
pub(crate) fn try_parse_from_attributes(attrs: &[Attribute]) -> Result<Option<Self>> {
let mut result: Option<Self> = None;
for attr in attrs {
if !attr.path().is_ident("xml") {
@ -400,13 +423,24 @@ impl XmlCompoundMeta {
}
result = Some(Self::parse_from_attribute(attr)?);
}
if let Some(result) = result {
Ok(result)
} else {
Err(syn::Error::new(
Ok(result)
}
/// Search through `attrs` for a single `#[xml(..)]` attribute and parse
/// it.
///
/// Undefined options or options with incompatible values are rejected
/// with an appropriate compile-time error.
///
/// If more than one or no `#[xml(..)]` attribute is found, an error is
/// emitted.
pub(crate) fn parse_from_attributes(attrs: &[Attribute]) -> Result<Self> {
match Self::try_parse_from_attributes(attrs)? {
Some(v) => Ok(v),
None => Err(syn::Error::new(
Span::call_site(),
"#[xml(..)] attribute required on struct or enum variant",
))
)),
}
}
}
@ -518,7 +552,7 @@ impl AttributeMeta {
/// Emit an error with the given message if the codec is set
fn reject_default(&self, msg: impl Into<String>) -> Result<()> {
if let Flag::Present(path) = &self.default_on_missing {
Err(Error::new_spanned(path, msg.into()))
Err(Error::new(*path, msg.into()))
} else {
Ok(())
}
@ -662,7 +696,7 @@ pub(crate) enum XmlFieldMeta {
/// Maps the field to the compounds' XML element's namespace.
///
/// See also [`crate::compound::CompoundNamespace::Dyn`].
/// See also [`crate::structs::StructNamespace::Dyn`].
///
/// Maps to the `#[xml(namespace)]` Rust syntax.
Namespace,
@ -787,7 +821,7 @@ impl XmlFieldMeta {
fn children_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let (namespace, name, extract, default_on_missing) = Self::child_common_from_meta(meta)?;
if let Flag::Present(default_on_missing) = default_on_missing {
return Err(syn::Error::new_spanned(default_on_missing, "default cannot be used on #[xml(children)] (it is implied, the default is the empty container)"));
return Err(syn::Error::new(default_on_missing, "default cannot be used on #[xml(children)] (it is implied, the default is the empty container)"));
}
Ok(Self::Child {
mode: ChildMode::Collection,
@ -848,7 +882,7 @@ impl XmlFieldMeta {
let meta = AttributeMeta::parse_from_meta(meta)?;
meta.reject_type("type = .. is not allowed on fields (only on extracts)")?;
if let Flag::Present(default_on_missing) = meta.default_on_missing {
return Err(syn::Error::new_spanned(
return Err(syn::Error::new(
default_on_missing,
"default cannot be used on #[xml(flag)] (it is implied, the default false)",
));

View File

@ -6,19 +6,393 @@ this crate on `struct` items.
It is thus the counterpart to [`crate::enums`].
*/
use proc_macro2::Span;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::*;
use crate::common::bake_generics;
use crate::common::{bake_generics, build_prepare, build_validate};
use crate::compound::Compound;
use crate::meta::{Flag, XmlCompoundMeta};
use crate::error_message::ParentRef;
use crate::meta::{Flag, Name, NameRef, NamespaceRef, StaticNamespace, XmlCompoundMeta};
/// A XML namespace as declared on a struct.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum StructNamespace {
/// The namespace is a static string.
Static(
/// The namespace as [`Path`] pointing at the static string.
StaticNamespace,
),
/// Instead of a fixed namespace, the namespace is dynamic. The allowed
/// values are determined by a field with
/// [`FieldKind::Namespace`][`crate::field::FieldKind::Namespace`] kind
/// (declared using `#[xml(namespace)]`).
Dyn {
/// The `dyn` token from the `#[xml(namespace = dyn)]` meta.
#[allow(dead_code)]
dyn_tok: Token![dyn],
/// The type of the namespace field.
ty: Type,
/// The member of the namespace field.
member: Member,
},
}
/// The inner parts of the struct.
///
/// This contains all data necessary for the matching logic, but does not
/// include validation/preparation of the data. The latter is handled by
/// [`StructDef`].
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum StructInner {
/// Single-field tuple-like struct declared with `#[xml(transparent)]`.
///
/// Transparent struct delegate all parsing and serialising to their
/// single 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)
/// - the fields must be unnamed
/// - there must be only exactly one field
/// - that field has no `#[xml]` attribute
Transparent {
/// Type of the only unnamed field.
ty: Type,
},
/// A compound of fields, *not* declared as transparent.
///
/// This can be a unit, tuple-like, or named struct.
Compound {
/// The XML namespace to match the struct against.
namespace: StructNamespace,
/// The XML name to match the struct against.
name: Name,
/// The contents of the struct.
inner: Compound,
},
}
impl StructInner {
/// Process the `meta` and `fields` into a [`StructInner`].
///
/// The `meta` must be "blank" except for the `transparent`, `namespace`
/// and `name` fields. If any other field has a non-`None` / non-`Absent`
/// value, this function panics!
pub(crate) fn new(meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
// These must be taken out by the caller.
assert!(!meta.exhaustive.is_set());
assert!(meta.validate.is_none());
assert!(meta.prepare.is_none());
assert!(!meta.debug.is_set());
assert!(!meta.fallback.is_set());
if let Flag::Present(_) = meta.transparent {
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"namespace option not allowed on transparent structs or enum variants",
));
}
if let Some(name) = meta.name {
return Err(Error::new_spanned(
name,
"name option not allowed on transparent structs or enum variants",
));
}
Self::new_transparent(fields)
} else {
let Some(namespace) = meta.namespace else {
return Err(Error::new(
meta.span,
"`namespace` option is required on non-transparent structs or enum variants",
));
};
let Some(name) = meta.name else {
return Err(Error::new(
meta.span,
"`name` option is required on non-transparent structs or enum variants",
));
};
Self::new_compound(namespace, name, fields)
}
}
/// Construct a new transparent struct with the given fields.
///
/// This function ensures that only a single, unnamed field is inside the
/// struct and causes a compile-time error otherwise.
fn new_transparent(fields: &Fields) -> Result<Self> {
let field = match fields {
Fields::Unit => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
))
}
Fields::Named(_) => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must be tuple-like",
))
}
Fields::Unnamed(fields) => {
if fields.unnamed.len() == 0 {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
));
} else if fields.unnamed.len() > 1 {
return Err(Error::new_spanned(
&fields.unnamed[1],
"transparent structs or enum variants must have exactly one field",
));
}
&fields.unnamed[0]
}
};
for attr in field.attrs.iter() {
if attr.path().is_ident("xml") {
return Err(Error::new_spanned(
attr.path(),
"the field inside a #[xml(transparent)] struct or enum variant cannot have an #[xml(..)] attribute."
));
}
}
Ok(Self::Transparent {
ty: field.ty.clone(),
})
}
/// Construct a new compound-based struct with the given namespace, name
/// and fields.
fn new_compound(namespace: NamespaceRef, name: NameRef, fields: &Fields) -> Result<Self> {
let inner = Compound::from_fields(fields)?;
let namespace_field = inner.namespace_field();
let namespace = match namespace {
NamespaceRef::Static(namespace) => {
if let Some((span, ..)) = namespace_field {
return Err(Error::new(
span,
"struct or enum variant must be declared with #[xml(namespace = dyn, ..)] to use a #[xml(namespace)] field."
));
}
StructNamespace::Static(namespace)
}
NamespaceRef::Dyn(namespace) => {
if let Some((_, ty, member)) = namespace_field {
StructNamespace::Dyn {
dyn_tok: namespace,
ty: ty.clone(),
member: member.clone(),
}
} else {
return Err(Error::new_spanned(
namespace,
"enum variant or struct declared with #[xml(namespace = dyn)] must have a field annotated with #[xml(namespace)]"
));
}
}
NamespaceRef::Super(ns) => {
return Err(Error::new_spanned(
ns,
"#[xml(namespace = super)] not allowed on enum variant or struct.",
));
}
};
Ok(Self::Compound {
namespace,
name: name.into(),
inner,
})
}
/// Construct an expression which consumes `residual` and evaluates to
/// `Result<T, Element>`.
///
/// - `struct_name` may contain either the path necessary to construct an
/// instance of the struct or a nested parent ref. In the latter case,
/// the struct is constructed as tuple instead of a struct.
///
/// - `residual` must be the identifier of the `minidom::Element` to
/// process.
///
/// If the element does not match the selectors of this struct, it is
/// returned in the `Err` variant for further probing.
pub(crate) fn build_try_from_element(
self,
struct_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
match self {
Self::Transparent { ty } => {
let cons = match struct_name {
ParentRef::Named(path) => quote! { #path },
ParentRef::Unnamed { .. } => quote! {},
};
Ok(quote! {
match <#ty as ::xso::FromXml>::from_tree(#residual) {
Ok(v) => Ok(#cons (v)),
Err(::xso::error::Error::TypeMismatch(_, _, #residual)) => Err(#residual),
Err(other) => return Err(other),
}
})
}
Self::Compound {
namespace,
name: xml_name,
inner,
} => {
let namespace_tempname = Ident::new("__struct_namespace", Span::call_site());
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: namespace_tempname.clone().into(),
});
let body = inner.build_try_from_element(struct_name, &namespace_expr, residual)?;
match namespace {
StructNamespace::Dyn { ty, .. } => Ok(quote! {
match <#ty as ::xso::DynNamespaceEnum>::from_xml_text(&#residual.ns()) {
Ok(#namespace_tempname) => if #residual.name() == #xml_name {
Ok(#body)
} else {
Err(#residual)
}
Err(::xso::error::DynNamespaceError::Invalid) => {
return Err(::xso::error::Error::ParseError(
"Invalid namespace"
));
}
Err(::xso::error::DynNamespaceError::Mismatch) => Err(#residual),
}
}),
StructNamespace::Static(xml_namespace) => Ok(quote! {
if #residual.is(#xml_name, #xml_namespace) {
let #namespace_tempname = #xml_namespace;
Ok(#body)
} else {
Err(#residual)
}
}),
}
}
}
}
/// Construct an expression which takes the fields as accessed through
/// `access_field` and converts them into a `minidom::Element`.
///
/// - `struct_name` is used primarily for diagnostic messages.s
///
/// - `access_field` must be a function which transforms a [`syn::Member`]
/// referring to a member of the struct to an expression under which the
/// member can be accessed.
pub(crate) fn build_into_element(
self,
struct_name: &ParentRef,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
match self {
Self::Transparent { ty } => {
let ident = access_field(Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
}));
Ok(quote! {
<#ty as ::xso::IntoXml>::into_tree(#ident).expect("inner element did not produce any data")
})
}
Self::Compound {
namespace,
name: xml_name,
inner,
} => {
let builder = Ident::new("builder", Span::call_site());
let (builder_init, namespace_expr) = match namespace {
StructNamespace::Dyn { member, ty, .. } => {
let expr = access_field(member);
(
quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
<#ty as ::xso::DynNamespaceEnum>::into_xml_text(#expr.clone()),
)
},
expr,
)
}
StructNamespace::Static(xml_namespace) => {
let expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: xml_namespace.clone(),
});
(
quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
#xml_namespace,
)
},
expr,
)
}
};
let body = inner.build_into_element(
struct_name,
&namespace_expr,
&builder,
&mut access_field,
)?;
Ok(quote! {
{
let mut #builder = #builder_init;
let #builder = #body;
#builder.build()
}
})
}
}
}
/// Return an iterator which returns the [`syn::Member`] structs to access
/// the struct's fields in declaration order.
pub(crate) fn iter_members(&self) -> Box<dyn Iterator<Item = Member> + '_> {
match self {
Self::Transparent { .. } => Box::new(
[Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
})]
.into_iter(),
),
Self::Compound { inner, .. } => inner.iter_members(),
}
}
}
/// Represent a struct.
#[cfg_attr(feature = "debug", derive(Debug))]
struct StructDef {
/// The `validate` if set on the struct.
pub(crate) struct StructDef {
/// The `validate` value, if set on the struct.
///
/// This is called after the struct has been otherwise parsed successfully
/// with the struct value as mutable reference as only argument. It is
@ -26,14 +400,18 @@ struct StructDef {
/// is forwarded correctly.
validate: Option<Path>,
/// The `prepare` if set on the struct.
/// The `prepare` value, if set on the struct.
///
/// This is called before the struct will be converted back into an XML
/// element with the struct value as mutable reference as only argument.
prepare: Option<Path>,
/// The contents of the struct.
inner: Compound,
/// Structure of the struct.
inner: StructInner,
/// The `debug` flag if set on the struct.
#[cfg_attr(not(feature = "debug"), allow(dead_code))]
debug: Flag,
}
impl StructDef {
@ -41,25 +419,94 @@ impl StructDef {
/// fields.
fn new(mut meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
if let Flag::Present(fallback) = meta.fallback.take() {
return Err(syn::Error::new_spanned(
return Err(syn::Error::new(
fallback,
"fallback is not allowed on structs",
));
}
if let Flag::Present(exhaustive) = meta.exhaustive.take() {
return Err(syn::Error::new_spanned(
return Err(syn::Error::new(
exhaustive,
"exhaustive is not allowed on structs",
));
}
let validate = meta.validate.take();
let prepare = meta.prepare.take();
let debug = meta.debug.take();
Ok(Self {
validate,
prepare,
inner: Compound::new(meta, fields)?,
debug,
inner: StructInner::new(meta, fields)?,
})
}
/// Construct an expression which consumes `residual` and evaluates to
/// `Result<T, Error>`.
///
/// - `struct_name` may contain either the path necessary to construct an
/// instance of the struct or a nested parent ref. In the latter case,
/// the struct is constructed as tuple instead of a struct.
///
/// - `residual` must be the identifier of the `minidom::Element` to
/// process.
fn build_try_from_element(
self,
struct_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
let validate = build_validate(self.validate);
let try_from_impl = self.inner.build_try_from_element(struct_name, residual)?;
let result = quote! {
let mut result = match #try_from_impl {
Ok(v) => v,
Err(residual) => return Err(Self::Error::TypeMismatch("", "", residual)),
};
#validate;
Ok(result)
};
#[cfg(feature = "debug")]
if self.debug.is_set() {
println!("{}", result);
}
Ok(result)
}
/// Construct an expression which consumes the `T` value at `value_ident`
/// and returns a `minidom::Element`.
///
/// - `struct_name` is used primarily for diagnostic messages.
///
/// - `value_ident` must be the identifier at which the entire struct can
/// be reached. It is used during preparation.
///
/// - `access_field` must be a function which transforms a [`syn::Member`]
/// referring to a member of the struct to an expression under which the
/// member can be accessed.
fn build_into_element(
self,
struct_name: &ParentRef,
value_ident: &Ident,
access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
let prepare = build_prepare(self.prepare, value_ident);
let into_impl = self.inner.build_into_element(struct_name, access_field)?;
let result = quote! {
#prepare
#into_impl
};
#[cfg(feature = "debug")]
if self.debug.is_set() {
println!("{}", result);
}
Ok(result)
}
}
/// `FromXml` derive macro implementation for structs.
@ -67,21 +514,10 @@ pub(crate) fn try_from_element(item: syn::ItemStruct) -> Result<proc_macro2::Tok
let meta = XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let ident = item.ident;
let def = StructDef::new(meta, &item.fields)?;
let try_from_impl = def.inner.build_try_from_element(
let try_from_impl = def.build_try_from_element(
&(Path::from(ident.clone()).into()),
None,
&Ident::new("residual", Span::call_site()),
None,
)?;
let validate = if let Some(validate) = def.validate {
quote! {
#validate(&mut result)?;
}
} else {
quote! {
{ let _ = &mut result; };
}
};
let (generics_decl, generics_ref, where_clause) = bake_generics(item.generics);
Ok(quote! {
#[allow(non_snake_case)]
@ -89,12 +525,7 @@ pub(crate) fn try_from_element(item: syn::ItemStruct) -> Result<proc_macro2::Tok
type Error = ::xso::error::Error;
fn try_from(mut residual: ::xso::exports::minidom::Element) -> Result<Self, Self::Error> {
let mut result = match #try_from_impl {
Ok(v) => v,
Err(residual) => return Err(Self::Error::TypeMismatch("", "", residual)),
};
#validate
Ok(result)
#try_from_impl
}
}
@ -115,9 +546,9 @@ pub(crate) fn into_element(item: syn::ItemStruct) -> Result<proc_macro2::TokenSt
let meta = XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let ident = item.ident;
let def = StructDef::new(meta, &item.fields)?;
let into_impl = def.inner.build_into_element(
let into_impl = def.build_into_element(
&(Path::from(ident.clone()).into()),
None,
&Ident::new("other", Span::call_site()),
|member| {
Expr::Field(ExprField {
attrs: Vec::new(),
@ -137,23 +568,12 @@ pub(crate) fn into_element(item: syn::ItemStruct) -> Result<proc_macro2::TokenSt
member,
})
},
None,
)?;
let prepare = if let Some(prepare) = def.prepare {
quote! {
let _: () = #prepare(&mut other);
}
} else {
quote! {
{ let _ = &mut other; };
}
};
let (generics_decl, generics_ref, where_clause) = bake_generics(item.generics);
Ok(quote! {
#[allow(non_snake_case)]
impl #generics_decl ::std::convert::From<#ident #generics_ref> for ::xso::exports::minidom::Element #where_clause {
fn from(mut other: #ident #generics_ref) -> Self {
#prepare
#into_impl
}
}

View File

@ -98,59 +98,61 @@ preserved, if the container preserves sort order on insertion.
`validate` is allowed and will be called.
## Enum attributes
## Enums
- `namespace = ..`: See struct attributes.
However, one enumerations, the
`namespace` attribute is optional if and only if all variants either specify
the namespace or are transparent.
In addition, `dyn` namespaces are not supported on enums, but you can omit
the `namespace` attribute altogether and use `namespace = dyn` (or
`transparent`) on all variants.
Enums come in multiple flavors. All flavors have the following attributes:
- `validate = ..`: See struct attributes.
- `prepare = ..`: See struct attributes.
- `name` is not allowed on the enum (but on variants).
- `transparent` is not allowed on the enum (but on variants).
- `exhaustive`: If set, the `namespace` must be set on the enum. The enum then
considers itself to be exhaustive for that namespace. That means that if
an element from that namespace is encountered where this enum may be parsed,
and it does not match any of variants, a hard error is produced instead of
allowing parsing to continue to try other elements which may be candidates.
This matters when the enum is used in a struct: If the struct has multiple
fields concerning this namespace, the exhaustive enum may take precedence
and abort parsing. This may be desirable e.g. for defined error
enumerations, because the error message is clearer than when a generic
"unknown child" error is emitted.
The following flavors exist:
This cannot be combined with a `fallback` variant.
- fully dynamic: The variants must be each either transparent or specify
their `namespace` and `name`.
- XML name matched: The enum itself defines a namespace. Each variant must
then specify the name. The variant is picked based on the name of the XML
element.
The flavor is determined based on the attributes of the enum declaration.
### Dynamic enums
No additional attributes are available on dynamic enumerations.
#### Dynamic enum variants
Dynamic enum variants work exactly like structs, except that the `prepare`
and `validate` attributes are not available.
### XML name matched enums
XML name matched enums support the following attributes:
- `namespace = ..` (required): This must be a path to a `&'static str`. It is
the namespace of the enumeration.
- `exhaustive` (flag): If present, the enum considers itself authoritative for
that namespace. If it encounters an element within the namespace which does
not match any variant, a fatal parsing error is returned.
This cannot be used if a variant is set as `fallback`.
This attribute has no relation to the Rust standard `#[non_exhaustive]`
attribute.
### Enum variant attributes
#### XML name matched enum variants
Enum variants support almost exactly the same attributes as structs, with the
following variations:
XML name matched enum variants support the following attributes:
- `fallback`: If set, this enumeration variant is generated when no other
variant matches. This is only allowed if all of the following are true:
- `name = ..` (required): String literal with the XML name to match against.
- `namespace` is declared on the enumeration itself
- `namespace` is declared on none of the variants
- No variant is `transparent`
- The variant which is declared as `fallback` is a unit variant (has no fields)
- The enum is not declared `exhaustive`.
- `fallback` (flag): If present, the variant is parsed when no other variant
matches.
Only a single variant may be declared as `fallback`.
This attribute has no influence on [`IntoXml`].
- `namespace` is optional if the enum has a `namespace` declared.
- `validate` and `prepare` cannot be used; they have to be applied to the enum
itself instead.
*Note:* When the enum is reserialized to XML, the XML name will be the one
declared in the `name` attribute of the variant; the original XML name is
lost.
## Field attributes