Implement `#[derive(FromXml, IntoXml)]`

This commit deserves some explanation.

1. Why move from the `macro_rules!` approach to a derive macro?

A derive macro is much more idiomatic to Rust. The `macro_rules!`-based
approach, while pretty clever and full-featured, is very opaque and
unclear to crate (but not partiuclarly Rust) newcomers. Not to mention
that the implementation, with all the variations it handles in the
rather functional macro_rules!-language, got very unwieldly and hard to
maintain.

2. Why add a `xso` crate? What is its purpose and its
   relation to `xmpp_parsers` and `xso_proc`?

From the code generated within the derive macro, we have to refer to
some traits and/or types we define. We cannot define these in the
`xso_proc` crate, because proc macro crates can only export
proc macros, no other items.

We also need to refer to these types unambiguously. As we don't know
which names are imported in the scopes the derive macro is invoked in,
we have to use absolute paths (such as `::foo::bar`).

That means we need a crate name.

Now there are two non-options:

- We could use `crate::`. That has the downside that the derive macro
  is only useful within the `xmpp_parsers` crate. It cannot be used
  outside, unless the traits are re-imported there.

- We could use `::xmpp_parsers::..`, except, we can't: That would
  prevent use of the macro within `xmpp_parsers`, because crates cannot
  refer to themselves by name.

So the only way that makes sense is to have a `xso` crate
which contains the traits as well as some basic implementations needed
for the macros to work, re-export the derive macros from there, and
depend on that crate from `xmpp_parsers`.

3. Oh god this is complex, how do I learn more or hack on this??

Run `cargo doc --document-private-items`. The `xso_proc`
crate is fully documented in the private areas, and that makes that
documentation quite accessible in your browser.
This commit is contained in:
Jonas Schäfer 2024-03-23 16:15:28 +01:00
parent 054447d147
commit 1d71e8b584
18 changed files with 4494 additions and 0 deletions

View File

@ -6,6 +6,8 @@ members = [ # alphabetically sorted
"sasl",
"tokio-xmpp",
"xmpp",
"xso-proc",
"xso",
]
resolver = "2"
@ -16,3 +18,5 @@ sasl = { path = "sasl" }
tokio-xmpp = { path = "tokio-xmpp" }
xmpp-parsers = { path = "parsers" }
xmpp = { path = "xmpp" }
xso_proc = { path = "xso-proc" }
xso = { path = "xso" }

25
xso-proc/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "xso_proc"
version = "0.1.0"
authors = [
"Jonas Schäfer <jonas@zombofant.net>",
]
description = "Macros for easier definition of XMPP structs (see xmpp-parsers)"
homepage = "https://gitlab.com/xmpp-rs/xmpp-rs"
repository = "https://gitlab.com/xmpp-rs/xmpp-rs"
keywords = ["xmpp", "jabber", "xml"]
categories = ["parsing", "network-programming"]
license = "MPL-2.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
quote = "^1"
syn = { version = "^2", features = ["full"] }
proc-macro2 = "^1"
[features]
debug = ["syn/extra-traits"]
default = ["debug"]

55
xso-proc/src/common.rs Normal file
View File

@ -0,0 +1,55 @@
/*!
Helpers used both for enums and structs.
*/
use proc_macro2::TokenStream;
use quote::quote;
use syn::*;
/// Extract the relevant parts from an [`Item`]'s [`Generics`] so
/// that they can be used inside [`quote::quote`] to form `impl` items.
///
/// The returned parts are:
/// - The list of parameters incl. bounds enclosed in `< .. >`, for use right
/// after the `impl` keyword. If there are no parameters, this part is
/// empty.
/// - The list of parameters without bounds enclosed in `< .. >`, for use when
/// referring to the Item's type. If there are no parameters, this part is
/// empty.
/// - The where clause, if any.
///
/// The results are formed so that they can be used unconditionally, i.e. the
/// parameter lists are completely empty token streams if and only if the
/// [`Generics`] do not contain any parameters.
pub(crate) fn bake_generics(generics: Generics) -> (TokenStream, TokenStream, Option<WhereClause>) {
let params = generics.params;
let where_clause = generics.where_clause;
if params.len() > 0 {
let mut params_ref = Vec::new();
for param in params.iter() {
params_ref.push(match param {
GenericParam::Lifetime(lt) => GenericArgument::Lifetime(lt.lifetime.clone()),
GenericParam::Type(ty) => GenericArgument::Type(Type::Path(TypePath {
qself: None,
path: ty.ident.clone().into(),
})),
GenericParam::Const(cst) => GenericArgument::Const(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: cst.ident.clone().into(),
})),
});
}
(
quote! {
< #params >
},
quote! {
< #( #params_ref ),* >
},
where_clause,
)
} else {
(quote! {}, quote! {}, where_clause)
}
}

521
xso-proc/src/compound.rs Normal file
View File

@ -0,0 +1,521 @@
/*!
# Types to represent compounds
A [`Compound`] is either an enum variant or a struct. These types are used by
[`crate::enums`] and [`crate::structs`], as well as for extracted child fields
in order to build the code to convert to/from XML nodes.
*/
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{spanned::Spanned, *};
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)]`).
///
/// Not implemented yet.
Dyn(
/// The `dyn` token from the `#[xml(namespace = dyn)]` meta.
Token![dyn],
),
}
/// An struct or enum variant's contents.
#[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,
},
/// 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>,
},
}
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 namespace = match meta.namespace {
None => None,
Some(NamespaceRef::Static(namespace)) => Some(CompoundNamespace::Static(namespace)),
Some(NamespaceRef::Dyn(namespace)) => Some(CompoundNamespace::Dyn(namespace)),
Some(NamespaceRef::Super(namespace)) => {
return Err(Error::new_spanned(
namespace,
"only fields can refer to the parent namespace",
))
}
};
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",
))
}
};
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> = None;
let mut collect_wildcard_field: Option<Span> = None;
for field in input {
let field = field?;
if field.kind.is_text() {
if text_field.is_some() {
return Err(Error::new_spanned(
field.ident,
"only one #[xml(text)] field is allowed",
));
}
text_field = Some(field.ident.span());
}
if field.kind.is_namespace() {
if namespace_field.is_some() {
return Err(Error::new_spanned(
field.ident,
"only one #[xml(text)] field is allowed",
));
}
namespace_field = Some(field.ident.span());
}
if field.kind.is_collect_wildcard() {
if collect_wildcard_field.is_some() {
return Err(Error::new_spanned(
field.ident,
"only one #[xml(collect)] field without namespace/name selector is allowed",
));
}
collect_wildcard_field = Some(field.ident.span());
}
fields.push(field);
}
if let Some(namespace_field) = namespace_field {
match namespace {
None => return Err(Error::new(namespace_field, "struct or enum variant must be declared with #[xml(namespace = dyn, ..)] to use a #[xml(namespace)] field.")),
Some(CompoundNamespace::Static(namespace)) => return Err(Error::new_spanned(namespace, "struct or enum variant must be declared with #[xml(namespace = dyn, ..)] to use a #[xml(namespace)] field.")),
Some(CompoundNamespace::Dyn(_)) => (),
}
};
Ok(Self::Struct {
namespace,
name,
fields,
})
}
/// Construct a [`Compound`] based on its [`XmlCompoundMeta`] and its
/// 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"))
}),
)
}
}
/// 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()),
}
}
/// 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,
},
}
}
/// Combine `a` and `b` to obtain a valid `CompoundNamespace` or return an
/// error.
///
/// The `name` argument is used to produce a nice error message.
fn need_namespace<'x>(
name: Option<&Path>,
a: Option<&'x CompoundNamespace>,
b: Option<&'x CompoundNamespace>,
) -> Result<&'x CompoundNamespace> {
if let Some(namespace) = a.or(b) {
Ok(namespace)
} else {
Err(Error::new_spanned(name, format!("cannot determine namespace for struct or enum variant {}. use #[xml(namespace = ..)] on the variant or enum.", name.to_token_stream())))
}
}
/// Construct a token stream containing an expression to parse this
/// compound from a `minidom::Element` in `#residual`.
///
/// The expression consumes the `#residual` and returns a
/// `Result<T, minidom::Element>`.
///
/// 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`.
///
/// Otherwise, `T` is a tuple of the fields inside the compound in
/// declaration order.
pub(crate) fn build_try_from_element(
self,
name: Option<&Path>,
item_namespace: Option<&CompoundNamespace>,
residual: &Ident,
) -> 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 xml_namespace = match namespace {
CompoundNamespace::Static(ns) => ns,
CompoundNamespace::Dyn(ns) => {
return Err(Error::new_spanned(
ns,
"dynamic namespaces not implemented yet.",
))
}
};
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 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)?;
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 {
Some(name) => quote! {
#name {
#init
}
},
None => quote! {
( #tupinit )
},
};
Ok(quote! {
if #residual.is(#xml_name, #xml_namespace) {
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)
} else {
Err(#residual)
}
})
}
}
}
/// Construct an expression evaluating to a `minidom::Element` containing
/// all 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.
///
/// 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).
///
/// 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.
pub(crate) fn build_into_element(
self,
item_name: Option<&Path>,
item_namespace: Option<&CompoundNamespace>,
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! {
::xso::exports::minidom::Element::from(#ident)
})
}
Self::Struct {
namespace,
name,
fields,
} => {
let xml_namespace =
match Self::need_namespace(item_name, namespace.as_ref(), item_namespace)? {
CompoundNamespace::Static(ns) => ns,
CompoundNamespace::Dyn(ns) => {
return Err(Error::new_spanned(ns, "dyn namespace not supported yet"))
}
};
let xml_name = name;
let mut build = quote! {};
for field in fields {
let field_build = field.build_into_element(&mut access_field)?;
build = quote! {
#build
builder = #field_build;
};
}
Ok(quote! {
{
let mut builder = ::xso::exports::minidom::Element::builder(#xml_name, #xml_namespace);
#build
builder.build()
}
})
}
}
}
/// Return an iterator which returns the [`syn::Member`] structs to access
/// the compound's fields in declaration order.
///
/// 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()))
}
}
}
}

420
xso-proc/src/enums.rs Normal file
View File

@ -0,0 +1,420 @@
/*!
# Processing of enum declarations
This module contains the main code for implementing the derive macros from
this crate on `enum` items.
It is thus the counterpart to [`crate::structs`].
*/
use proc_macro2::Span;
use quote::quote;
use syn::*;
use crate::common::bake_generics;
use crate::compound::{Compound, CompoundNamespace};
use crate::meta::{Flag, NamespaceRef, XmlCompoundMeta};
/// Represent a single enum variant.
#[cfg_attr(feature = "debug", derive(Debug))]
struct EnumVariant {
/// The identifier of the enum variant.
ident: Ident,
/// The `fallback` flag on the enum variant.
fallback: Flag,
/// The contents of the enum variant.
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)",
));
}
if let Some(prepare) = meta.prepare.take() {
return Err(syn::Error::new_spanned(
prepare,
"prepare is not allowed on enum variants (but on enums)",
));
}
if let Flag::Present(exhaustive) = meta.exhaustive.take() {
return Err(syn::Error::new_spanned(
exhaustive,
"exhaustive is not allowed on enum variants (but on enums)",
));
}
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",
))
}
}
}
Ok(Self {
fallback,
ident,
inner,
})
}
}
/// Represent an enum.
#[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>,
/// The `prepare` 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 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>,
/// The enum variants.
variants: Vec<EnumVariant>,
/// 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`.
///
/// Then, if an XML element matching the namespace but none of the
/// variants is encountered, a hard parse error with an appropriate error
/// message is emitted. That prevents parsing the XML element as other
/// fields of a parent struct (such as #[xml(elements)]`).
///
/// This can be useful in some circumstances.
exhaustive: Flag,
}
impl EnumDef {
/// Construct a new enum from its `#[xml(..)]` attribute and the variants.
///
/// 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.
fn new<'x, I: Iterator<Item = &'x Variant>>(
mut meta: XmlCompoundMeta,
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)) => Some(CompoundNamespace::Dyn(namespace)),
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(
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::Dyn(ns))=> return Err(syn::Error::new_spanned(ns, "fallback variants cannot be combined with dynamic namespaces")),
}
}
if let Flag::Present(ref exhaustive) = meta.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::Dyn(ns))=> return Err(syn::Error::new_spanned(ns, "exhaustive enums cannot be combined with dynamic namespaces")),
}
}
if variants.len() == 0 {
return Err(syn::Error::new(
meta.span,
"empty enumerations are not supported",
));
}
Ok(Self {
validate: meta.validate,
prepare: meta.prepare,
namespace,
variants,
exhaustive: meta.exhaustive,
})
}
/// Construct the entire implementation of
/// `TryFrom<minidom::Element>::try_from`.
fn build_try_from_element(
self,
enum_ident: &Ident,
residual: &Ident,
) -> Result<proc_macro2::TokenStream> {
let xml_namespace = self.namespace;
let validate = if let Some(validate) = self.validate {
quote! {
#validate(&mut result)?;
}
} else {
quote! {
{ let _ = &mut result; };
}
};
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 variant_impl = variant.inner.build_try_from_element(
Some(&Path {
leading_colon: None,
segments: [
PathSegment::from(enum_ident.clone()),
PathSegment::from(ident.clone()),
]
.into_iter()
.collect(),
}),
xml_namespace.as_ref(),
residual,
)?;
iter = quote! {
#iter
let mut #residual = match #variant_impl {
Ok(mut result) => {
#validate
return Ok(result);
},
Err(residual) => residual,
};
}
}
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.")));
}
};
}
Ok(quote! {
#iter
#fallback
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> {
let mut matchers = quote! {};
for variant in self.variants {
let path = Path {
leading_colon: None,
segments: [
PathSegment::from(ty_ident.clone()),
PathSegment::from(variant.ident),
]
.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 into_element = variant.inner.build_into_element(
Some(&Path::from(ty_ident.clone())),
self.namespace.as_ref(),
map_ident,
)?;
matchers = quote! {
#matchers
#path { #( #orig_names: #mapped_names ),* } => {
#into_element
},
};
}
Ok(quote! {
match #value_ident {
#matchers
}
})
}
}
/// `FromXml` derive macro implementation for enumerations.
pub(crate) fn try_from_element(item: syn::ItemEnum) -> Result<proc_macro2::TokenStream> {
let meta = XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let ident = item.ident;
let def = EnumDef::new(meta, item.variants.iter())?;
let try_from_impl =
def.build_try_from_element(&ident, &Ident::new("residual", Span::call_site()))?;
let (generics_decl, generics_ref, where_clause) = bake_generics(item.generics);
Ok(quote! {
#[allow(non_snake_case)]
impl #generics_decl ::std::convert::TryFrom<::xso::exports::minidom::Element> for #ident #generics_ref #where_clause {
type Error = ::xso::error::Error;
fn try_from(mut residual: ::xso::exports::minidom::Element) -> Result<Self, Self::Error> {
#try_from_impl
}
}
impl #generics_decl ::xso::FromXml for #ident #generics_ref #where_clause {
fn from_tree(elem: ::xso::exports::minidom::Element) -> Result<Self, ::xso::error::Error> {
Self::try_from(elem)
}
fn absent() -> Option<Self> {
None
}
}
})
}
/// `IntoXml` derive macro implementation for enumerations.
pub(crate) fn into_element(item: syn::ItemEnum) -> Result<proc_macro2::TokenStream> {
let meta = XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let ident = item.ident;
let mut def = EnumDef::new(meta, item.variants.iter())?;
let prepare = if let Some(prepare) = def.prepare.take() {
quote! {
let _: () = #prepare(&mut other);
}
} else {
quote! {
{ let _ = &mut other; };
}
};
let into_impl = def.build_into_element(&ident, &Ident::new("other", Span::call_site()))?;
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
}
}
impl #generics_decl ::xso::IntoXml for #ident #generics_ref {
fn into_tree(self) -> Option<::xso::exports::minidom::Element> {
Some(::minidom::Element::from(self))
}
}
})
}

View File

@ -0,0 +1,123 @@
//! Infrastructure for parsing fields from attributes.
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::*;
use crate::meta::{Flag, Name, NameRef};
use super::FieldParsePart;
/// A field parsed from an XML attribute.
///
/// Maps to `#[xml(attribute)]`.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) struct AttributeField {
/// The XML name of the attribute.
///
/// *Note:* Namespaced attributes are currently not supported.
pub(super) name: Name,
/// Whether [`Default`] should be used to obtain a value f the attribute
/// is missing.
///
/// If the flag is *not* set, an error is returned when parsing an element
/// without the attribute.
pub(super) default_on_missing: Flag,
}
impl AttributeField {
/// Construct a new `#[xml(attribute)]` field.
///
/// The `field_ident` must be the field's identifier (if in a named
/// compound).
///
/// `name` must be the XML name assigned to the attribtue, if any, as
/// parsed from the `#[xml(..)]` meta on the field.
///
/// `default_on_missing` must be the `default` flag as parsed from the
/// `#[xml(..)]` meta on the field.
///
/// `attr_span` is used for emitting error messages when no better span
/// can be constructed. This should point at the `#[xml(..)]` meta of the
/// field or another closely-related object.
pub(super) fn new(
attr_span: &Span,
field_ident: Option<&Ident>,
name: Option<NameRef>,
default_on_missing: Flag,
) -> Result<Self> {
let name = name
.map(Name::Lit)
.or_else(|| field_ident.map(|ident| Name::Ident(ident.clone())));
let Some(name) = name else {
return Err(Error::new(attr_span.clone(), "missing attribute name on unnamed field. specify using #[xml(attribute = \"foo\")]"));
};
Ok(Self {
name,
default_on_missing,
})
}
/// Construct the code necessary to parse the attribute from a
/// `minidom::Element` into a field.
///
/// `ty` must be the field's type. The other arguments are currently
/// ignored.
pub(super) fn build_try_from_element(
self,
_name: Option<&Path>,
_tempname: Ident,
_ident: Member,
ty: Type,
) -> Result<FieldParsePart> {
let name = self.name;
let missing_msg = format!("Required attribute '{}' missing.", name);
let on_missing = if self.default_on_missing.is_set() {
quote! {
<#ty as ::std::default::Default>::default()
}
} else {
quote! {
return Err(::xso::error::Error::ParseError(
#missing_msg,
))
}
};
Ok(FieldParsePart {
attrcheck: quote! {
if key == #name {
continue;
}
},
value: quote! {
match <#ty as ::xso::FromAttribute>::from_attribute(
residual.attr(#name)
)? {
Some(v) => v,
None => #on_missing,
}
},
..FieldParsePart::default()
})
}
/// Construct an expression which consumes the ident `builder`, which must
/// be a minidom `Builder`, and returns it, modified in such a way that it
/// contains the field's data.
///
/// - `ident` must be an expression to consume the field's data. It is
/// evaluated exactly once.
/// - `ty` must be the field's type.
pub(super) fn build_into_element(self, ident: Expr, ty: Type) -> Result<TokenStream> {
let name = self.name;
Ok(quote! {
match <#ty as ::xso::exports::minidom::IntoAttributeValue>::into_attribute_value(#ident) {
Some(v) => builder.attr(#name, v),
None => builder,
}
})
}
}

532
xso-proc/src/field/child.rs Normal file
View File

@ -0,0 +1,532 @@
//! Infrastructure for parsing fields from child elements while destructuring
//! their contents.
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::*;
use crate::compound::Compound;
use crate::meta::{ExtractMeta, Flag, NameRef, NamespaceRef, StaticNamespace, XmlCompoundMeta};
use super::{field_name, ChildMode, FieldDef, FieldParsePart};
/// Definition of a child data extraction.
///
/// This is used to implement fields annotated with
/// `#[xml(child(.., extract(..))]` or `#[xml(children(.., extract(..)))]`.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(super) struct ExtractDef {
/// Compound which contains the arguments of the `extract(..)` attribute,
/// transformed into a struct with unnamed fields.
///
/// This is used to generate the parsing/serialisation code, by
/// essentially "declaring" a shim struct, as if it were a real Rust
/// struct, and using the result of the parsing process directly for the
/// field on which the `extract(..)` option was used, instead of putting
/// it into a Rust struct.
pub(super) parts: Compound,
}
impl ExtractDef {
/// Construct an `ExtractDef`.
///
/// The `namespace` and `name` identify the XML element this `ExtractDef`
/// works on, i.e. the child element to match.
///
/// `parts` contains the pieces of data to extract from the child in the
/// order they are extracted.
fn new(namespace: StaticNamespace, name: NameRef, parts: Vec<ExtractMeta>) -> Result<Self> {
let meta = XmlCompoundMeta {
span: name.span(),
namespace: Some(NamespaceRef::Static(namespace.clone())),
name: Some(name.clone()),
fallback: Flag::Absent,
transparent: Flag::Absent,
exhaustive: Flag::Absent,
validate: None,
prepare: None,
};
let parts = Compound::new_struct(
meta,
parts
.into_iter()
.enumerate()
.map(|(i, x)| FieldDef::from_extract(x, i as u32)),
)?;
Ok(Self { parts })
}
/// Construct a token stream containing an expression which tries to
/// process the child element at the identifier `residual`.
///
/// `target_ty` must be set to the Type of the field the result of this
/// extraction is going to be assigned to if and only if it is used in the
/// context of a `#[xml(child(.., extract(..)))]` (as opposed to
/// `#[xml(children(..))]`).
///
/// The value of the expression is a `Result<T, Element>`. If the
/// element in `residual` does not match the XML namespace and name of
/// this extract definition, it is returned as `Err(#residual)`.
///
/// Otherwise, the element is destructured according to the extraction
/// specification contained in `self`. The type of `T` varies with the
/// exact specification and context:
///
/// - If this extract only obtains a single part **and** `target_ty` is
/// given:
/// - If the extracted part is text-like (an attribute or the text
/// contents of the child), the part is converted to
/// `Option<#target_ty>` via `FromAttribute`.
/// - Otherwise, the part is returned as `Some(..)`.
/// - Otherwise, if this extract only contains a single part and
/// `target_ty` is **not** given, the value is returned as directly.
/// - Otherwise, if this extract contains more than one part, the parts
/// are returned together in a tuple.
///
/// This complexity is currently necessary in order to handle the
/// different cases of extraction; generally, the user of the derive
/// macros wants to be able to use the standard `FromAttribute`
/// conversion for extracted values.
///
/// However, some values are not text-based, e.g. when `extract(elements)`
/// is used or, indeed, multiple parts are being extracted. In that case,
/// `FromAttribute` could not be used and attempting to use it would lead
/// to type errors.
///
/// Because we cannot switch based on the traits implemented by the target
/// type, we have to use these somewhat complex heuristics (and document
/// them as "limitations" in the API/attribute reference).
///
/// It works nicely though.
fn build_extract(self, residual: &Ident, target_ty: Option<&Type>) -> Result<TokenStream> {
let nfields = self.parts.field_count().unwrap();
let repack = if nfields == 1 {
if let Some(target_ty) = target_ty {
// special case: single-field extract on a #[child(..)] is
// treated extra-special and the conversion to the target type
// is handled here, because we know what kind of field this
// is.
match self.parts {
Compound::Struct { ref fields, .. } => {
if fields[0].kind.is_text_like() {
quote! {
<#target_ty as ::xso::FromAttribute>::from_attribute(Some(data.0.as_str()))?
}
} else {
quote! { Some(data.0) }
}
}
_ => unreachable!(),
}
} else {
quote! { data.0 }
}
} else {
quote! { data }
};
let build = self.parts.build_try_from_element(None, None, residual)?;
Ok(quote! {
match #build {
Ok(data) => Ok(#repack),
Err(e) => Err(e),
}
})
}
/// Construct a token stream containing an expression evaluating to a
/// `Option<minidom::Element>`.
///
/// This is the reverse operation of
/// [`build_extract`][`Self::build_extract`], and like in that function,
/// there are weird edge cases in here.
///
/// `field` must be the expression which contains the extracted field's
/// data. It is evaluated exactly once.
///
/// If `target_ty` is given, this extract contains exactly one part and
/// that part is text-like, the value of `field` will be converted into
/// a string using the `IntoAttributeValue` trait of `target_ty`.
///
/// Otherwise, the value is used nearly as-is and forwarded to the
/// implementation returned by [`Compound::build_into_element`].
fn build_assemble(self, field: &Expr, target_ty: Option<&Type>) -> Result<TokenStream> {
let nfields = self.parts.field_count().unwrap();
let repack = if nfields == 1 {
quote! { let data = (data,); }
} else {
quote! { let data = data; }
};
let ident = Ident::new("data", Span::call_site());
let eval = if nfields == 1 {
if let Some(target_ty) = target_ty {
// special case: single-field extract on a #[child(..)] is
// treated extra-special and the conversion to the target type
// is handled here, because we know what kind of field this
// is.
match self.parts {
Compound::Struct { ref fields, .. } => {
if fields[0].kind.is_text_like() {
quote! {
<#target_ty as ::xso::exports::minidom::IntoAttributeValue>::into_attribute_value(#field)
}
} else {
quote! { Some(#field) }
}
}
_ => unreachable!(),
}
} else {
quote! {
Some(#field)
}
}
} else {
quote! { Some(#field) }
};
let build = self.parts.build_into_element(None, None, |member| {
Expr::Field(ExprField {
attrs: Vec::new(),
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! {
{
match #eval {
Some(data) => {
#repack
Some(#build)
}
None => None
}
}
})
}
}
/// A field parsed from an XML child, destructured into a Rust data structure.
///
/// Maps to `#[xml(child)]` and `#[xml(children)]`.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) struct ChildField {
/// Determines whether one or more matching child elements are expected.
///
/// This is basically the difference between `#[xml(child(..))]` and
/// `#[xml(children(..))]`.
pub(super) mode: ChildMode,
/// If set, the field's value will be obtained by destructuring the child
/// element using the given [`ExtractDef`], instead of parsing it using
/// `FromXml`.
pub(super) extract: Option<ExtractDef>,
/// If set, the field's value will be generated using
/// [`std::default::Default`] if no matching child can be found, instead
/// of aborting parsing with an error.
pub(super) default_on_missing: Flag,
}
impl ChildField {
/// Construct a new `#[xml(child)]` or `#[xml(children)]` field.
///
/// `mode` distinguishes between `#[xml(child(..))]` and
/// `#[xml(children(..))]` fields.
///
/// If the child is going to be extracted, it `namespace` and `name` must
/// identify the target child's XML namespace and name and `extract` must
/// be the extraction parts to process.
///
/// Otherwise, if no extract is intended, `namespace` and `name` must be
/// `None` and `extract` must be empty.
///
/// The `default_on_missing` flag stored, see [`Self::default_on_missing`]
/// for semantics.
///
/// `attr_span` is used for emitting error messages when no better span
/// can be constructed. This should point at the `#[xml(..)]` meta of the
/// field or another closely-related object.
pub(super) fn new(
attr_span: &Span,
mode: ChildMode,
namespace: Option<NamespaceRef>,
name: Option<NameRef>,
extract: Vec<ExtractMeta>,
default_on_missing: Flag,
) -> Result<Self> {
if extract.len() > 0 {
let namespace = match namespace {
None => {
return Err(Error::new(
attr_span.clone(),
"namespace must be specified on extracted fields",
))
}
Some(NamespaceRef::Static(ns)) => ns,
Some(NamespaceRef::Dyn(ns)) => {
return Err(Error::new_spanned(
ns,
"extracted fields cannot use dynamic namespaces",
))
}
Some(NamespaceRef::Super(ns)) => {
return Err(Error::new_spanned(
ns,
"extracted fields cannot refer to parent namespaces",
))
}
};
let Some(name) = name else {
return Err(Error::new(
attr_span.clone(),
"name must be specified on extracted fields",
));
};
match mode {
ChildMode::Single => {
if extract.len() > 1 {
return Err(Error::new(
attr_span.clone(),
"extracting multiple texts from children is only on collection fields",
));
}
}
ChildMode::Collection => (),
}
Ok(Self {
mode,
extract: Some(ExtractDef::new(namespace, name.into(), extract)?),
default_on_missing,
})
} else {
if let Some(namespace) = namespace {
return Err(Error::new_spanned(
namespace,
"namespace declaration not allowed on non-extracted child fields",
));
}
if let Some(name) = name {
return Err(Error::new_spanned(
name,
"name declaration not allowed on non-extracted child fields",
));
}
Ok(Self {
mode,
extract: None,
default_on_missing,
})
}
}
/// Construct the code necessary to parse the attribute from a
/// `minidom::Element` into a field.
///
/// - `name` should be the identifier or path of the containing compound
/// and is used to generate runtime error messages.
///
/// - `tempname` must be an identifier which the implementation can freely
/// use for a temporary variable related to parsing.
///
/// - `ident` must be the target struct member identifier or index, used
/// for error messages.
///
/// - `ty` must be the field's type. If this field is not an extract, the
/// type's `FromXml` implementation is used to destructure matching XML
/// element(s).
pub(super) fn build_try_from_element(
self,
name: Option<&Path>,
tempname: Ident,
ident: Member,
ty: Type,
) -> Result<FieldParsePart> {
match self.mode {
ChildMode::Single => {
let missingerr = format!(
"Missing child {} in {} element.",
field_name(&ident),
name.to_token_stream()
);
let duperr = format!(
"Element {} must not have more than one {} child.",
name.to_token_stream(),
field_name(&ident)
);
let on_missing = if self.default_on_missing.is_set() {
quote! {
<#ty as ::std::default::Default>::default()
}
} else {
quote! {
return Err(::xso::error::Error::ParseError(#missingerr));
}
};
match self.extract {
Some(extract) => {
let extract = extract
.build_extract(&Ident::new("residual", Span::call_site()), Some(&ty))?;
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Option<#ty> = None;
},
childiter: quote! {
residual = match #extract {
Ok(v) => {
if #tempname.is_some() {
return Err(::xso::error::Error::ParseError(#duperr));
}
#tempname = v;
continue;
},
Err(residual) => residual,
};
},
value: quote! {
if let Some(v) = #tempname {
v
} else {
#on_missing
}
},
..FieldParsePart::default()
})
}
None => Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Option<#ty> = None;
},
childiter: quote! {
let mut residual = match <#ty as ::xso::FromXml>::from_tree(residual) {
Ok(v) => {
if #tempname.is_some() {
return Err(::xso::error::Error::ParseError(#duperr));
}
#tempname = Some(v);
continue
}
Err(::xso::error::Error::TypeMismatch(_, _, e)) => e,
Err(other) => return Err(other),
};
},
value: quote! {
if let Some(v) = #tempname {
v
} else if let Some(v) = <#ty as ::xso::FromXml>::absent() {
v
} else {
#on_missing
}
},
..FieldParsePart::default()
}),
}
}
ChildMode::Collection => match self.extract {
Some(extract) => {
let extract =
extract.build_extract(&Ident::new("residual", Span::call_site()), None)?;
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname = <#ty as ::xso::XmlDataCollection>::new_empty();
},
childiter: quote! {
residual = match #extract {
Ok(v) => {
let v = <<#ty as ::xso::XmlDataCollection>::Item as ::xso::XmlDataItem<<#ty as ::xso::XmlDataCollection>::Input>>::from_raw(v)?;
<#ty as ::xso::XmlDataCollection>::append_data(&mut #tempname, v);
continue;
},
Err(residual) => residual,
};
},
value: quote! { #tempname },
..FieldParsePart::default()
})
}
None => Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname = <#ty as ::xso::XmlCollection>::new_empty();
},
childiter: quote! {
let mut residual = match <#ty as ::xso::XmlCollection>::try_append(&mut #tempname, residual) {
Ok(()) => continue,
Err(::xso::error::Error::TypeMismatch(_, _, e)) => e,
Err(other) => return Err(other),
};
},
value: quote! { #tempname },
..FieldParsePart::default()
}),
},
}
}
/// Construct an expression which consumes the ident `builder`, which must
/// be a minidom `Builder`, and returns it, modified in such a way that it
/// contains the field's data.
///
/// - `ident` must be an expression to consume the field's data. It is
/// evaluated exactly once.
/// - `ty` must be the field's type.
pub(super) fn build_into_element(self, ident: Expr, ty: Type) -> Result<TokenStream> {
match self.mode {
ChildMode::Single => match self.extract {
Some(extract) => {
let assemble = extract.build_assemble(&ident, Some(&ty))?;
Ok(quote! {
match #assemble {
Some(el) => builder.append(::xso::exports::minidom::Node::Element(el)),
None => builder,
}
})
}
None => Ok(quote! {
match <#ty as ::xso::IntoXml>::into_tree(#ident) {
Some(el) => builder.append(::xso::exports::minidom::Node::Element(el)),
None => builder,
}
}),
},
ChildMode::Collection => match self.extract {
Some(extract) => {
let assemble = extract.build_assemble(
&Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Path::from(Ident::new("data", Span::call_site())),
}),
None,
)?;
Ok(quote! {
builder.append_all(
#ident.into_iter().filter_map(|item| {
let data = <<#ty as ::xso::XmlDataCollection>::Item as ::xso::XmlDataItem<<#ty as ::xso::XmlDataCollection>::Input>>::into_raw(item);
#assemble
})
)
})
}
None => Ok(quote! {
builder.append_all(#ident.into_iter().filter_map(|item| {
::xso::IntoXml::into_tree(item).map(|el| ::xso::exports::minidom::Node::Element(el))
}))
}),
},
}
}
}

View File

@ -0,0 +1,312 @@
//! Infrastructure for parsing fields from child elements without
//! destructuring their contents.
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::*;
use crate::meta::{Flag, Name, NameRef, NamespaceRef, StaticNamespace};
use super::FieldParsePart;
/// A field parsed from an XML child, without destructuring it into Rust
/// data structures.
///
/// Maps to `#[xml(element)]`.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) struct ElementField {
/// The XML namespace of the child element to collect.
namespace: StaticNamespace,
/// The XML name of the child element to collect.
name: Name,
/// If set, the field value will be generated using
/// [`std::default::Default`] if no matching child element is encountered
/// during parsing. If unset, an error is generated instead and parsing of
/// the parent element fails.
default_on_missing: Flag,
}
impl ElementField {
/// Construct a new `#[xml(element)]` field.
///
/// `namespace` must be a [`NamespaceRef::Static`] describing the
/// XML namespace of the target child element. Otherwise, a compile-time
/// error is returned.
///
/// `name` must be a [`NameRef`] describing the XML name of the target
/// child element. Otherwise, a compile-time error is returned.
///
/// `attr_span` is used for emitting error messages when no better span
/// can be constructed. This should point at the `#[xml(..)]` meta of the
/// field or another closely-related object.
pub(super) fn new(
attr_span: &Span,
namespace: Option<NamespaceRef>,
name: Option<NameRef>,
default_on_missing: Flag,
) -> Result<Self> {
let namespace = match namespace {
None => {
return Err(Error::new(
attr_span.clone(),
"#[xml(element)] requires namespace attribute",
))
}
Some(NamespaceRef::Static(ns)) => ns,
Some(NamespaceRef::Dyn(ns)) => {
return Err(Error::new_spanned(
ns,
"dynamic namespaces cannot be used with #[xml(element)]",
))
}
Some(NamespaceRef::Super(ns)) => {
return Err(Error::new_spanned(
ns,
"collected elements cannot refer to the parent namespace",
))
}
};
let name = match name {
None => {
return Err(Error::new(
attr_span.clone(),
"#[xml(element)] requires name attribute",
))
}
Some(name) => name,
};
Ok(Self {
namespace,
name: name.into(),
default_on_missing,
})
}
/// Construct the code necessary to parse the child element from a
/// `minidom::Element` into a field.
///
/// - `name` should be the identifier or path of the containing compound
/// and is used to generate runtime error messages.
///
/// - `tempname` must be an identifier which the implementation can freely
/// use for a temporary variable related to parsing.
///
/// - `ident` must be the target struct member identifier or index, used
/// for error messages.
///
/// - `ty` must be the field's type. This type must implement
/// `From<Element>`. If [`Self::default_on_missing`] is set, it also
/// needs to implement `Default`.
pub(super) fn build_try_from_element(
self,
name: Option<&Path>,
tempname: Ident,
_ident: Member,
ty: Type,
) -> Result<FieldParsePart> {
let field_name = self.name;
let field_namespace = self.namespace;
let missingerr = format!(
"Missing child {} in {} element.",
field_name,
name.to_token_stream()
);
let duperr = format!(
"Element {} must not have more than one {} child.",
name.to_token_stream(),
field_name,
);
let on_missing = if self.default_on_missing.is_set() {
quote! {
<#ty as ::std::default::Default>::default()
}
} else {
quote! {
return Err(::xso::error::Error::ParseError(#missingerr));
}
};
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Option<::xso::exports::minidom::Element> = None;
},
childiter: quote! {
residual = if residual.is(#field_name, #field_namespace) {
if #tempname.is_some() {
return Err(::xso::error::Error::ParseError(#duperr));
}
#tempname = Some(residual);
continue;
} else {
residual
};
},
value: quote! {
if let Some(v) = #tempname {
<#ty as From<::xso::exports::minidom::Element>>::from(v)
} else {
#on_missing
}
},
..FieldParsePart::default()
})
}
/// Construct an expression which consumes the ident `builder`, which must
/// be a minidom `Builder`, and returns it, modified in such a way that it
/// contains the field's data.
///
/// - `ident` must be an expression to consume the field's data. It is
/// evaluated exactly once.
/// - `ty` must be the field's type. This type must implement
/// `Into<Option<Element>>`.
pub(super) fn build_into_element(self, ident: Expr, ty: Type) -> Result<TokenStream> {
Ok(quote! {
match <#ty as Into<Option<::xso::exports::minidom::Element>>>::into(#ident) {
Some(v) => builder.append(::xso::exports::minidom::Node::Element(v)),
None => builder
}
})
}
}
/// A field parsed from a XML children, without destructuring them into Rust
/// data structures.
///
/// Maps to `#[xml(elements)]`.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) struct ElementsField {
/// Selector to choose the child elements to collect.
///
/// - If `None`, *all* children are collected. This is equivalent to
/// `#[xml(elements)]` on a field and may only occur once in a compound.
/// - If `Some((ns, None))`, all children matching the namespace,
/// irrespective of the XML name, are collected.
/// - If `Some((ns, Some(name)))`, only children matching the namespace
/// and name are collected.
pub(super) selector: Option<(StaticNamespace, Option<Name>)>,
}
impl ElementsField {
/// Construct a new `#[xml(elements)]`.
///
/// `namespace` and `name` are optional selectors for XML child elements
/// to match. If `namespace` is not set, `name` must not be set either,
/// or a compile-time error is returned.
///
/// `attr_span` is used for emitting error messages when no better span
/// can be constructed. This should point at the `#[xml(..)]` meta of the
/// field or another closely-related object.
pub(super) fn new(
_attr_span: &Span,
namespace: Option<NamespaceRef>,
name: Option<NameRef>,
) -> Result<Self> {
let namespace = match namespace {
None => {
if let Some(name) = name {
return Err(Error::new_spanned(
name,
"#[xml(elements(..))] cannot be used with an unnamespaced name",
));
}
None
}
Some(NamespaceRef::Static(ns)) => Some(ns),
Some(NamespaceRef::Dyn(ns)) => {
return Err(Error::new_spanned(
ns,
"dynamic namespaces cannot be used with #[xml(elements)]",
))
}
Some(NamespaceRef::Super(ns)) => {
return Err(Error::new_spanned(
ns,
"collected elements cannot refer to the parent namespace",
))
}
};
Ok(Self {
selector: namespace.map(|x| (x, name.map(|x| x.into()))),
})
}
/// Construct the code necessary to parse the child elements from a
/// `minidom::Element` into a field.
///
/// - `name` should be the identifier or path of the containing compound
/// and is used to generate runtime error messages.
///
/// - `tempname` must be an identifier which the implementation can freely
/// use for a temporary variable related to parsing.
///
/// - `ident` must be the target struct member identifier or index, used
/// for error messages.
///
/// - `ty` must be the field's type. This must be `Vec<minidom::Element>`,
/// otherwise compile-time errors will follow.
pub(super) fn build_try_from_element(
self,
_name: Option<&Path>,
tempname: Ident,
_ident: Member,
_ty: Type,
) -> Result<FieldParsePart> {
match self.selector {
Some((field_namespace, field_name)) => {
let childiter = match field_name {
// namespace match only
None => quote! {
residual = if residual.has_ns(#field_namespace) {
#tempname.push(residual);
continue;
} else {
residual
};
},
Some(field_name) => quote! {
residual = if residual.is(#field_name, #field_namespace) {
#tempname.push(residual);
continue;
} else {
residual
};
},
};
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Vec<::xso::exports::minidom::Element> = Vec::new();
},
childiter,
value: quote! { #tempname },
..FieldParsePart::default()
})
}
None => Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Vec<::xso::exports::minidom::Element> = Vec::new();
},
childfallback: Some(quote! {
#tempname.push(residual);
}),
value: quote! { #tempname },
..FieldParsePart::default()
}),
}
}
/// Construct an expression which consumes the ident `builder`, which must
/// be a minidom `Builder`, and returns it, modified in such a way that it
/// contains the field's data.
///
/// - `ident` must be an expression to consume the field's data. It is
/// evaluated exactly once.
/// - `ty` must be the field's type.
pub(super) fn build_into_element(self, ident: Expr, _ty: Type) -> Result<TokenStream> {
Ok(quote! {
builder.append_all(#ident.into_iter().map(|elem| ::xso::exports::minidom::Node::Element(elem)))
})
}
}

View File

@ -0,0 +1,28 @@
use std::fmt;
pub(super) enum ParentRef {
Named(Path),
Unnamed {
parent: Box<ParentRef>,
field: Member,
},
}
impl fmt::Display for ParentRef {
fn fmt<'f>(&self, f: &'f mut fmt::Formatter) -> fmt::Result {
match self {
Self::Named(name) => write!(f, "{} element", name.to_token_stream()),
Self::Unnamed {
parent,
field,
} => write!(f, "extraction for child {} in {}", field, parent),
}
}
}
pub(super) fn on_missing_child(
field: &Member,
parent_name: ParentRef,
) -> String {
format!("Missing child {} in {}.", field_name(field), parent_name)
}

140
xso-proc/src/field/flag.rs Normal file
View File

@ -0,0 +1,140 @@
//! Infrastructure for parsing boolean fields indicating child presence.
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::*;
use crate::meta::{Name, NameRef, NamespaceRef, StaticNamespace};
use super::FieldParsePart;
/// A field parsed from the presence of an empty XML child.
///
/// Maps to `#[xml(flag)]`.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) struct FlagField {
/// The XML namespace of the child element to look for.
namespace: StaticNamespace,
/// The XML name of the child element to look for.
name: Name,
}
impl FlagField {
/// Construct a new `#[xml(flag)]` field.
///
/// `namespace` and `name` must both be set and `namespace` must be a
/// [`NamespaceRef::Static`].
///
/// `attr_span` is used for emitting error messages when no better span
/// can be constructed. This should point at the `#[xml(..)]` meta of the
/// field or another closely-related object.
pub(super) fn new(
attr_span: &Span,
namespace: Option<NamespaceRef>,
name: Option<NameRef>,
) -> Result<Self> {
let namespace = match namespace {
None => {
return Err(Error::new(
attr_span.clone(),
"#[xml(flag)] requires namespace attribute",
))
}
Some(NamespaceRef::Static(ns)) => ns,
Some(NamespaceRef::Dyn(ns)) => {
return Err(Error::new_spanned(
ns,
"dynamic namespaces cannot be used with #[xml(flag)]",
))
}
Some(NamespaceRef::Super(ns)) => {
return Err(Error::new_spanned(
ns,
"flag elements cannot refer to the parent namespace",
))
}
};
let name = match name {
None => {
return Err(Error::new(
attr_span.clone(),
"#[xml(flag)] requires name attribute",
))
}
Some(name) => name,
};
Ok(Self {
namespace,
name: name.into(),
})
}
/// Construct the code necessary to parse the child element from a
/// `minidom::Element` into a field.
///
/// - `name` should be the identifier or path of the containing compound
/// and is used to generate runtime error messages.
///
/// - `tempname` must be an identifier which the implementation can freely
/// use for a temporary variable related to parsing.
///
/// - `ident` must be the target struct member identifier or index, used
/// for error messages.
///
/// - `ty` must be the field's type. This must be `bool`.
pub(super) fn build_try_from_element(
self,
name: Option<&Path>,
tempname: Ident,
_ident: Member,
_ty: Type,
) -> Result<FieldParsePart> {
let field_name = self.name;
let field_namespace = self.namespace;
let duperr = format!(
"Element {} must not have more than one {} child.",
name.to_token_stream(),
field_name,
);
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname = false;
},
childiter: quote! {
residual = if residual.is(#field_name, #field_namespace) {
// TODO: reject contents
if #tempname {
return Err(::xso::error::Error::ParseError(#duperr));
}
#tempname = true;
continue;
} else {
residual
};
},
value: quote! { #tempname },
..FieldParsePart::default()
})
}
/// Construct an expression which consumes the ident `builder`, which must
/// be a minidom `Builder`, and returns it, modified in such a way that it
/// contains the field's data.
///
/// - `ident` must be an expression to consume the field's data. It is
/// evaluated exactly once.
/// - `ty` must be the field's type. This type must implement
/// `Into<Option<Element>>`.
pub(super) fn build_into_element(self, ident: Expr, _ty: Type) -> Result<TokenStream> {
let child_name = self.name;
let child_namespace = self.namespace;
Ok(quote! {
if #ident {
builder.append(::xso::exports::minidom::Node::Element(::xso::exports::minidom::Element::builder(#child_name, #child_namespace).build()))
} else {
builder
}
})
}
}

370
xso-proc/src/field/mod.rs Normal file
View File

@ -0,0 +1,370 @@
/*!
Infrastructure for processing fields of compounds.
This module contains the Infrastructure necessary to parse Rust fields from
XML and convert them back to XML.
The main file of the module contains the outward, generic types covering all
use cases. The actual implementations for all but the trivial field kinds are
sorted into submodules.
*/
mod attribute;
mod child;
mod element;
mod flag;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{spanned::Spanned, *};
use crate::meta::{ChildMode, ExtractMeta, Flag, XmlFieldMeta};
use crate::compound::Compound;
use self::attribute::AttributeField;
use self::child::ChildField;
use self::element::{ElementField, ElementsField};
use self::flag::FlagField;
/// Provide some kind of human-useful string from a [`Member`].
fn field_name(m: &Member) -> String {
match m {
Member::Named(v) => v.to_string(),
Member::Unnamed(v) => format!("<unnamed {}>", v.index),
}
}
/// Code slices necessary for parsing a single field.
#[derive(Default)]
pub(crate) struct FieldParsePart {
/// Zero or more statements which check whether an attribute is allowed.
/// These should be `if` statements where the body consists of `continue`
/// if and only if that attribute is allowed.
///
/// Typically only generated by FieldKind::Attribute.
pub(crate) attrcheck: TokenStream,
/// Zero or more statements to initialize a temporary variable before the
/// child element parsing loop.
///
/// For child-element-related fields, this will generally create a
/// temporary `Option<..>` or `Vec<..>` or so, into which the target
/// data is then collected.
pub(crate) tempinit: TokenStream,
/// An expression, which consumes the identifier `residual` and either
/// returns it unchanged, calls `continue`, or returns with an error.
///
/// Generally, this will be some kind of
/// `if residual.is(..) { .. } else { residual }` construct.
pub(crate) childiter: TokenStream,
/// Zero or more statements which are added to the end of the child
/// parsing loop.
///
/// Only one field may return this. If multiple fields return this, the
/// derive macro panics. The invariant that only one field may emit this
/// should already be enforced by `Compound::new_struct`.
pub(crate) childfallback: Option<TokenStream>,
/// The expression which evaluates to the final value of the field.
pub(crate) value: TokenStream,
}
/// Enumeration of possible XML data ↔ Rust field mappings.
///
/// This matches the processed `#[xml(..)]` metas on the corresponding enum
/// variant or struct fields.
///
/// This enum only covers the parsing logic; the surrounding metadata, such as
/// the field identifier and type, are contained in [`FieldDef`].
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum FieldKind {
/// This field is parsed from an XML attribute (`#[xml(attribute)]`).
Attribute(AttributeField),
/// This field is parsed from XML text content (`#[xml(text)]`).
Text,
/// This field represents the parent compound's namespace
/// (`#[xml(namespace)]`).
///
/// See also
/// [`CompoundNamespace::Dyn`][`crate::compound::CompoundNamespace::Dyn`].
///
/// Not implemented yet.
Namespace,
/// This field is parsed from an XML child using the `FromXml` /
/// `IntoXml` traits (`#[xml(child)]`, `#[xml(children)]`).
Child(ChildField),
/// This field is parsed from an XML child as raw `minidom::Element`
/// (`#[xml(element)]`).
Element(ElementField),
/// This field is parsed from multiple XML children as raw
/// `Vec<minidom::Element>` (`#[xml(elements)]`).
Elements(ElementsField),
/// This field represents the presence/absence of an empty XML child
/// (`#[xml(flag)]`).
Flag(FlagField),
/// This field is not parsed to/from XML.
Ignore,
}
impl FieldKind {
/// Return true if the field kind is equal to [`Self::Text`].
pub(crate) fn is_text(&self) -> bool {
match self {
Self::Text => true,
_ => false,
}
}
/// Return true if the field kind is equal to [`Self::Namespace`].
pub(crate) fn is_namespace(&self) -> bool {
match self {
Self::Namespace => true,
_ => false,
}
}
/// Return true if the field kind is a [`Self::Elements`] which matches
/// all child elements.
pub(crate) fn is_collect_wildcard(&self) -> bool {
match self {
Self::Elements(ElementsField { selector: None }) => true,
_ => false,
}
}
/// Return true if the field processes XML text data, i.e. is an
/// attribute, a text, or a single child-extracted text-like field.
pub(crate) fn is_text_like(&self) -> bool {
match self {
Self::Text => true,
Self::Attribute { .. } => true,
Self::Child(ChildField {
mode: ChildMode::Single,
extract: Some(extract),
..
}) => match extract.parts {
Compound::Transparent { .. } => false,
Compound::Struct { ref fields, .. } => {
fields.len() == 1 && fields[0].kind.is_text_like()
}
},
_ => false,
}
}
}
impl TryFrom<ExtractMeta> for FieldKind {
type Error = Error;
fn try_from(other: ExtractMeta) -> Result<Self> {
match other {
ExtractMeta::Attribute { name } => Ok(Self::Attribute(AttributeField {
name: name.into(),
default_on_missing: Flag::Absent,
})),
ExtractMeta::Text => Ok(Self::Text),
ExtractMeta::Elements => Ok(Self::Elements(ElementsField { selector: None })),
}
}
}
/// All data necessary to generate code to convert a Rust field to or from
/// XML.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) struct FieldDef {
/// The identifier (in a named compound) or index (in an unnamed/tuple-like
/// compound) of the field.
pub(crate) ident: Member,
/// The type of the field.
pub(crate) ty: Type,
/// The way the field is mapped to XML.
pub(crate) kind: FieldKind,
}
impl FieldDef {
/// Generate a [`FieldDef`] from an [`ExtractMeta`] specification.
///
/// This is used to build a [`crate::compound::Compound`] used to
fn from_extract(extract: ExtractMeta, index: u32) -> Result<Self> {
Ok(Self {
ident: Member::Unnamed(Index {
index,
span: Span::call_site(),
}),
ty: parse_str("::std::string::String").expect("cannot construct string type"),
kind: extract.try_into()?,
})
}
/// Generate a [`FieldDef`] from a [`Field`].
///
/// `index` must be the number of the field within the compound, starting
/// at zero. It is used only for unnamed fields.
///
/// This parses the attributes using [`XmlFieldMeta`].
pub(crate) fn from_field(field: &Field, index: u32) -> Result<Self> {
let mut meta: Option<(XmlFieldMeta, Span)> = None;
for attr in field.attrs.iter() {
if !attr.path().is_ident("xml") {
continue;
}
if meta.is_some() {
return Err(Error::new_spanned(
attr,
"only one #[xml(..)] attribute per field allowed.",
));
}
meta = Some((XmlFieldMeta::parse_from_attribute(attr)?, attr.span()));
}
let Some((meta, span)) = meta else {
return Err(Error::new_spanned(
field,
"exactly one #[xml(..)] attribute per field required.",
));
};
let ident: Member = match field.ident.as_ref() {
Some(v) => Member::Named(v.clone()),
None => Member::Unnamed(Index {
index,
span: Span::call_site(),
}),
};
let kind = match meta {
XmlFieldMeta::Attribute {
name,
default_on_missing,
} => FieldKind::Attribute(AttributeField::new(
&span,
field.ident.as_ref(),
name,
default_on_missing,
)?),
XmlFieldMeta::Child {
mode,
namespace,
name,
extract,
default_on_missing,
} => FieldKind::Child(ChildField::new(
&span,
mode,
namespace,
name,
extract,
default_on_missing,
)?),
XmlFieldMeta::Text => FieldKind::Text,
XmlFieldMeta::Namespace => FieldKind::Namespace,
XmlFieldMeta::Element {
namespace,
name,
default_on_missing,
} => FieldKind::Element(ElementField::new(
&span,
namespace,
name,
default_on_missing,
)?),
XmlFieldMeta::Elements { namespace, name } => {
FieldKind::Elements(ElementsField::new(&span, namespace, name)?)
}
XmlFieldMeta::Flag { namespace, name } => {
FieldKind::Flag(FlagField::new(&span, namespace, name)?)
}
XmlFieldMeta::Ignore => FieldKind::Ignore,
};
Ok(Self {
ident,
ty: field.ty.clone(),
kind,
})
}
/// Construct a [`FieldParsePart`] which creates the field's value from
/// XML.
pub(crate) fn build_try_from_element(self, name: Option<&Path>) -> 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),
FieldKind::Text => Ok(FieldParsePart {
tempinit: quote! {
let #tempname: #ty = <#ty as ::xso::FromText>::from_text(&residual.text())?;
},
value: quote! { #tempname },
..FieldParsePart::default()
}),
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 => {
return Err(Error::new_spanned(
ident,
"dynamic namespaces not implemented yet.",
))
}
FieldKind::Ignore => Ok(FieldParsePart {
value: quote! { <#ty as ::std::default::Default>::default() },
..FieldParsePart::default()
}),
}
}
/// Construct an expression which consumes the ident `builder` and returns
/// it, after modifying it to contain the field's data.
///
/// The field's data is accessed using the `Expr` returned by
/// `access_field` when passed the identifier or index of the field.
///
/// The caller is responsible to set up the enviroment where the returned
/// `TokenStream` is used so that the expressions returned by
/// `access_field` and the `builder` ident are accessible.
pub(crate) fn build_into_element(
self,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
let ident = access_field(self.ident);
let ty = self.ty;
match self.kind {
FieldKind::Attribute(field) => field.build_into_element(ident, ty),
FieldKind::Text => Ok(quote! {
{
let __text = <#ty as ::xso::IntoText>::into_text(#ident);
if __text.len() > 0 {
builder.append(::xso::exports::minidom::Node::Text(__text))
} else {
builder
}
}
}),
FieldKind::Child(field) => field.build_into_element(ident, ty),
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),
FieldKind::Namespace => Err(Error::new_spanned(
ident,
"dynamic namespaces not implemented yet.",
)),
FieldKind::Ignore => Ok(quote! { builder }),
}
}
}

94
xso-proc/src/lib.rs Normal file
View File

@ -0,0 +1,94 @@
/*!
# Macros for parsing XML into Rust structs, and vice versa
**If you are a user of `xso_proc` or `xso`, please
return to `xso` for more information**. The documentation of
`xso_proc` is geared toward developers of `_macros` and `_core`.
**You have been warned.**
## How the derive macros work
The processing is roughly grouped in the following stages:
1. [`syn`] is used to parse the incoming [`TokenStream`] into a [`syn::Item`].
Based on that, the decision is made whether a struct or an enum is being
derived on.
2. Depending on the item type (enum vs. struct), the further processing is
delegated to the function matching the invoked derive from
[`crate::structs`] or [`crate::enums`]. Nontheless, much of the remaining
processing is equivalent.
3. The [`crate::meta::XmlCompoundMeta`] type is used to convert the
raw token streams from the `#[xml(..)]` attributes into structs/enums for
easier handling.
That stage only does syntax checks, no (or just little) semantics. This
separation of concerns helps with simplifying the code both in `meta` and
the following modules.
4. Enum variants and structs are processed using
[`crate::compound::Compound`], their fields being converted from [`crate::meta::XmlFieldMeta`] to [`crate::field::FieldDef`]. For enums,
additional processing on the enum itself takes place in [`crate::enums`].
Likewise there's special handling for structs in [`crate::structs`].
5. After all data has been structured, action is taken depending on the
specific derive macro which has been invoked.
*/
#![warn(missing_docs)]
#![allow(rustdoc::private_intra_doc_links)]
mod common;
mod compound;
mod enums;
mod field;
mod meta;
mod structs;
use proc_macro::TokenStream;
use syn::Item;
/// Derive macro for `FromXml`.
#[proc_macro_derive(FromXml, attributes(xml))]
pub fn from_element(input: TokenStream) -> TokenStream {
let item = syn::parse_macro_input!(input as Item);
let result = match item {
Item::Struct(item) => self::structs::try_from_element(item),
Item::Enum(item) => self::enums::try_from_element(item),
other => {
return syn::Error::new_spanned(
other,
"FromXml can only be applied to enum and struct definitions",
)
.into_compile_error()
.into()
}
};
match result {
Ok(v) => v.into(),
Err(e) => e.into_compile_error().into(),
}
}
/// Derive macro for `IntoXml`.
#[proc_macro_derive(IntoXml, attributes(xml))]
pub fn into_element(input: TokenStream) -> TokenStream {
let item = syn::parse_macro_input!(input as Item);
let result = match item {
Item::Struct(item) => self::structs::into_element(item),
Item::Enum(item) => self::enums::into_element(item),
other => {
return syn::Error::new_spanned(
other,
"IntoXml can only be applied to enum and struct definitions",
)
.into_compile_error()
.into()
}
};
match result {
Ok(v) => v.into(),
Err(e) => e.into_compile_error().into(),
}
}

871
xso-proc/src/meta.rs Normal file
View File

@ -0,0 +1,871 @@
/*!
# Data structures and processing for `#[xml(..)]` attributes
This module is responsible for processing the syntax trees of the `#[xml(..)]`
attributes fields, enums, enum variants, and structs into more friendly
data structures.
However, it is not yet concerned as much with semantic validation: a lot of
invalid combinations can and will be represented with the data structures from
this module. The downstream consumers of these data structures, such as the
structs in the [`crate::field`] module, are responsible for ensuring that the
given combinations make sense and emit compile-time errors if they do not.
*/
use std::fmt;
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use syn::{spanned::Spanned, *};
/// Concatenate a list of identifiers into a comma-separated string.
///
/// Used for generating error messages.
macro_rules! concat_options {
($head:ident, $($field:ident,)+) => {
concat!(stringify!($head), ", ", concat_options!($($field,)+))
};
($last:ident,) => {
stringify!($last)
}
}
/// Format an "unsupported option" error message.
///
/// The allowed options should be passed as identifiers.
macro_rules! unsupported_option_message {
($($option:ident,)+) => {
concat!("unsupported option. supported options: ", concat_options!($($option,)+), ".")
}
}
/// Parse some fields out of a [`syn::meta::ParseNestedMeta`] struct.
///
/// The first argument, `$meta`, must be the `ParseNestedMeta` struct. The
/// remaining arguments, except for the last, must be identifiers of arguments
/// to parse out of `$meta`.
///
/// Each `$field` is stringified and matched against `$meta.path.is_ident`
/// in the order they are given. Each `$field` must also be available as
/// mutable `Option<T>` variable outside the scope of this macro. If the
/// `$field` matches the `$meta`, the value is extracted and parsed and
/// assigned as `Some(..)` to the `$field`.
///
/// If the field has already been assigned to, an error is returned.
///
/// Lastly, if no `$field` matched the identifier of the `$meta.path`, the
/// `$else` block is evaluated. This can (and must) be used to return an error
/// or chain a call to [`parse_some_flags!`].
macro_rules! parse_some_fields {
($meta:ident, $($field:ident,)+ $else:block) => {
$(
if $meta.path.is_ident(stringify!($field)) {
if $field.is_some() {
return Err(syn::Error::new_spanned(
$meta.path,
concat!("duplicate ", stringify!($field), " option"),
));
}
$field = Some($meta.value()?.parse()?);
Ok(())
} else
)+
$else
}
}
/// Parse some flags out of a [`syn::meta::ParseNestedMeta`] struct.
///
/// The first argument, `$meta`, must be the `ParseNestedMeta` struct. The
/// remaining arguments, except for the last, must be identifiers of arguments
/// to parse out of `$meta`.
///
/// Each `$flag` is stringified and matched against `$meta.path.is_ident`
/// in the order they are given. Each `$flag` must also be available as
/// mutable [`Flag`] variable outside the scope of this macro. If the
/// `$flag` matches the `$meta`, the `$flag` is assigned `Flag::Present` with
/// the path of the meta as value.
///
/// If the flag has already been set, an error is returned.
///
/// Lastly, if no `$flag` matched the identifier of the `$meta.path`, the
/// `$else` block is evaluated. This can (and must) be used to return an error
/// or chain a call to [`parse_some_fields!`].
macro_rules! parse_some_flags {
($meta:ident, $($flag:ident,)+ $else:block) => {
$(
if $meta.path.is_ident(stringify!($flag)) {
if $flag.is_set() {
return Err(syn::Error::new_spanned(
$meta.path,
concat!("duplicate ", stringify!($flag), " flag"),
));
}
$flag = Flag::Present($meta.path);
Ok(())
} else
)+
$else
}
}
/// Represents a boolean flag from a `#[xml(..)]` attribute meta.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum Flag {
/// The flag is not set.
Absent,
/// The flag was set.
Present(
/// The [`syn::meta::ParseNestedMeta::path`] which enabled the flag.
///
/// This is used to generate useful error messages by pointing at the
/// specific place the flag was activated.
Path,
),
}
impl Flag {
/// Return true if the flag is set, false otherwise.
pub fn is_set(&self) -> bool {
match self {
Self::Absent => false,
Self::Present(_) => true,
}
}
/// Basically [`Option::take`], but for [`Flag`].
pub fn take(&mut self) -> Self {
let mut tmp = Flag::Absent;
std::mem::swap(&mut tmp, self);
tmp
}
}
/// Type alias for a XML namespace setting.
///
/// This may in the future be replaced by an enum supporting both `Path` and
/// `LitStr`.
pub(crate) type StaticNamespace = Path;
/// Value of a `#[xml(namespace = ..)]` attribute.
///
/// XML namespaces can be configured in different ways, which are
/// distinguished in this enum.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum NamespaceRef {
/// A dynamic namespace, expressed as `#[xml(namespace = dyn)]`.
///
/// See [`crate::compound::CompoundNamespace::Dyn`] for details.
Dyn(
/// The original `dyn` token for better error messages.
Token![dyn],
),
/// Refer to the parent struct's namespace, expressed as
/// `#[xml(namespace = super)]`.
Super(
/// The original `super` token for better error messages.
Token![super],
),
/// A static namespace identified by a static string, expressed as
/// `#[xml(namespace = crate::ns::FOO)]` or similar.
Static(StaticNamespace),
}
impl parse::Parse for NamespaceRef {
fn parse(input: parse::ParseStream<'_>) -> Result<Self> {
if input.peek(Token![dyn]) {
let ns = input.parse()?;
Ok(Self::Dyn(ns))
} else if input.peek(Token![super]) && !input.peek2(Token![::]) {
let ns = input.parse()?;
Ok(Self::Super(ns))
} else {
let ns = input.parse()?;
Ok(Self::Static(ns))
}
}
}
impl ToTokens for NamespaceRef {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Dyn(ns) => ns.to_tokens(tokens),
Self::Super(ns) => ns.to_tokens(tokens),
Self::Static(ns) => ns.to_tokens(tokens),
}
}
fn into_token_stream(self) -> TokenStream {
match self {
Self::Dyn(ns) => ns.into_token_stream(),
Self::Super(ns) => ns.into_token_stream(),
Self::Static(ns) => ns.into_token_stream(),
}
}
}
/// Type alias for a XML name setting.
///
/// This may in the future be replaced by an enum supporting both `Path` and
/// `LitStr`.
pub(crate) type NameRef = LitStr;
/// An XML name.
///
/// This enum, unlike when passing around the XML name as a string, preserves
/// the original tokens, which is useful for better error messages.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum Name {
/// Represented as a string literal.
Lit(LitStr),
/// Represented as an identifier, e.g. when it is derived from an
/// `#[xml(attribute)]` field's identifier.
Ident(Ident),
}
impl From<LitStr> for Name {
fn from(other: LitStr) -> Self {
Self::Lit(other)
}
}
impl From<Ident> for Name {
fn from(other: Ident) -> Self {
Self::Ident(other)
}
}
impl From<Name> for LitStr {
fn from(other: Name) -> Self {
match other {
Name::Lit(v) => v,
Name::Ident(v) => LitStr::new(&v.to_string(), v.span()),
}
}
}
impl From<&Name> for LitStr {
fn from(other: &Name) -> Self {
match other {
Name::Lit(v) => v.clone(),
Name::Ident(v) => LitStr::new(&v.to_string(), v.span()),
}
}
}
impl fmt::Display for Name {
fn fmt<'f>(&self, f: &'f mut fmt::Formatter) -> fmt::Result {
match self {
Self::Lit(s) => f.write_str(&s.value()),
Self::Ident(s) => s.fmt(f),
}
}
}
impl ToTokens for Name {
fn to_tokens(&self, tokens: &mut TokenStream) {
let s: LitStr = self.into();
s.to_tokens(tokens)
}
fn into_token_stream(self) -> TokenStream {
let s: LitStr = self.into();
s.into_token_stream()
}
}
/// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
///
/// This is the counterpart to [`XmlFieldMeta`].
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) struct XmlCompoundMeta {
/// The span of the `#[xml(..)]` meta from which this was parsed.
///
/// This is useful for error messages.
pub(crate) span: Span,
/// The value assigned to `namespace` inside `#[xml(..)]`, if any.
pub(crate) namespace: Option<NamespaceRef>,
/// The value assigned to `name` inside `#[xml(..)]`, if any.
pub(crate) name: Option<NameRef>,
/// Flag indicating the presence of `fallback` inside `#[xml(..)].
pub(crate) fallback: Flag,
/// Flag indicating the presence of `transparent` inside `#[xml(..)].
pub(crate) transparent: Flag,
/// Flag indicating the presence of `exhaustive` inside `#[xml(..)].
pub(crate) exhaustive: Flag,
/// The value assigned to `validate` inside `#[xml(..)]`, if any.
pub(crate) validate: Option<Path>,
/// The value assigned to `prepare` inside `#[xml(..)]`, if any.
pub(crate) prepare: Option<Path>,
}
impl XmlCompoundMeta {
/// Parse the meta values from a `#[xml(..)]` attribute.
///
/// Undefined options or options with incompatible values are rejected
/// with an appropriate compile-time error.
fn parse_from_attribute(attr: &Attribute) -> Result<Self> {
let mut name: Option<NameRef> = None;
let mut namespace: Option<NamespaceRef> = None;
let mut fallback = Flag::Absent;
let mut transparent = Flag::Absent;
let mut exhaustive = Flag::Absent;
let mut validate: Option<Path> = None;
let mut prepare: Option<Path> = None;
attr.parse_nested_meta(|meta| {
parse_some_fields!(meta, name, namespace, validate, prepare, {
parse_some_flags!(meta, fallback, transparent, exhaustive, {
Err(syn::Error::new_spanned(
meta.path,
unsupported_option_message!(
name,
namespace,
validate,
fallback,
transparent,
exhaustive,
),
))
})
})
})?;
Ok(Self {
span: attr.span(),
namespace,
name,
fallback,
transparent,
validate,
exhaustive,
prepare,
})
}
/// 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> {
let mut result: Option<Self> = None;
for attr in attrs {
if !attr.path().is_ident("xml") {
continue;
}
if result.is_some() {
return Err(syn::Error::new_spanned(
attr.path(),
"only one #[xml(..)] per struct or enum variant allowed",
));
}
result = Some(Self::parse_from_attribute(attr)?);
}
if let Some(result) = result {
Ok(result)
} else {
Err(syn::Error::new(
Span::call_site(),
"#[xml(..)] attribute required on struct or enum variant",
))
}
}
}
/// Helper struct to parse the triplet of XML namespace, XML name and
/// `default` flag.
///
/// These three are used in several places.
#[cfg_attr(feature = "debug", derive(Debug))]
struct AttributeMeta {
/// The value assigned to `namespace` inside a potentially nested
/// `#[xml(..)]`, if any.
namespace: Option<NamespaceRef>,
/// The value assigned to `name` inside a potentially nested
/// `#[xml(..)]`, if any.
name: Option<NameRef>,
/// The presence of the `default` flag a potentially nested `#[xml(..)]`,
/// if any.
default_on_missing: Flag,
}
impl AttributeMeta {
/// Parse the struct's contents from a potenially nested meta.
///
/// This supports three syntaxes, assuming that `foo` is the meta we're
/// at:
///
/// - `#[xml(.., foo, ..)]` (no value at all): All pieces of the returned
/// struct are `None` / `Flag::Absent`.
///
/// - `#[xml(.., foo = "bar", ..)]` (direct assignment): The `name` is set
/// to `"bar"`, the other pieces are `None` / `Flag::Absent`.
///
/// - `#[xml(.., foo(..), ..)]`, where `foo` may contain the keys
/// `namespace`, `name` and `default`, specifying the values of the
/// struct respectively.
fn parse_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
if meta.input.peek(Token![=]) {
let name: LitStr = meta.value()?.parse()?;
Ok(Self {
name: Some(name),
namespace: None,
default_on_missing: Flag::Absent,
})
} else if meta.input.peek(token::Paren) {
let mut name: Option<NameRef> = None;
let mut namespace: Option<NamespaceRef> = None;
let mut default_on_missing: Flag = Flag::Absent;
meta.parse_nested_meta(|meta| {
parse_some_fields!(meta, namespace, name, {
if meta.path.is_ident("default") {
if default_on_missing.is_set() {
return Err(syn::Error::new_spanned(
meta.path,
"duplicate default option",
));
}
default_on_missing = Flag::Present(meta.path);
Ok(())
} else {
Err(Error::new_spanned(
meta.path,
format!(
"unsupported option. supported options are: {}",
concat_options!(namespace, name, default,),
),
))
}
})
})?;
Ok(Self {
namespace,
name,
default_on_missing,
})
} else {
Ok(Self {
namespace: None,
name: None,
default_on_missing: Flag::Absent,
})
}
}
}
/// A single extraction part inside an `#[xml(..(.., extract(..)))]`
/// attribute.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum ExtractMeta {
/// XML attribute extraction.
///
/// Maps to `extract(.., attribute, ..)`,
/// `extract(.., attribute = .., ..)`, or
/// `extract(.., attribute(..), ..)`.
Attribute { name: NameRef },
/// XML text extraction.
///
/// Maps to `extract(.., text, ..)`.
Text,
/// XML child element extraction.
///
/// Maps to `extract(.., elements, ..)`.
Elements,
}
impl ExtractMeta {
/// Parse a single extraction spec from the given `meta`.
///
/// See the enum variants for accepted syntaxes.
fn parse_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
if meta.path.is_ident("text") {
Ok(Self::Text)
} else if meta.path.is_ident("attribute") {
let path = meta.path.clone();
let meta = AttributeMeta::parse_from_meta(meta)?;
let Some(name) = meta.name else {
return Err(syn::Error::new_spanned(path, "attribute name must be specified in extract(), e.g. extract(attribute = \"name\")"));
};
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"namespaced attributes are not supported yet...",
));
}
Ok(Self::Attribute { name })
} else if meta.path.is_ident("elements") {
if meta.input.peek(token::Paren) {
return Err(syn::Error::new_spanned(
meta.path,
"arguments to `collect` inside #[xml(..(extract(..)))] are not supported.",
));
}
Ok(Self::Elements)
} else {
return Err(syn::Error::new_spanned(
meta.path,
"unsupported extract spec",
));
}
}
}
/// Mode for destructured child collection.
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum ChildMode {
/// The field represents a single XML child element.
Single,
/// The field represents more than one XML child element.
Collection,
}
/// Contents of an `#[xml(..)]` attribute on a field.
///
/// This is the counterpart to [`XmlCompoundMeta`].
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum XmlFieldMeta {
/// Maps the field to an XML attribute.
///
/// Maps to the `#[xml(attribute)]`, `#[xml(attribute = ..)]` and
/// `#[xml(attribute(..))]` Rust syntaxes.
///
/// Gets transformed into [`crate::field::FieldKind::Attribute`] in later
/// processing stages.
Attribute {
/// Contents of the `name = ..` option or value assigned to
/// `attribute` in the shorthand syntax.
name: Option<NameRef>,
/// Presence of the `default` flag.
default_on_missing: Flag,
},
/// Maps the field to a destructured XML child element.
///
/// Maps to the `#[xml(child)]`, `#[xml(child(..))]`, `#[xml(children)]`,
/// and `#[xml(children(..)]` Rust syntaxes.
///
/// Gets transformed into [`crate::field::FieldKind::Child`] in later
/// processing stages.
Child {
/// Distinguishes between `#[xml(child)]` and `#[xml(children)]`
/// variants.
mode: ChildMode,
/// Contents of the `namespace = ..` option.
namespace: Option<NamespaceRef>,
/// Contents of the `name = ..` option.
name: Option<NameRef>,
/// Contents of the `extract(..)` option.
extract: Vec<ExtractMeta>,
/// Presence of the `default` flag.
default_on_missing: Flag,
},
/// Maps the field to the compounds' XML element's namespace.
///
/// See also [`crate::compound::CompoundNamespace::Dyn`].
///
/// Maps to the `#[xml(namespace)]` Rust syntax.
Namespace,
/// Maps the field to the text contents of the XML element.
///
/// Maps to the `#[xml(text)]` Rust syntax.
Text,
/// Maps the field to a subset of XML child elements, without
/// destructuring them into Rust structs.
///
/// Maps to the `#[xml(elements)]` and `#[xml(elements(..))]` Rust
/// syntaxes.
Elements {
/// Contents of the `namespace = ..` option.
namespace: Option<NamespaceRef>,
/// Contents of the `name = ..` option.
name: Option<NameRef>,
},
/// Maps the field to a single of XML child element, without destructuring
/// it into a Rust struct.
///
/// Maps to the `#[xml(element(..))]` Rust syntax.
Element {
/// Contents of the `namespace = ..` option.
namespace: Option<NamespaceRef>,
/// Contents of the `name = ..` option.
name: Option<NameRef>,
/// Presence of the `default` flag.
default_on_missing: Flag,
},
/// Maps the field to the presence of an empty XML child element.
///
/// Maps to the `#[xml(flag(..))]` Rust syntax.
Flag {
/// Contents of the `namespace = ..` option.
namespace: Option<NamespaceRef>,
/// Contents of the `name = ..` option.
name: Option<NameRef>,
},
/// Ignores the field for the purpose of XML processing.
///
/// Maps to the `#[xml(ignore)]` Rust syntax.
Ignore,
}
impl XmlFieldMeta {
/// Processes a `#[xml(attribute)]` meta, creating a [`Self::Attribute`]
/// variant.
fn attribute_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let meta = AttributeMeta::parse_from_meta(meta)?;
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"namespaced attributes are not supported yet...",
));
}
Ok(Self::Attribute {
name: meta.name,
default_on_missing: meta.default_on_missing,
})
}
/// Common processing for the `#[xml(child)]` and `#[xml(children)]`
/// metas.
fn child_common_from_meta(
meta: meta::ParseNestedMeta<'_>,
) -> Result<(
Option<NamespaceRef>,
Option<NameRef>,
Vec<ExtractMeta>,
Flag,
)> {
if meta.input.peek(token::Paren) {
let mut name: Option<NameRef> = None;
let mut namespace: Option<NamespaceRef> = None;
let mut extract: Vec<ExtractMeta> = Vec::new();
let mut default_on_missing = Flag::Absent;
meta.parse_nested_meta(|meta| {
parse_some_fields!(meta, namespace, name, {
if meta.path.is_ident("extract") {
meta.parse_nested_meta(|meta| {
extract.push(ExtractMeta::parse_from_meta(meta)?);
Ok(())
})?;
Ok(())
} else if meta.path.is_ident("default") {
if default_on_missing.is_set() {
return Err(syn::Error::new_spanned(
meta.path,
"duplicate default option",
));
}
default_on_missing = Flag::Present(meta.path);
Ok(())
} else {
Err(Error::new_spanned(
meta.path,
unsupported_option_message!(namesace, name, extract,),
))
}
})
})?;
Ok((namespace, name, extract, default_on_missing))
} else {
Ok((None, None, Vec::new(), Flag::Absent))
}
}
/// Processes a `#[xml(child)]` meta, creating a [`Self::Child`]
/// variant with [`ChildMode::Single`].
fn child_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let (namespace, name, extract, default_on_missing) = Self::child_common_from_meta(meta)?;
Ok(Self::Child {
mode: ChildMode::Single,
namespace,
name,
extract,
default_on_missing,
})
}
/// Processes a `#[xml(children)]` meta, creating a [`Self::Child`]
/// variant with [`ChildMode::Collection`].
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)"));
}
Ok(Self::Child {
mode: ChildMode::Collection,
namespace,
name,
extract,
default_on_missing: Flag::Absent,
})
}
/// Processes a `#[xml(namespace)]` meta, creating a [`Self::Namespace`]
/// variant.
fn namespace_from_meta(_meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
Ok(Self::Namespace)
}
/// Processes a `#[xml(texd)]` meta, creating a [`Self::Text`]
/// variant.
fn text_from_meta(_meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
Ok(Self::Text)
}
/// Processes a `#[xml(element)]` meta, creating a [`Self::Element`]
/// variant.
fn element_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let meta = AttributeMeta::parse_from_meta(meta)?;
Ok(Self::Element {
name: meta.name,
namespace: meta.namespace,
default_on_missing: meta.default_on_missing,
})
}
/// Processes a `#[xml(elements)]` meta, creating a [`Self::Elements`]
/// variant.
fn elements_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let meta = AttributeMeta::parse_from_meta(meta)?;
if let Flag::Present(default_on_missing) = meta.default_on_missing {
return Err(syn::Error::new_spanned(default_on_missing, "default cannot be used on #[xml(elements)] (it is implied, the default is the empty container)"));
}
Ok(Self::Elements {
name: meta.name,
namespace: meta.namespace,
})
}
/// Processes a `#[xml(flag)]` meta, creating a [`Self::Flag`] variant.
fn flag_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let meta = AttributeMeta::parse_from_meta(meta)?;
if let Flag::Present(default_on_missing) = meta.default_on_missing {
return Err(syn::Error::new_spanned(
default_on_missing,
"default cannot be used on #[xml(flag)] (it is implied, the default false)",
));
}
Ok(Self::Flag {
name: meta.name,
namespace: meta.namespace,
})
}
/// Processes a `#[xml(ignore)]` meta, creating a [`Self::Ignore`]
/// variant.
fn ignore_from_meta(_meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
Ok(Self::Ignore)
}
/// Parse an `#[xml(..)]` attribute.
///
/// This switches based on the first identifier within the `#[xml(..)]`
/// attribute and generates a struct variant accordingly.
///
/// Only a single nested attribute is allowed; more than one will be
/// rejected with an appropriate compile-time error.
///
/// If no attribute is contained at all, a compile-time error is
/// generated.
///
/// Undefined options or options with incompatible values are rejected
/// with an appropriate compile-time error.
pub(crate) fn parse_from_attribute(attr: &Attribute) -> Result<Self> {
static VALID_OPTIONS: &'static str =
"attribute, child, children, namespace, text, elements, element, flag";
let mut result: Option<Self> = None;
attr.parse_nested_meta(|meta| {
if result.is_some() {
return Err(Error::new_spanned(
meta.path,
"multiple field options are not supported",
));
}
if meta.path.is_ident("attribute") {
result = Some(Self::attribute_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("child") {
result = Some(Self::child_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("children") {
result = Some(Self::children_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("namespace") {
result = Some(Self::namespace_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("text") {
result = Some(Self::text_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("elements") {
result = Some(Self::elements_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("element") {
result = Some(Self::element_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("flag") {
result = Some(Self::flag_from_meta(meta)?);
Ok(())
} else if meta.path.is_ident("ignore") {
result = Some(Self::ignore_from_meta(meta)?);
Ok(())
} else {
Err(Error::new_spanned(
meta.path,
format!(
"unsupported field option. supported options: {}.",
VALID_OPTIONS
),
))
}
})?;
if let Some(result) = result {
Ok(result)
} else {
Err(Error::new_spanned(
attr,
format!(
"missing field options. specify at least one of {}.",
VALID_OPTIONS
),
))
}
}
}

163
xso-proc/src/structs.rs Normal file
View File

@ -0,0 +1,163 @@
/*!
# Processing of struct declarations
This module contains the main code for implementing the derive macros from
this crate on `struct` items.
It is thus the counterpart to [`crate::enums`].
*/
use proc_macro2::Span;
use quote::quote;
use syn::*;
use crate::common::bake_generics;
use crate::compound::Compound;
use crate::meta::{Flag, XmlCompoundMeta};
/// Represent a struct.
#[cfg_attr(feature = "debug", derive(Debug))]
struct StructDef {
/// The `validate` 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
/// expected to return `Result<(), Error>`, the `Err(..)` variant of which
/// is forwarded correctly.
validate: Option<Path>,
/// The `prepare` 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,
}
impl StructDef {
/// Construct a new struct from its `#[xml(..)]` attribute and the
/// fields.
fn new(mut meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
if let Flag::Present(fallback) = meta.fallback.take() {
return Err(syn::Error::new_spanned(
fallback,
"fallback is not allowed on structs",
));
}
if let Flag::Present(exhaustive) = meta.exhaustive.take() {
return Err(syn::Error::new_spanned(
exhaustive,
"exhaustive is not allowed on structs",
));
}
let validate = meta.validate.take();
let prepare = meta.prepare.take();
Ok(Self {
validate,
prepare,
inner: Compound::new(meta, fields)?,
})
}
}
/// `FromXml` derive macro implementation for structs.
pub(crate) fn try_from_element(item: syn::ItemStruct) -> Result<proc_macro2::TokenStream> {
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(
Some(&Path::from(ident.clone())),
None,
&Ident::new("residual", Span::call_site()),
)?;
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)]
impl #generics_decl ::std::convert::TryFrom<::xso::exports::minidom::Element> for #ident #generics_ref #where_clause {
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)
}
}
impl #generics_decl ::xso::FromXml for #ident #generics_ref #where_clause {
fn from_tree(elem: ::xso::exports::minidom::Element) -> Result<Self, ::xso::error::Error> {
Self::try_from(elem)
}
fn absent() -> Option<Self> {
None
}
}
})
}
/// `IntoXml` derive macro implementation for structs.
pub(crate) fn into_element(item: syn::ItemStruct) -> Result<proc_macro2::TokenStream> {
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(Some(&Path::from(ident.clone())), None, |member| {
Expr::Field(ExprField {
attrs: Vec::new(),
dot_token: syn::token::Dot {
spans: [Span::call_site()],
},
base: Box::new(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Path {
leading_colon: None,
segments: [PathSegment::from(Ident::new("other", Span::call_site()))]
.into_iter()
.collect(),
},
})),
member,
})
})?;
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
}
}
impl #generics_decl ::xso::IntoXml for #ident #generics_ref {
fn into_tree(self) -> Option<::xso::exports::minidom::Element> {
Some(::minidom::Element::from(self))
}
}
})
}

23
xso/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "xso"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
base64 = "0.21"
digest = "0.10"
sha1 = "0.10"
sha2 = "0.10"
sha3 = "0.10"
blake2 = "0.10.4"
chrono = { version = "0.4.5", default-features = false, features = ["std"] }
# same repository dependencies
jid = { version = "0.10", features = ["minidom"], path = "../jid" }
minidom = { version = "0.15", path = "../minidom" }
xso_proc = { version = "0.1.0", optional = true }
[features]
default = ["macros"]
macros = ["dep:xso_proc"]

136
xso/src/error.rs Normal file
View File

@ -0,0 +1,136 @@
// Copyright (c) 2017-2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
/*!
Error types for parsing and serialisation.
*/
use std::error::Error as StdError;
use std::fmt;
/// Contains one of the potential errors triggered while parsing an
/// [Element](../struct.Element.html) into a specialised struct.
#[derive(Debug)]
pub enum Error {
/// The usual error when parsing something.
///
/// TODO: use a structured error so the user can report it better, instead
/// of a freeform string.
ParseError(&'static str),
/// Element local-name/namespace mismatch
///
/// Returns the original element unaltered, as well as the expected ns and
/// local-name.
TypeMismatch(&'static str, &'static str, minidom::Element),
/// Generated when some base64 content fails to decode, usually due to
/// extra characters.
Base64Error(base64::DecodeError),
/// Generated when text which should be an integer fails to parse.
ParseIntError(std::num::ParseIntError),
/// Generated when text which should be a string fails to parse.
ParseStringError(std::string::ParseError),
/// Generated when text which should be an IP address (IPv4 or IPv6) fails
/// to parse.
ParseAddrError(std::net::AddrParseError),
/// Generated when text which should be a [JID](../../jid/struct.Jid.html)
/// fails to parse.
JidParseError(jid::Error),
/// Generated when text which should be a
/// [DateTime](../date/struct.DateTime.html) fails to parse.
ChronoParseError(chrono::ParseError),
}
impl Error {
/// Converts the TypeMismatch error to a generic ParseError
///
/// This must be used when TryFrom is called on children to avoid confusing
/// user code which assumes that TypeMismatch refers to the top level
/// element only.
#[doc(hidden)]
pub fn hide_type_mismatch(self) -> Self {
match self {
Error::TypeMismatch(..) => Error::ParseError("Unexpected child element"),
other => other,
}
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
match self {
Error::ParseError(_) | Error::TypeMismatch(..) => None,
Error::Base64Error(e) => Some(e),
Error::ParseIntError(e) => Some(e),
Error::ParseStringError(e) => Some(e),
Error::ParseAddrError(e) => Some(e),
Error::JidParseError(e) => Some(e),
Error::ChronoParseError(e) => Some(e),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::ParseError(s) => write!(fmt, "parse error: {}", s),
Error::TypeMismatch(ns, localname, element) => write!(
fmt,
"element type mismatch: expected {{{}}}{}, got {{{}}}{}",
ns,
localname,
element.ns(),
element.name()
),
Error::Base64Error(e) => write!(fmt, "base64 error: {}", e),
Error::ParseIntError(e) => write!(fmt, "integer parsing error: {}", e),
Error::ParseStringError(e) => write!(fmt, "string parsing error: {}", e),
Error::ParseAddrError(e) => write!(fmt, "IP address parsing error: {}", e),
Error::JidParseError(e) => write!(fmt, "JID parsing error: {}", e),
Error::ChronoParseError(e) => write!(fmt, "time parsing error: {}", e),
}
}
}
impl From<base64::DecodeError> for Error {
fn from(err: base64::DecodeError) -> Error {
Error::Base64Error(err)
}
}
impl From<std::num::ParseIntError> for Error {
fn from(err: std::num::ParseIntError) -> Error {
Error::ParseIntError(err)
}
}
impl From<std::string::ParseError> for Error {
fn from(err: std::string::ParseError) -> Error {
Error::ParseStringError(err)
}
}
impl From<std::net::AddrParseError> for Error {
fn from(err: std::net::AddrParseError) -> Error {
Error::ParseAddrError(err)
}
}
impl From<jid::Error> for Error {
fn from(err: jid::Error) -> Error {
Error::JidParseError(err)
}
}
impl From<chrono::ParseError> for Error {
fn from(err: chrono::ParseError) -> Error {
Error::ChronoParseError(err)
}
}

548
xso/src/lib.rs Normal file
View File

@ -0,0 +1,548 @@
#![deny(missing_docs)]
/*!
# Core types, macros and traits for parsing structs from XML
This crate provides the facilities for parsing XML data into Rust structs, and
vice versa. Think of it as an alternative[^serde-note] to serde, more suited to
XML.
To get started, use the [`FromXml`] and [`IntoXml`] derive macros
on your struct. See in particular the documentation of [`FromXml`] for
a full reference on the supported attributes.
[^serde-note]: Though it should be said that you can combine serde and this
crate on the same struct, no problem with that!
*/
use std::collections::BTreeMap;
pub mod error;
mod text;
#[doc(inline)]
pub use text::*;
/**
# Make a struct or enum parseable from XML
This macro generates the necessary trait implementations to convert a struct
from XML.
In order to work, structs, enums, enum variants and fields all must be
annotated using the `#[xml(..)]` meta. The insides of that meta are explained
below.
## Examples
```
# use xso::{FromXml};
static MY_NAMESPACE: &'static str = "urn:uuid:55c56882-3915-49de-a7ee-fd672d7a85cf";
#[derive(FromXml)]
#[xml(namespace = MY_NAMESPACE, name = "foo")]
struct Foo;
// parses <foo xmlns="urn:uuid:55c56882-3915-49de-a7ee-fd672d7a85cf"/>
```
## Field order
Field order **matters**. The fields are parsed in the order they are declared
(for children, anyway). If multiple fields match a given child element, the
first field which matches will be taken. The only exception is
`#[xml(elements)]` (without further arguments), which is always processed
last.
When XML is generated from a struct, the child elements are also generated
in the order of the fields. That means that passing an XML element through
FromXml and IntoXml may re-order some child elements.
Sorting order between elements which match the same field is generally
preserved, if the container preserves sort order on insertion.
## Struct attributes
- `namespace = ..`: This can be one of the following:
- `dyn`: Not implemented yet.
- A path referring to a `&'static str` `static` which contains the
namespace URI of the XML element represented by the struct.
Required on non-`transparent` structs.
- `name = ..`: A string literal which contains the local XML name of
of the XML element represented by the struct.
Required on non-`transparent` structs.
- `validate = ..`: A path referring to a
`fn(&mut T) -> Result<(), xso::error::Error>`, where `T` is the
struct which is being defined. If set, the function will be called after
parsing has completed. If it returns an error, that error is returned
instead of the struct.
This attribute has no influence on [`IntoXml`].
- `prepare = ..`: A path referring to a
`fn(&mut T) -> ()`, where `T` is the struct which is being defined. If set,
the function will be called before the struct is converted into Element.
This attribute has no influence on [`FromXml`].
- `transparent`: Only allowed on tuple-like structs with exactly one field.
If set, the parsing is fully delegated to the inner field, which must in
turn implement `FromXml` (or `IntoXml` respectively).
Attributes on the single struct field are ignored.
`validate` is allowed and will be called.
## Enum attributes
- `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.
- `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.
This cannot be combined with a `fallback` variant.
This attribute has no relation to the Rust standard `#[non_exhaustive]`
attribute.
### Enum variant attributes
Enum variants support almost exactly the same attributes as structs, with the
following variations:
- `fallback`: If set, this enumeration variant is generated when no other
variant matches. This is only allowed if all of the following are true:
- `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`.
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.
## Field attributes
Field attributes are composed of a field kind, followed by a value or a list
of attributes. Examples:
```
# use xso::FromXml;
# static NS: &'static str = "urn:uuid:55c56882-3915-49de-a7ee-fd672d7a85cf";
# #[derive(FromXml)]
# #[xml(namespace = NS, name = "foo")]
# struct Foo {
#[xml(attribute)]
# f1: String,
#[xml(attribute = "foo")]
# f2: String,
#[xml(attribute(name = "foo"))]
# f3: String,
# }
```
If the `kind = ..` syntax is allowed, the attribute which is specified that
way will be marked as `default`.
The following field kinds are available:
- `attribute`, `attribute = name`, `attribute(..)`:
Extract a string from an XML attribute. The field type must
implement `FromAttribute` (for [`FromXml`]) or
`IntoAttributeValue` (for [`IntoXml`]).
- `name = ..` (default): The XML name of the attribute. If this is not
set, the field's identifier is used.
- `namespace = ..`: The XML namespace of the attribute. This is optional,
and if absent, only unnamespaced attributes are considered.
- `default`: If set, the attribute's value will be defaulted using
[`Default::default`] if the attribute is absent and none could be
generated through [`FromAttribute::from_attribute`].
- `child(..)`: Extract one a text data from a child element. The field type
must implement `FromAttribute` (for [`FromXml`]) or
`IntoAttributeValue` (for [`IntoXml`]).
- `name = ..` (required): The XML name of the child to match.
- `namespace = ..` (required): The XML namespace of the child to match.
- `extract(..)` (required): Specification of data to extract. This must
contain one of:
- `text`: Extract the child element's text.
- `attribute = name`, `attribute(..)`: Extract the child element's
attribute value. The semantics are the same as for the top-level
`attribute` kind, however, specifying the name is mandatory.
- `collect`: All children of the child element as [`minidom::Element`]
structs. This requires the field to be of type `Vec<Element>`.
All contents of the child element which are not caught by the extraction
specification are rejected with an error.
- `default`: If the child is not present, the field value will be created
using [`Default::default`]. For `extract(..)`, this is even required when
using `Option<..>` as a field type.
- `child`: Extract an entire child element. The field type must
implement [`FromXml`] (for [`FromXml`]) or `IntoXml` (for
[`IntoXml`]).
The namespace and name to match are determined by the field type, thus it
is not allowed to specify them here.
- `children(..)`: Like `child(..)`, with the following differences:
- More than one clause inside `extract(..)` are allowed.
- More than one matching child is allowed
- The field type must implement `XmlDataCollection<T>`, where T is a tuple
of N strings, where N is the number of clauses in the `extract(..)`
attribute.
- `children`: Extract zero or more entire child elements. The field type must
implement [`XmlCollection`].
The namespace and name to match are determined by the field type, thus it
is not allowed to specify them here.
- `text`: Extract the element's text contents. The field type must
implement `FromText` (for [`FromXml`]) or `IntoText`
(for [`IntoXml`]).
- `element(..)`: Collect a single element as [`minidom::Element`] instead of
attempting to destructure it. The field type must implement `From<Element>`
and `Into<Option<Element>>` ([`minidom::Element`] implements both).
- `name = ..` : The XML name of the element to match.
- `namespace = ..`: The XML namespace of the element to match.
- `default`: If the element cannot be found, initialize the field using
[`std::default::Default`] instead of emitting an error. The type must
implement `Default` for that (in addition to the other required traits).
If the field converts into `None` when invoking `Into<Option<Element>>`,
the element is omitted from the output altogether.
- `elements(..)`: Collect otherwise unknown children as [`minidom::Element`].
- `namespace = ..`: The XML namespace of the element to match.
- `name = ..` (optional): The XML name of the element to match. If omitted,
all elements from the given namespace are collected.
- `elements`: Collect all unknown children as [`minidom::Element`]. The field
type must be `Vec<Element>`.
- `ignore`: The field is not considered during parsing or serialisation. The
type must implement [`Default`].
*/
pub use xso_proc::FromXml;
/**
# Make a struct or enum convertible into XML
For all supported attributes, please see [`FromXml`].
*/
pub use xso_proc::IntoXml;
#[doc(hidden)]
pub mod exports {
pub use minidom;
}
pub use self::exports::minidom::IntoAttributeValue;
use jid::{BareJid, FullJid, Jid};
macro_rules! from_text_via_parse {
($cons:path, $($t:ty,)+) => {
$(
impl FromText for $t {
fn from_text(s: &str) -> Result<Self, error::Error> {
s.parse().map_err($cons)
}
}
impl IntoText for $t {
fn into_text(self) -> String {
self.to_string()
}
}
)+
}
}
from_text_via_parse! {
error::Error::ParseIntError,
u8,
u16,
u32,
u64,
u128,
usize,
i8,
i16,
i32,
i64,
i128,
isize,
}
from_text_via_parse! {
error::Error::ParseAddrError,
std::net::IpAddr,
std::net::Ipv4Addr,
std::net::Ipv6Addr,
}
from_text_via_parse! {
error::Error::JidParseError,
Jid,
FullJid,
BareJid,
}
/// Provide construction of a value from an XML attribute.
///
/// Most likely, you don't need to implement this; implement [`FromText`]
/// instead (which automatically provides a [`FromAttribute`] implementation).
///
/// This trait has to exist to handle the special-ness of `Option<_>` when it
/// comes to values which don't always exist.'
pub trait FromAttribute: Sized {
/// Convert the XML string to a value, maybe.
///
/// This should return `None` if and only if `s` is `None`, but in some
/// cases it makes sense to return `Some(..)` even for an input of
/// `None`, e.g. when a specific default is desired.
fn from_attribute(s: Option<&str>) -> Result<Option<Self>, error::Error>;
}
impl<T: FromAttribute> FromAttribute for Option<T> {
/// This implementation returns `Some(None)` for an input of `None`.
///
/// That way, absent attributes with a `Option<T>` field type effectively
/// default to `None`.
fn from_attribute(s: Option<&str>) -> Result<Option<Self>, error::Error> {
match s {
None => Ok(Some(None)),
Some(v) => Ok(Some(T::from_attribute(Some(v))?)),
}
}
}
impl<T: FromText> FromAttribute for T {
fn from_attribute(s: Option<&str>) -> Result<Option<Self>, error::Error> {
match s {
None => Ok(None),
Some(v) => Ok(Some(T::from_text(v)?)),
}
}
}
/// Provide construction of structs from XML (sub-)trees.
///
/// This trait is what is really implemented by the [`FromXml`] derive
/// macro.
pub trait FromXml: Sized {
/// Convert an XML subtree into a struct or fail with an error.
fn from_tree(tree: minidom::Element) -> Result<Self, error::Error>;
/// Provide an optional default if the element is absent.
///
/// This is used to automatically make `Option<T>` default to `None`.
fn absent() -> Option<Self>;
}
impl<T: FromXml> FromXml for Option<T> {
fn from_tree(tree: minidom::Element) -> Result<Self, error::Error> {
Ok(Some(T::from_tree(tree)?))
}
fn absent() -> Option<Self> {
Some(T::absent())
}
}
/// Convert a struct into an XML tree.
///
/// This trait is what is really implemented by the [`IntoXml`] derive
/// macro.
pub trait IntoXml {
/// Destruct the value into an optional [`minidom::Element`].
///
/// When returning `None`, no element will appear in the output. This
/// should only be used for values which can be constructed via
/// [`FromXml::absent`] in order to ensure that the result can be parsed
/// again.
fn into_tree(self) -> Option<minidom::Element>;
}
impl<T: IntoXml> IntoXml for Option<T> {
fn into_tree(self) -> Option<minidom::Element> {
self?.into_tree()
}
}
/// Convert XML text to a value.
pub trait FromText: Sized {
/// Construct a value from XML text.
///
/// This is similar to [`std::str::FromStr`], but the error type is fixed.
fn from_text(s: &str) -> Result<Self, error::Error>;
}
impl FromText for String {
/// Copy the string from the source into the result.
fn from_text(s: &str) -> Result<Self, error::Error> {
Ok(s.to_string())
}
}
/// Convert a value into XML text.
pub trait IntoText {
/// Consume the value and return it as XML string.
fn into_text(self) -> String;
}
impl IntoText for String {
fn into_text(self) -> String {
self
}
}
/// TODO: remove this
pub trait XmlCollection {
/// TODO: remove this
fn new_empty() -> Self;
/// TODO: remove this
fn try_append(&mut self, element: minidom::Element) -> Result<(), error::Error>;
}
impl<T: TryFrom<minidom::Element, Error = error::Error>> XmlCollection for Vec<T> {
fn new_empty() -> Self {
Self::new()
}
fn try_append(&mut self, element: minidom::Element) -> Result<(), error::Error> {
self.push(T::try_from(element)?);
Ok(())
}
}
/// Specify handling for complex data types mapped to XML.
///
/// Implementing this trait allows a value to be used in collections in the
/// context of a field marked with `#[children(.., extract(..))]`. In most
/// cases, this will be used with T being a single value or a tuple of two
/// elements, to feed vectors or maps respectively.
///
/// Indeed, this trait is automatically implemented for types implementing
/// both [`FromText`] and [`IntoText`] with `T = String` and likewise for
/// tuples of two such types with `T = (String, String)`, covering most use
/// cases.
///
/// **If you need more implementations on foreign types, do not hesitate to
/// make a PR or file an issue!**
pub trait XmlDataItem<T>: Sized {
/// Construct the value from a value of the type variable, or fail with
/// an error.
fn from_raw(data: T) -> Result<Self, error::Error>;
/// Deconstruct the value into a value of the type variabe..
fn into_raw(self) -> T;
}
impl<T: FromText + IntoText> XmlDataItem<String> for T {
fn from_raw(data: String) -> Result<Self, error::Error> {
Self::from_text(&data)
}
fn into_raw(self) -> String {
self.into_text()
}
}
impl<A: FromText + IntoText, B: FromText + IntoText> XmlDataItem<(String, String)> for (A, B) {
fn from_raw(data: (String, String)) -> Result<Self, error::Error> {
Ok((A::from_text(&data.0)?, B::from_text(&data.1)?))
}
fn into_raw(self) -> (String, String) {
(self.0.into_text(), self.1.into_text())
}
}
/// TODO: remove this
pub trait XmlDataCollection {
/// TODO: remove this
type Input;
/// TODO: remove this
type Item: XmlDataItem<Self::Input>;
/// TODO: remove this
type IntoIter: Iterator<Item = Self::Item>;
/// TODO: remove this
fn new_empty() -> Self;
/// TODO: remove this
fn append_data(&mut self, item: Self::Item);
/// TODO: remove this
fn into_data_iterator(self) -> Self::IntoIter;
}
impl<T: XmlDataItem<String>> XmlDataCollection for Vec<T> {
type Input = String;
type Item = T;
type IntoIter = std::vec::IntoIter<T>;
fn new_empty() -> Self {
Vec::new()
}
fn append_data(&mut self, item: Self::Item) {
self.push(item);
}
fn into_data_iterator(self) -> Self::IntoIter {
self.into_iter()
}
}
impl<K: Ord, V> XmlDataCollection for BTreeMap<K, V>
where
(K, V): XmlDataItem<(String, String)>,
{
type Input = (String, String);
type Item = (K, V);
type IntoIter = std::collections::btree_map::IntoIter<K, V>;
fn new_empty() -> Self {
BTreeMap::new()
}
fn append_data(&mut self, item: Self::Item) {
self.insert(item.0, item.1);
}
fn into_data_iterator(self) -> Self::IntoIter {
self.into_iter()
}
}

129
xso/src/text.rs Normal file
View File

@ -0,0 +1,129 @@
use std::fmt;
use std::ops::{Deref, DerefMut};
use minidom::IntoAttributeValue;
use base64::{engine::general_purpose::STANDARD as Base64Engine, Engine};
use crate::{error::Error, FromText, IntoText};
/// Base64-encoding behavior for `Vec<u8>`.
///
/// Use this type for XML-mapped attribute or text fields in order to decode
/// base64 data on deserialisation and encode it on serialisation.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Base64(pub Vec<u8>);
impl Deref for Base64 {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Base64 {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromText for Base64 {
fn from_text(s: &str) -> Result<Self, Error> {
Ok(Self(Base64Engine.decode(s)?))
}
}
impl IntoText for Base64 {
fn into_text(self) -> String {
Base64Engine.encode(&self.0)
}
}
/// Base64-encoding behavior for `Vec<u8>` while ignoring whitespace.
///
/// In contrast to [`Base64`], this type ignores whitespace in its input,
/// which makes it more tolerant toward weird base64 emitters.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WhitespaceAwareBase64(pub Vec<u8>);
impl Deref for WhitespaceAwareBase64 {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for WhitespaceAwareBase64 {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromText for WhitespaceAwareBase64 {
fn from_text(s: &str) -> Result<Self, Error> {
let s: String = s
.chars()
.filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
.collect();
Ok(Self(Base64Engine.decode(s)?))
}
}
impl IntoText for WhitespaceAwareBase64 {
fn into_text(self) -> String {
Base64Engine.encode(&self.0)
}
}
/// Shim wrapper around [`String`] which refuses to parse if the string
/// is empty
#[derive(Debug, Clone)]
pub struct NonEmptyString(String);
impl NonEmptyString {
/// Return `Some(s)` if s is not empty, `None` otherwise.
pub fn new(s: String) -> Option<Self> {
if s.len() == 0 {
return None;
}
Some(Self(s))
}
}
impl Deref for NonEmptyString {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for NonEmptyString {
fn fmt<'f>(&self, f: &'f mut fmt::Formatter) -> fmt::Result {
<String as fmt::Display>::fmt(&self.0, f)
}
}
impl FromText for NonEmptyString {
fn from_text(s: &str) -> Result<Self, Error> {
if s.len() == 0 {
return Err(Error::ParseError("string must not be empty"));
}
Ok(Self(s.to_string()))
}
}
impl IntoText for NonEmptyString {
fn into_text(self) -> String {
self.0
}
}
impl IntoAttributeValue for NonEmptyString {
fn into_attribute_value(self) -> Option<String> {
Some(self.0)
}
}