Merge branch 'feature/derive-macro-part-1' into 'main'

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

See merge request xmpp-rs/xmpp-rs!306
This commit is contained in:
Jonas Schäfer 2024-04-17 15:08:13 +00:00
commit 170cba322e
28 changed files with 9072 additions and 127 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" }

View File

@ -24,6 +24,7 @@ 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 = { version = "0.1.0" }
[features]
# Build xmpp-parsers to make components instead of clients.

View File

@ -28,6 +28,8 @@ pub use crate::util::error::Error;
pub use jid::{self, BareJid, Error as JidParseError, FullJid, Jid};
pub use minidom::Element;
pub use xso as core;
pub use blake2;
pub use sha1;
pub use sha2;
@ -39,6 +41,9 @@ pub mod ns;
#[macro_use]
mod util;
#[cfg(test)]
mod macro_tests;
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub mod bind;
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core

File diff suppressed because it is too large Load Diff

View File

@ -4,130 +4,4 @@
// 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/.
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, crate::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.
pub(crate) 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)
}
}
pub use xso::error::*;

View File

@ -3,6 +3,12 @@
// 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/.
#![allow(unused_macros)]
// ^ This helps the transition to using the derive macros, because we do not
// have to remove the macro at the exact commit where it stops being used.
// That gives us more flexibility about the order in which the MRs
// transitioning the various modules are merged.
// TODO: remove the above once the transition is done.
macro_rules! get_attr {
($elem:ident, $attr:tt, $type:tt) => {
@ -93,6 +99,18 @@ macro_rules! generate_attribute {
}))
}
}
impl ::xso::FromXmlText for $elem {
fn from_xml_text(s: &str) -> Result<Self, crate::util::error::Error> {
<Self as ::std::str::FromStr>::from_str(s)
}
}
impl ::xso::IntoXmlText for $elem {
fn into_xml_text(self) -> String {
String::from(match self {
$($elem::$a => $b),+
})
}
}
);
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+$(,)?}, Default = $default:ident) => (
$(#[$meta])*
@ -126,6 +144,16 @@ macro_rules! generate_attribute {
$elem::$default
}
}
impl ::xso::FromXmlText for $elem {
fn from_xml_text(s: &str) -> Result<Self, crate::util::error::Error> {
<Self as ::std::str::FromStr>::from_str(s)
}
}
impl ::xso::IntoOptionalXmlText for $elem {
fn into_optional_xml_text(self) -> Option<String> {
<Self as ::minidom::IntoAttributeValue>::into_attribute_value(self)
}
}
);
($(#[$meta:meta])* $elem:ident, $name:tt, ($(#[$meta_symbol:meta])* $symbol:ident => $value:tt)) => (
$(#[$meta])*
@ -158,6 +186,16 @@ macro_rules! generate_attribute {
$elem::None
}
}
impl ::xso::FromXmlText for $elem {
fn from_xml_text(s: &str) -> Result<Self, crate::util::error::Error> {
<Self as ::std::str::FromStr>::from_str(s)
}
}
impl ::xso::IntoOptionalXmlText for $elem {
fn into_optional_xml_text(self) -> Option<String> {
<Self as ::minidom::IntoAttributeValue>::into_attribute_value(self)
}
}
);
($(#[$meta:meta])* $elem:ident, $name:tt, bool) => (
$(#[$meta])*
@ -191,6 +229,16 @@ macro_rules! generate_attribute {
$elem::False
}
}
impl ::xso::FromXmlText for $elem {
fn from_xml_text(s: &str) -> Result<Self, crate::util::error::Error> {
<Self as ::std::str::FromStr>::from_str(s)
}
}
impl ::xso::IntoOptionalXmlText for $elem {
fn into_optional_xml_text(self) -> Option<String> {
<Self as ::minidom::IntoAttributeValue>::into_attribute_value(self)
}
}
);
($(#[$meta:meta])* $elem:ident, $name:tt, $type:tt, Default = $default:expr) => (
$(#[$meta])*
@ -215,6 +263,16 @@ macro_rules! generate_attribute {
$elem($default)
}
}
impl ::xso::FromXmlText for $elem {
fn from_xml_text(s: &str) -> Result<Self, crate::util::error::Error> {
<Self as ::std::str::FromStr>::from_str(s)
}
}
impl ::xso::IntoOptionalXmlText for $elem {
fn into_optional_xml_text(self) -> Option<String> {
<Self as ::minidom::IntoAttributeValue>::into_attribute_value(self)
}
}
);
}
@ -413,6 +471,18 @@ macro_rules! generate_id {
Some(self.0)
}
}
impl ::xso::FromXmlText for $elem {
fn from_xml_text(s: &str) -> Result<Self, crate::util::error::Error> {
Ok(Self(s.to_owned()))
}
}
impl ::xso::IntoXmlText for $elem {
fn into_xml_text(self) -> String {
self.0
}
}
);
}
@ -676,6 +746,16 @@ macro_rules! generate_element {
)*
}
impl ::xso::FromXml for $elem {
fn from_tree(elem: crate::Element) -> Result<Self, crate::util::error::Error> {
Self::try_from(elem)
}
fn absent() -> Option<Self> {
None
}
}
impl ::std::convert::TryFrom<crate::Element> for $elem {
type Error = crate::util::error::Error;
@ -748,6 +828,12 @@ macro_rules! generate_element {
builder.build()
}
}
impl ::xso::IntoXml for $elem {
fn into_tree(self) -> Option<::minidom::Element> {
Some(::minidom::Element::from(self))
}
}
);
}

View File

@ -13,3 +13,6 @@ pub(crate) mod text_node_codecs;
/// Helper macros to parse and serialise more easily.
#[macro_use]
mod macros;
#[cfg(test)]
pub(crate) mod test;

38
parsers/src/util/test.rs Normal file
View File

@ -0,0 +1,38 @@
use crate::core::{error::Error, FromXml, IntoXml};
use crate::Element;
#[allow(dead_code)]
pub(crate) fn roundtrip<T: IntoXml + FromXml>(s: &str) {
let initial: Element = s.parse().unwrap();
let structural = match T::from_tree(initial.clone()) {
Ok(v) => v,
Err(e) => panic!("failed to parse from {:?}: {}", s, e),
};
let recovered = structural
.into_tree()
.expect("roundtrip did not produce an element");
assert_eq!(initial, recovered);
}
pub(crate) fn roundtrip_full<T: IntoXml + FromXml + PartialEq + std::fmt::Debug + Clone>(s: &str) {
let initial: Element = s.parse().unwrap();
let structural = match T::from_tree(initial.clone()) {
Ok(v) => v,
Err(e) => panic!("failed to parse from {:?}: {}", s, e),
};
let recovered = structural
.clone()
.into_tree()
.expect("roundtrip did not produce an element");
assert_eq!(initial, recovered);
let structural2 = match T::from_tree(recovered) {
Ok(v) => v,
Err(e) => panic!("failed to parse from serialisation of {:?}: {}", s, e),
};
assert_eq!(structural, structural2);
}
pub(crate) fn parse_str<T: FromXml>(s: &str) -> Result<T, Error> {
let initial: Element = s.parse().unwrap();
T::from_tree(initial)
}

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

@ -0,0 +1,20 @@
[package]
name = "xso_proc"
version = "0.1.0"
authors = [
"Jonas Schäfer <jonas@zombofant.net>",
]
description = "Macro implementation of #[derive(FromXml, IntoXml)]"
homepage = "https://xmpp.rs"
repository = "https://gitlab.com/xmpp-rs/xmpp-rs"
keywords = ["xso", "derive", "serialization"]
license = "MPL-2.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
quote = "^1"
syn = { version = "^2", features = ["full", "extra-traits"] }
proc-macro2 = "^1"

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

@ -0,0 +1,144 @@
/*!
Helpers used both for enums and structs.
*/
use proc_macro2::TokenStream;
use quote::quote;
use syn::*;
use crate::error_message::ParentRef;
/// 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)
}
}
/// Build a statement calling the validator function at `validate`, if any.
///
/// This assumes that the argument for `validate` is called `result`.
pub(crate) fn build_validate(validate: Option<&Path>) -> Stmt {
syn::parse2(if let Some(validate) = validate {
quote! {
#validate(&mut result)?;
}
} else {
quote! {
{ let _ = &mut result; };
}
})
.expect("failed to build validation code")
}
/// Build a statement calling the preparation function at `prepare`, if any.
///
/// The argument passed to `prepare` is `value_ident`.
pub(crate) fn build_prepare(prepare: Option<&Path>, value_ident: &Ident) -> TokenStream {
if let Some(prepare) = prepare {
quote! {
#prepare(&mut #value_ident);
}
} else {
quote! {
{ let _ = &mut #value_ident; };
}
}
}
pub trait ItemDef: std::fmt::Debug {
/// Construct an expression which consumes `residual` and evaluates to
/// `Result<T, Error>`.
///
/// - `item_name` may contain either the path necessary to construct an
/// instance of the item or a nested parent ref. The latter case may not
/// be supported by all implementations of `ItemDef`.
///
/// - `residual` must be the identifier of the `minidom::Element` to
/// process.
fn build_try_from_element(
&self,
item_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream>;
/// Construct an expression which consumes the `T` value at `value_ident`
/// and returns a `minidom::Element`.
///
/// - `item_name` is used primarily for diagnostic messages.
///
/// - `value_ident` must be the identifier at which the entire struct can
/// be reached. It is used during preparation.
fn build_into_element(&self, item_name: &ParentRef, value_ident: &Ident)
-> Result<TokenStream>;
/// Construct a token stream containing the entire body of the
/// `impl DynNamespace` block.
///
/// Can only be used on `namespace = dyn` items; any other variants will
/// cause an appropriate compile-time error.
fn build_dyn_namespace(&self) -> Result<TokenStream>;
}
impl<T: ItemDef + ?Sized> ItemDef for Box<T> {
fn build_try_from_element(
&self,
item_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
(**self).build_try_from_element(item_name, residual)
}
fn build_into_element(
&self,
item_name: &ParentRef,
value_ident: &Ident,
) -> Result<TokenStream> {
(**self).build_into_element(item_name, value_ident)
}
fn build_dyn_namespace(&self) -> Result<TokenStream> {
(**self).build_dyn_namespace()
}
}

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

@ -0,0 +1,428 @@
/*!
# 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;
use syn::{spanned::Spanned, *};
use crate::error_message::ParentRef;
use crate::field::{FieldDef, FieldParsePart};
/// Expand the given identifier to resolve as member of the
/// `UnknownChildPolicy` enum. If the identifier is absent, invoke
/// `UnknownChildPolicy::default()` instead.
fn default_on_unknown_child(p: Option<Ident>) -> Expr {
match p {
Some(v) => Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Path {
leading_colon: Some(token::PathSep {
spans: [Span::call_site(), Span::call_site()],
}),
segments: [
PathSegment::from(Ident::new("xso", Span::call_site())),
PathSegment::from(Ident::new("UnknownChildPolicy", Span::call_site())),
PathSegment::from(v),
]
.into_iter()
.collect(),
},
}),
None => syn::parse2(quote! { ::xso::UnknownChildPolicy::default() })
.expect("failed to construct default unknown child policy"),
}
}
/// Expand the given identifier to resolve as member of the
/// `UnknownAttributePolicy` enum. If the identifier is absent, invoke
/// `UnknownAttributePolicy::default()` instead.
fn default_on_unknown_attribute(p: Option<Ident>) -> Expr {
match p {
Some(v) => Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Path {
leading_colon: Some(token::PathSep {
spans: [Span::call_site(), Span::call_site()],
}),
segments: [
PathSegment::from(Ident::new("xso", Span::call_site())),
PathSegment::from(Ident::new("UnknownAttributePolicy", Span::call_site())),
PathSegment::from(v),
]
.into_iter()
.collect(),
},
}),
None => syn::parse2(quote! { ::xso::UnknownAttributePolicy::default() })
.expect("failed to construct default unknown attribute policy"),
}
}
/// An struct or enum variant's contents.
///
/// This struct is used to generate the parsing/serialisation loop, but most
/// notably it is *not* responsible for matching the incoming element; that
/// is the responsibility of the caller of the corresponding functions.
#[derive(Debug)]
pub(crate) struct Compound {
/// The fields, in declaration order.
fields: Vec<FieldDef>,
/// Information about the field annotated with `#[xml(namespace)]`, if
/// any.
namespace_field: Option<(Span, Type, Member)>,
/// Member of the `UnknownChildPolicy` enum to use when handling unknown
/// children.
on_unknown_child: Expr,
/// Member of the `UnknownAttributePolicy` enum to use when handling
/// unknown attributes.
on_unknown_attribute: Expr,
}
impl Compound {
/// Construct a new compound from an iterator of [`FieldDef`] structs.
pub(crate) fn new<T: Iterator<Item = Result<FieldDef>>>(
on_unknown_child: Option<Ident>,
on_unknown_attribute: Option<Ident>,
input: T,
) -> Result<Self> {
let mut fields = Vec::with_capacity(input.size_hint().1.unwrap_or(0));
let mut text_field: Option<Span> = None;
let mut namespace_field: Option<(Span, Type, Member)> = None;
let mut collect_wildcard_field: Option<Span> = None;
for field in input {
let field = field?;
if let Some(ty) = field.namespace_field_type() {
if namespace_field.is_some() {
return Err(Error::new_spanned(
field.ident,
"only one #[xml(namespace)] field is allowed",
));
}
namespace_field = Some((field.span, ty.clone(), field.ident.clone()));
};
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_child_wildcard() {
if collect_wildcard_field.is_some() {
return Err(Error::new_spanned(
field.ident,
"only one #[xml(elements)] field without namespace/name selector is allowed",
));
}
collect_wildcard_field = Some(field.ident.span());
}
fields.push(field);
}
Ok(Self {
fields,
namespace_field,
on_unknown_child: default_on_unknown_child(on_unknown_child),
on_unknown_attribute: default_on_unknown_attribute(on_unknown_attribute),
})
}
/// Construct a compound from [`syn::Fields`].
///
/// This a convenience wrapper around [`Self::new`], converting the
/// [`syn::Field`] structs to [`FieldDef`].
pub(crate) fn from_fields(
on_unknown_child: Option<Ident>,
on_unknown_attribute: Option<Ident>,
fields: &Fields,
) -> Result<Self> {
Self::new(
on_unknown_child,
on_unknown_attribute,
fields.iter().enumerate().map(|(i, field)| {
FieldDef::from_field(field, i.try_into().expect("too many fields"))
}),
)
}
/// Obtain references to the information about the
/// `#[xml(namespace)]`-annotated field, if this compound has one.
pub(crate) fn namespace_field(&self) -> Option<(Span, &Type, &Member)> {
self.namespace_field.as_ref().map(|(a, b, c)| (*a, b, c))
}
/// Number of fields.
pub(crate) fn field_count(&self) -> usize {
self.fields.len()
}
/// Construct a token stream which contains an expression which parses
/// the contents `minidom::Element` at `residual` into the compound.
///
/// - `container_name` is used both for error messages and to construct
/// the resulting compound. If it directly refers to a path, that path
/// is used as constructor. Otherwise, the compound is constructed as
/// tuple.
///
/// - `container_namespace_expr` must be an expression which evaluates to
/// the parsed namespace of the parent element. If
/// [`Self::namespace_field`] is not `None`, this must be an expression
/// which can be moved out of and which matches the type, as that
/// expression will be used to initialize that field.
///
/// In all other cases, this expression only needs to be usable in a
/// `std::cmp::PartialEq<str>` context.
///
/// - `residual` must be the identifier at which the element is found.
/// - `forgive_attributes` must be a (potentially empty) slice of XML
/// attribute names to ignore during the unknown attribute check. This
/// can be used to ignore attributes which have been used in element
/// matching (e.g. enum discriminators).
pub(crate) fn build_try_from_element(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
residual: &Ident,
forgive_attributes: &[&str],
) -> Result<TokenStream> {
let readable_name = container_name.to_string();
let on_unknown_child = &self.on_unknown_child;
let on_unknown_attribute = &self.on_unknown_attribute;
let mut init = quote! {};
let mut tupinit = quote! {};
let mut attrcheck = quote! {
#(
if key == #forgive_attributes {
continue;
}
)*
};
let mut tempinit = quote! {};
let mut childiter = quote! {};
let mut childfallback = quote! {
#on_unknown_child.trigger(concat!("Unknown child in ", #readable_name, "."))?;
};
let mut had_fallback: bool = false;
for field in self.fields.iter() {
let ident = field.ident.clone();
let FieldParsePart {
tempinit: field_tempinit,
childiter: field_childiter,
attrcheck: field_attrcheck,
value,
childfallback: field_childfallback,
} = field.build_try_from_element(container_name, container_namespace_expr)?;
attrcheck = quote! { #attrcheck #field_attrcheck };
tempinit = quote! { #tempinit #field_tempinit };
childiter = quote! { #childiter #field_childiter };
if let Some(field_childfallback) = field_childfallback {
if had_fallback {
panic!(
"internal error: multiple fields attempting to collect all child elements."
);
}
had_fallback = true;
childfallback = field_childfallback;
}
init = quote! {
#init
#ident: #value,
};
tupinit = quote! {
#tupinit
#value,
};
}
let construct = match container_name {
ParentRef::Named(ref path) => quote! {
#path {
#init
}
},
ParentRef::Unnamed { .. } | ParentRef::Wrapper { .. } => quote! {
( #tupinit )
},
};
Ok(quote! {
{
for (key, _) in #residual.attrs() {
#attrcheck
#on_unknown_attribute.trigger(concat!("Unknown attribute in ", #readable_name, "."))?;
}
#tempinit
for mut residual in #residual.take_contents_as_children() {
#childiter
#childfallback
}
#construct
}
})
}
/// Construct an expression consuming the `builder` and returning it,
/// filled with the data from the compound.
///
/// - `container_name` is used for error messages.
///
/// - `container_namespace_expr` must be an expression which evaluates to
/// the parsed namespace of the parent element. This needs to implement
/// `::xso::DynNamespaceEnum` if any fields use
/// `#[xml(namespace = super)]`.
///
/// - `builder` must be an expression which can be moved out from and
/// which is the `minidom::Builder` into which the element should be
/// constructed.
///
/// - `access_field`: Accessor function for fields within the compound.
/// That function will be called for each field and the expression is
/// used exactly once to obtain the data of the field inside the
/// compound.
///
/// This indirection is necessary to be able to handle both structs
/// (where fields are accessed as struct members) and enumeration
/// variants (where fields are bound to local names).
///
/// Note that the field referenced by [`Self::namespace_field`] is not
/// accessed by this function.
pub(crate) fn build_into_element(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
builder: &Ident,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
let mut build = quote! {};
for field in self.fields.iter() {
let field_build = field.build_into_element(
container_name,
container_namespace_expr,
&mut access_field,
)?;
build = quote! {
#build
builder = #field_build;
};
}
Ok(quote! {
{
let mut builder = #builder;
#build
builder
}
})
}
/// Return an iterator which returns the [`syn::Member`] structs to access
/// 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> + '_> {
Box::new(self.fields.iter().map(|x| x.ident.clone().into()))
}
/// If and only if this compound has exactly one field, return a reference
/// to that field's type.
pub(crate) fn single_type(&self) -> Option<&Type> {
if self.fields.len() != 1 {
None
} else {
Some(&self.fields[0].ty)
}
}
/// Return a [`DynCompound`] refering to `self`, if and only if this
/// compound has a valid `#[xml(namespace)]` field.
///
/// The function and type name refer to this being a precondition for a
/// valid `namespace = dyn` struct or enum variant.
pub(crate) fn as_dyn(&self) -> Option<DynCompound<'_>> {
let (_, ty, member) = self.namespace_field.as_ref()?;
Some(DynCompound {
namespace_ty: ty,
namespace_member: member,
fields: &self.fields,
})
}
}
/// Reference to a [`Compound`] which has proven that it has a
/// `#[xml(namespace)]` field.
///
/// This simplifies some checks here and there.
pub(crate) struct DynCompound<'x> {
/// The type of the `#[xml(namespace)]` field.
namespace_ty: &'x Type,
/// The member referring to the `#[xml(namespace)]` field.
namespace_member: &'x Member,
/// The fields of the compound.
fields: &'x [FieldDef],
}
impl<'x> DynCompound<'x> {
/// Return a reference to the [`Type`] of the field annotated with
/// `#[xml(namespace)]`.
pub(crate) fn namespace_ty(&self) -> &'x Type {
self.namespace_ty
}
/// Build the implementation of
/// `DynNamespace::namespace(&self) -> &Self::Namespace`.
pub(crate) fn build_get_namespace(
&self,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
let member = access_field(self.namespace_member.clone());
Ok(quote! {
&#member
})
}
/// Build the implementation of
/// `DynNamespace::set_namespace<T: Into<Self::Namespace>>(&mut self, ns: T)`.
pub(crate) fn build_set_namespace(
&self,
input: &Ident,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
let member = access_field(self.namespace_member.clone());
let mut field_impls = quote! {};
for field in self.fields {
let field_impl = field.build_set_namespace(input, &mut access_field);
field_impls.extend(field_impl);
}
Ok(quote! {
#field_impls
#member = #input;
})
}
}

1061
xso-proc/src/enums.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,183 @@
//! Infrastructure for contextual error messages
use std::fmt;
use proc_macro2::Span;
use syn::{spanned::Spanned, Ident, Member, Path};
/// Reference to a compound's parent
///
/// This reference can be converted to a hopefully-useful human-readable
/// string via [`std::fmt::Display`].
#[derive(Clone, Debug)]
pub(super) enum ParentRef {
/// The parent is addressable by a path, e.g. a struct type or enum
/// variant.
Named(Path),
/// The parent is not addressable.
///
/// This is typically the case for compounds created for `ExtractDef`.
Unnamed {
/// The parent's ref.
///
/// For extracts, this refers to the compound where the field with
/// the extract is declared.
parent: Box<ParentRef>,
/// The field inside that parent.
///
/// For extracts, this refers to the compound field where the extract
/// is declared.
field: Member,
},
/// The parent is not addressable, but it is also not the child of another
/// addressable thing.
///
/// This is typically the case for compounds created for `Wrapped`.
Wrapper {
/// A reference to something nameable
inner: Box<ParentRef>,
},
}
impl From<Path> for ParentRef {
fn from(other: Path) -> Self {
Self::Named(other)
}
}
impl<'x> From<&'x Path> for ParentRef {
fn from(other: &'x Path) -> Self {
Self::Named(other.clone())
}
}
impl ParentRef {
/// Create a new `ParentRef` for a member inside this one.
///
/// Returns a [`Self::Unnamed`] with `self` as parent and `member` as
/// field.
pub(crate) fn child(&self, member: Member) -> Self {
Self::Unnamed {
parent: Box::new(self.clone()),
field: member,
}
}
/// Create a new `ParentRef` for a wrapper of this one.
///
/// Returns a [`Self::Wrapper`] with `self` as inner element.
pub(crate) fn wrapper(&self) -> Self {
Self::Wrapper {
inner: Box::new(self.clone()),
}
}
/// Return a span which can be used for error messages.
///
/// This points at the closest [`Self::Named`] variant in the parent
/// chain.
#[allow(dead_code)]
pub(crate) fn span(&self) -> Span {
match self {
Self::Named(p) => p.span(),
Self::Unnamed { parent, .. } => parent.span(),
Self::Wrapper { inner, .. } => inner.span(),
}
}
/// Try to extract an ident from this ParentRef.
pub(crate) fn try_as_ident(&self) -> Option<&Ident> {
match self {
Self::Named(path) => {
if path.leading_colon.is_some() {
return None;
}
if path.segments.len() != 1 {
return None;
}
let segment = &path.segments[0];
if !segment.arguments.is_empty() {
return None;
}
Some(&segment.ident)
}
_ => None,
}
}
}
impl fmt::Display for ParentRef {
fn fmt<'f>(&self, f: &'f mut fmt::Formatter) -> fmt::Result {
match self {
Self::Named(name) => {
let mut first = true;
for segment in name.segments.iter() {
if !first {
write!(f, "::")?;
}
first = false;
write!(f, "{}", segment.ident)?;
}
write!(f, " element")
}
Self::Unnamed { parent, field } => {
write!(f, "extraction for {} in {}", FieldName(field), parent)
}
Self::Wrapper { inner } => {
write!(f, "wrapper element of {}", inner)
}
}
}
}
/// Ephemeral struct to create a nice human-readable representation of
/// [`syn::Member`].
///
/// It implements [`std::fmt::Display`] for that purpose and is otherwise of
/// little use.
#[repr(transparent)]
struct FieldName<'x>(&'x Member);
impl<'x> fmt::Display for FieldName<'x> {
fn fmt<'f>(&self, f: &'f mut fmt::Formatter) -> fmt::Result {
match self.0 {
Member::Named(v) => write!(f, "field '{}'", v),
Member::Unnamed(v) => write!(f, "unnamed field {}", v.index),
}
}
}
/// Create a string error message for a missing child element.
///
/// `parent_name` should point at the compound which is being parsed and
/// `field` should be the field to which the child belongs.
pub(super) fn on_missing_child(parent_name: &ParentRef, field: &Member) -> String {
format!("Missing child {} in {}.", FieldName(&field), parent_name)
}
/// Create a string error message for a duplicate child element.
///
/// `parent_name` should point at the compound which is being parsed and
/// `field` should be the field to which the child belongs.
pub(super) fn on_duplicate_child(parent_name: &ParentRef, field: &Member) -> String {
format!(
"{} must not have more than one child in {}.",
parent_name,
FieldName(&field)
)
}
/// Create a string error message for a missing attribute.
///
/// `parent_name` should point at the compound which is being parsed and
/// `field` should be the field to which the attribute belongs.
pub(super) fn on_missing_attribute(parent_name: &ParentRef, field: &Member) -> String {
format!(
"Required attribute {} on {} missing.",
FieldName(&field),
parent_name
)
}

View File

@ -0,0 +1,180 @@
//! Infrastructure for parsing fields from attributes.
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, *};
use crate::error_message::{self, ParentRef};
use crate::meta::{FlagOr, Name, NameRef};
use super::{Field, FieldParsePart};
/// A field parsed from an XML attribute.
///
/// Maps to `#[xml(attribute)]`.
#[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`] or a given callable should be used to obtain a
/// value if the attribute is missing.
///
/// If the flag is *not* set, an error is returned when parsing an element
/// without the attribute.
pub(super) default_: FlagOr<Path>,
/// The codec implementation to use.
///
/// If set, parsing does not use the `FromXmlText` / `IntoXmlText` traits
/// but instead uses the `TextCodec` trait on the given type.
pub(super) codec: Option<Type>,
}
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.
///
/// `codec` must be the value of the `codec = ..` option, which, if given
/// overrides how the attribute's text is converted to a rust value.
///
/// `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_: FlagOr<Path>,
codec: Option<Type>,
) -> 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_,
codec,
})
}
}
impl Field for AttributeField {
fn build_try_from_element(
&self,
container_name: &ParentRef,
_container_namespace_expr: &Expr,
_tempname: Ident,
member: &Member,
ty: &Type,
) -> Result<FieldParsePart> {
let missing_msg = error_message::on_missing_attribute(container_name, &member);
let name = &self.name;
let on_missing = match self.default_ {
FlagOr::Absent => {
quote! {
return Err(::xso::error::Error::ParseError(
#missing_msg,
))
}
}
FlagOr::Present(_) => {
let ty_default =
quote_spanned! {ty.span()=> <#ty as ::std::default::Default>::default};
quote! {
#ty_default()
}
}
FlagOr::Value { ref value, .. } => {
quote! {
#value()
}
}
};
let decode = match self.codec {
Some(ref codec_ty) => {
let codec_ty_decode = quote_spanned! {codec_ty.span()=> <#codec_ty as ::xso::TextCodec::<#ty>>::decode};
quote! {
residual.attr(#name).map(|value| {
#codec_ty_decode(value)
}).transpose()?
}
}
None => {
let ty_from_optional_xml_text = quote_spanned! {ty.span()=> <#ty as ::xso::FromOptionalXmlText>::from_optional_xml_text};
quote! {
#ty_from_optional_xml_text(residual.attr(#name))?
}
}
};
Ok(FieldParsePart {
attrcheck: quote! {
if key == #name {
continue;
}
},
value: quote! {
match #decode {
Some(v) => v,
None => #on_missing,
}
},
..FieldParsePart::default()
})
}
fn build_into_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
_member: &Member,
ty: &Type,
access: Expr,
) -> Result<TokenStream> {
let encode = match self.codec {
Some(ref codec_ty) => {
let codec_ty_encode = quote_spanned! {codec_ty.span()=> <#codec_ty as ::xso::TextCodec::<#ty>>::encode};
quote! {
#codec_ty_encode(#access)
}
}
None => {
let ty_into_optional_xml_text = quote_spanned! {ty.span()=> <#ty as ::xso::IntoOptionalXmlText>::into_optional_xml_text};
quote! {
#ty_into_optional_xml_text(#access)
}
}
};
let name = &self.name;
Ok(quote! {
match #encode {
Some(v) => builder.attr(#name, v),
None => builder,
}
})
}
fn build_set_namespace(
&self,
_input: &Ident,
_ty: &Type,
_access: Expr,
) -> Result<TokenStream> {
Ok(TokenStream::default())
}
}

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

@ -0,0 +1,707 @@
//! Infrastructure for parsing fields from child elements while destructuring
//! their contents.
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, *};
use crate::compound::Compound;
use crate::error_message::{self, ParentRef};
use crate::meta::{Flag, FlagOr, NameRef, NamespaceRef, XmlFieldMeta};
use super::{ChildMode, Field, FieldDef, FieldNamespace, FieldParsePart};
/// Definition of a child data extraction.
///
/// This is used to implement fields annotated with
/// `#[xml(child(.., extract(..))]` or `#[xml(children(.., extract(..)))]`.
#[derive(Debug)]
pub(super) struct ExtractDef {
namespace: FieldNamespace,
name: NameRef,
/// Compound which contains the arguments of the `extract(..)` attribute,
/// transformed into a struct with unnamed fields.
///
/// 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.
parts: Compound,
}
impl ExtractDef {
fn expand_namespace(&self, container_namespace_expr: &Expr) -> Expr {
match self.namespace {
FieldNamespace::Static(ref ns) => Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: ns.clone().into(),
}),
FieldNamespace::Super(_) => container_namespace_expr.clone(),
}
}
/// Construct an `ExtractDef`.
///
/// The `namespace` and `name` identify the XML element this `ExtractDef`
/// 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.
///
/// Finally, `single_extract_type` should be passed if the extract is used
/// in the context of a `#[xml(child)]` field (i.e. not for a container)
/// and it should then be the type of that field. This allows defaulting
/// the type of the extract's field to that type if it has not been
/// specified explicitly by the user.
fn new(
span: Span,
namespace: FieldNamespace,
name: NameRef,
parts: Vec<Box<XmlFieldMeta>>,
mut single_extract_type: Option<Type>,
) -> Result<Self> {
if parts.len() != 1 {
single_extract_type = None;
}
let parts = Compound::new(
None,
None,
parts.into_iter().enumerate().map(|(i, x)| {
FieldDef::from_extract(span.clone(), *x, i as u32, single_extract_type.take())
}),
)?;
Ok(Self {
namespace,
name,
parts,
})
}
/// Construct a token stream containing an expression which tries to
/// process the child element at the identifier `residual`.
///
/// 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`. If this extract consists of a
/// single field, that field is returned after an unbounded call to
/// `Into::into`. The call to `into` allows assignment to `Option<T>`.
///
/// If the extract consists of more than one field, these fields are
/// returned as tuple without any further conversion.
///
/// The `parent_namespace_expr`, if given, is evaluated, cloned and
/// latched into a local variable (in the generated code) and that is
/// passed on to the inner call to [`Compound::build_into_element`].
fn build_extract(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
residual: &Ident,
) -> Result<TokenStream> {
let namespace_expr = self.expand_namespace(container_namespace_expr);
let xml_name = &self.name;
let nfields = self.parts.field_count();
let repack = if nfields == 1 {
quote! { data.0.into() }
} else {
quote! { data }
};
let parse =
self.parts
.build_try_from_element(container_name, &namespace_expr, residual, &[])?;
let test_expr = match self.namespace {
FieldNamespace::Static(ref xml_namespace) => quote! {
#residual.is(#xml_name, #xml_namespace)
},
FieldNamespace::Super(_) => quote! {
::std::cmp::PartialEq::eq(&#namespace_expr, #residual.ns().as_str()) && #residual.name() == #xml_name
},
};
Ok(quote! {
if #test_expr {
let data = #parse;
Ok(#repack)
} else {
Err(#residual)
}
})
}
/// 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.
///
/// The `parent_namespace_expr`, if given, is evaluated, cloned and
/// latched into a local variable (in the generated code) and that is
/// passed on to the inner call to [`Compound::build_into_element`].
fn build_assemble(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
field: &Expr,
) -> Result<TokenStream> {
let xml_namespace = self.expand_namespace(container_namespace_expr);
let xml_name = &self.name;
let nfields = self.parts.field_count();
let ident = Ident::new("__extract_data", Span::call_site());
let repack = if nfields == 1 {
quote! { let #ident = (#ident,); }
} else {
quote! { let #ident = #ident; }
};
let builder = Ident::new("builder", Span::call_site());
let builder_init = match self.namespace {
FieldNamespace::Static(ref xml_namespace) => quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
#xml_namespace
)
},
FieldNamespace::Super(_) => quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
::xso::DynNamespaceEnum::into_xml_text(#xml_namespace.clone()),
)
},
};
let build =
self.parts
.build_into_element(container_name, &xml_namespace, &builder, |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! {
{
let #ident = #field;
#repack
let #builder = #builder_init;
let #builder = #build;
#builder.build()
}
})
}
/// Return the type of the only field, if this extract has exactly one
/// field, or None otherwise.
fn inner_type(&self) -> Option<&Type> {
self.parts.single_type()
}
}
/// A field parsed from an XML child, destructured into a Rust data structure.
///
/// Maps to `#[xml(child)]` and `#[xml(children)]`.
#[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(..))]`.
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`.
extract: Option<ExtractDef>,
/// If set, `extract` must be None, the child's type must implement
/// `DynNamespace` and the compound must use `namespace = dyn`.
super_namespace: Flag,
/// If set, the field's value will be generated using
/// [`std::default::Default`] or the given path if no matching child can
/// be found, instead of aborting parsing with an error.
default_: FlagOr<Path>,
/// If set, it must point to a function. That function will be called with
/// an immutable reference to the field's value and must return a boolean.
/// If that boolean is true, the child will not be emitted.
skip_if: Option<Path>,
/// If set, it must point to a type. The `FromXml`/`IntoXml`
/// implementations of that type will be used instead, and the type must
/// implement `ElementCodec<T>`, where `T` is the type of the field.
codec: Option<Path>,
}
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_` flag stored, see [`Self::default_`] for semantics.
///
/// `field_type` must be the type of the field. It is used to configure
/// the extract correctly, if it is specified and the mode is single.
///
/// `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<Box<XmlFieldMeta>>,
default_: FlagOr<Path>,
skip_if: Option<Path>,
codec: Option<Path>,
field_type: &Type,
) -> 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)) => FieldNamespace::Static(ns),
Some(NamespaceRef::Dyn(ns)) => {
return Err(Error::new_spanned(
ns,
"extracted fields cannot use dynamic namespaces",
))
}
Some(NamespaceRef::Super(ns)) => FieldNamespace::Super(ns),
};
let Some(name) = name else {
return Err(Error::new(
attr_span.clone(),
"name must be specified on extracted fields",
));
};
if let Some(codec) = codec {
return Err(Error::new_spanned(
codec,
"codec = .. cannot be combined with extract(..)",
));
}
let single_extract_type = 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",
));
};
Some(field_type.clone())
}
ChildMode::Collection => None,
};
Ok(Self {
mode,
extract: Some(ExtractDef::new(
attr_span.clone(),
namespace,
name.into(),
extract,
single_extract_type,
)?),
skip_if,
default_,
super_namespace: Flag::Absent,
codec: None,
})
} else {
let super_namespace = match namespace {
None => Flag::Absent,
Some(NamespaceRef::Super(ns)) => Flag::Present(ns.span),
Some(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_,
skip_if,
super_namespace,
codec,
})
}
}
}
impl Field for ChildField {
fn build_try_from_element(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
tempname: Ident,
member: &Member,
ty: &Type,
) -> Result<FieldParsePart> {
let ty_span = ty.span();
let ty_default = quote_spanned! {ty_span=> <#ty as std::default::Default>::default};
match self.mode {
ChildMode::Single => {
let missingerr = error_message::on_missing_child(container_name, &member);
let duperr = error_message::on_duplicate_child(container_name, &member);
let on_missing = match self.default_ {
FlagOr::Absent => {
quote! {
return Err(::xso::error::Error::ParseError(#missingerr));
}
}
FlagOr::Present(_) => {
quote! {
#ty_default()
}
}
FlagOr::Value { ref value, .. } => {
quote! {
#value()
}
}
};
match self.extract {
Some(ref extract) => {
let extract = extract.build_extract(
&container_name.child(member.clone()),
container_namespace_expr,
&Ident::new("residual", Span::call_site()),
)?;
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 = Some(v);
continue;
},
Err(residual) => residual,
};
},
value: quote! {
if let Some(v) = #tempname {
v
} else {
#on_missing
}
},
..FieldParsePart::default()
})
}
None => {
let ns_test = match self.super_namespace {
Flag::Absent => quote! { true },
Flag::Present(_) => quote! {
::std::cmp::PartialEq::eq(&#container_namespace_expr, residual.ns().as_str())
},
};
let codec_ty = match self.codec {
Some(ref ty) => Type::Path(TypePath {
qself: None,
path: ty.clone(),
}),
None => ty.clone(),
};
let field_ty = ty;
let codec_ty_span = codec_ty.span();
let codec_ty_from_tree = quote_spanned! {codec_ty_span=> <#codec_ty as ::xso::FromXml>::from_tree};
let codec_ty_absent =
quote_spanned! {codec_ty_span=> <#codec_ty as ::xso::FromXml>::absent};
let codec_ty_decode = quote_spanned! {codec_ty_span=> <#codec_ty as ::xso::ElementCodec::<#field_ty>>::decode};
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Option<#field_ty> = None;
},
childiter: quote! {
let mut residual = if #ns_test {
match #codec_ty_from_tree(residual) {
Ok(v) => {
let v = #codec_ty_decode(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),
}
} else {
residual
};
},
value: quote! {
if let Some(v) = #tempname {
v
} else if let Some(v) = #codec_ty_absent().map(#codec_ty_decode) {
v
} else {
#on_missing
}
},
..FieldParsePart::default()
})
}
}
}
ChildMode::Collection => {
let item_ty: Type = syn::parse2(quote_spanned! {ty_span=>
<#ty as IntoIterator>::Item
})
.expect("failed to construct item type");
let ty_try_extend =
quote_spanned! {ty_span=> <#ty as ::xso::TryExtend<#item_ty>>::try_extend};
match self.extract {
Some(ref extract) => {
let extract = extract.build_extract(
&container_name.child(member.clone()),
container_namespace_expr,
&Ident::new("residual", Span::call_site()),
)?;
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname = #ty_default();
},
childiter: quote! {
residual = match #extract {
Ok(v) => {
#ty_try_extend(&mut #tempname, [v])?;
continue;
},
Err(residual) => residual,
};
},
value: quote! { #tempname },
..FieldParsePart::default()
})
}
None => {
if let Flag::Present(span) = self.super_namespace {
return Err(Error::new(
span,
"#[xml(namespace = dyn)] not supported for #[xml(children)]",
));
}
let codec_ty = match self.codec {
Some(ref ty) => Type::Path(TypePath {
qself: None,
path: ty.clone(),
}),
None => item_ty.clone(),
};
let codec_ty_span = codec_ty.span();
let codec_ty_from_tree = quote_spanned! {codec_ty_span=> <#codec_ty as ::xso::FromXml>::from_tree};
let codec_ty_decode = quote_spanned! {codec_ty_span=> <#codec_ty as ::xso::ElementCodec::<#item_ty>>::decode};
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname = #ty_default();
},
childiter: quote! {
let mut residual = match #codec_ty_from_tree(residual) {
Ok(item) => {
let item = #codec_ty_decode(item);
#ty_try_extend(&mut #tempname, [item])?;
continue;
},
Err(::xso::error::Error::TypeMismatch(_, _, e)) => e,
Err(other) => return Err(other),
};
},
value: quote! { #tempname },
..FieldParsePart::default()
})
}
}
}
}
}
fn build_set_namespace(&self, input: &Ident, ty: &Type, access: Expr) -> Result<TokenStream> {
match self.mode {
ChildMode::Single => match self.extract {
Some(_) => Ok(quote! {}),
None => match self.super_namespace {
Flag::Absent => Ok(quote! {}),
Flag::Present(_) => {
let ty_span = ty.span();
// using quote_spanned in this way here causes the "the trait `DynNamespace` is not implemented for `..`" error message appear on member_ty instead of on the derive macro invocation.
let method =
quote_spanned! {ty_span=> <#ty as ::xso::DynNamespace>::set_namespace};
Ok(quote! {
#method(&mut #access, #input.clone());
})
}
},
},
_ => Ok(quote! {}),
}
}
fn build_into_element(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
member: &Member,
ty: &Type,
access: Expr,
) -> Result<TokenStream> {
let temp_ident = Ident::new("__data", Span::call_site());
let skip_map = match self.skip_if {
Some(ref callable) => quote! {
match #callable(&#temp_ident) {
false => Some(#temp_ident),
true => None,
}
},
None => quote! { Some(#temp_ident) },
};
match self.mode {
ChildMode::Single => match self.extract {
Some(ref extract) => {
let temp_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: temp_ident.clone().into(),
});
let inner_ty = extract
.inner_type()
.expect("child extract can only have one field!")
.clone();
let assemble = extract.build_assemble(
&container_name.child(member.clone()),
container_namespace_expr,
&temp_expr,
)?;
Ok(quote! {
match Option::<#inner_ty>::from(#access).and_then(|#temp_ident| #skip_map) {
Some(#temp_ident) => builder.append(::xso::exports::minidom::Node::Element(#assemble)),
None => builder,
}
})
}
None => {
let codec_ty = match self.codec {
Some(ref ty) => Type::Path(TypePath {
qself: None,
path: ty.clone(),
}),
None => ty.clone(),
};
let field_ty = ty;
let codec_ty_span = codec_ty.span();
let codec_ty_into_tree =
quote_spanned! {codec_ty_span=> <#codec_ty as ::xso::IntoXml>::into_tree};
let codec_ty_encode = quote_spanned! {codec_ty_span=> <#codec_ty as ::xso::ElementCodec::<#field_ty>>::encode};
Ok(quote! {
{
let #temp_ident = #access;
match #skip_map.map(#codec_ty_encode).and_then(#codec_ty_into_tree) {
Some(#temp_ident) => builder.append(::xso::exports::minidom::Node::Element(#temp_ident)),
None => builder,
}
}
})
}
},
ChildMode::Collection => match self.extract {
Some(ref extract) => {
let assemble = extract.build_assemble(
&container_name.child(member.clone()),
container_namespace_expr,
&Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: temp_ident.clone().into(),
}),
)?;
Ok(quote! {
builder.append_all(
#access.into_iter().filter_map(|#temp_ident| {
match #skip_map {
Some(#temp_ident) => Some(#assemble),
None => None,
}
})
)
})
}
None => {
let ty_span = ty.span();
let item_ty: Type = syn::parse2(quote_spanned! {ty_span=>
<#ty as IntoIterator>::Item
})
.expect("failed to construct item type");
let codec_ty = match self.codec {
Some(ref ty) => Type::Path(TypePath {
qself: None,
path: ty.clone(),
}),
None => item_ty.clone(),
};
let codec_ty_span = codec_ty.span();
let codec_ty_into_tree =
quote_spanned! {codec_ty_span=> <#codec_ty as ::xso::IntoXml>::into_tree};
let codec_ty_encode = quote_spanned! {codec_ty_span=> <#codec_ty as ::xso::ElementCodec::<#item_ty>>::encode};
Ok(quote! {
builder.append_all(#access.into_iter().filter_map(|#temp_ident| {
#skip_map.map(#codec_ty_encode).and_then(#codec_ty_into_tree).map(|el| ::xso::exports::minidom::Node::Element(el))
}))
})
}
},
}
}
}

View File

@ -0,0 +1,306 @@
//! Infrastructure for parsing fields from child elements without
//! destructuring their contents.
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, *};
use crate::error_message::{self, ParentRef};
use crate::meta::{FlagOr, Name, NameRef, NamespaceRef, StaticNamespace};
use crate::structs::ElementSelector;
use super::{Field, FieldParsePart};
/// A field parsed from an XML child, without destructuring it into Rust
/// data structures.
///
/// Maps to `#[xml(element)]`.
#[derive(Debug)]
pub(crate) struct ElementField {
/// Logic to select matching child elements.
selector: ElementSelector,
/// If set, the field value will be generated using
/// [`std::default::Default`] or the given callable if no matching child
/// element is encountered during parsing. If unset, an error is generated
/// instead and parsing of the parent element fails.
default_: FlagOr<Path>,
}
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_: FlagOr<Path>,
) -> Result<Self> {
let namespace = match namespace {
None => None,
Some(NamespaceRef::Static(ns)) => Some(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 = name.map(Name::from);
let selector = match (namespace, name) {
(Some(namespace), Some(name)) => ElementSelector::Qualified { namespace, name },
(Some(namespace), None) => ElementSelector::ByNamespace(namespace),
(None, Some(name)) => ElementSelector::ByName(name),
(None, None) => ElementSelector::Any,
};
Ok(Self { selector, default_ })
}
}
impl Field for ElementField {
fn build_try_from_element(
&self,
container_name: &ParentRef,
_container_namespace_expr: &Expr,
tempname: Ident,
member: &Member,
ty: &Type,
) -> Result<FieldParsePart> {
let test = self
.selector
.build_test(&Ident::new("residual", Span::call_site()));
let missingerr = error_message::on_missing_child(container_name, member);
let duperr = error_message::on_duplicate_child(container_name, member);
let ty_span = ty.span();
let ty_default = quote_spanned! {ty_span=> <#ty as std::default::Default>::default};
let ty_from_element =
quote_spanned! {ty_span=> <#ty as From<::xso::exports::minidom::Element>>::from};
let on_missing = match self.default_ {
FlagOr::Absent => {
quote! {
return Err(::xso::error::Error::ParseError(#missingerr));
}
}
FlagOr::Present(_) => {
quote! {
#ty_default()
}
}
FlagOr::Value { ref value, .. } => {
quote! {
#value()
}
}
};
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Option<::xso::exports::minidom::Element> = None;
},
childiter: quote! {
residual = if #test {
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_from_element(v)
} else {
#on_missing
}
},
..FieldParsePart::default()
})
}
fn build_into_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
_member: &Member,
ty: &Type,
access: Expr,
) -> Result<TokenStream> {
Ok(quote! {
match <#ty as Into<Option<::xso::exports::minidom::Element>>>::into(#access) {
Some(v) => builder.append(::xso::exports::minidom::Node::Element(v)),
None => builder
}
})
}
fn build_set_namespace(
&self,
_input: &Ident,
_ty: &Type,
_access: Expr,
) -> Result<TokenStream> {
Ok(TokenStream::default())
}
}
/// A field parsed from a XML children, without destructuring them into Rust
/// data structures.
///
/// Maps to `#[xml(elements)]`.
#[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()))),
})
}
}
impl Field for ElementsField {
fn is_child_wildcard(&self) -> bool {
match self.selector {
None => true,
_ => false,
}
}
fn build_try_from_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
tempname: Ident,
_member: &Member,
_ty: &Type,
) -> Result<FieldParsePart> {
match self.selector {
Some((ref field_namespace, ref 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()
}),
}
}
fn build_into_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
_member: &Member,
_ty: &Type,
access: Expr,
) -> Result<TokenStream> {
Ok(quote! {
builder.append_all(#access.into_iter().map(|elem| ::xso::exports::minidom::Node::Element(elem)))
})
}
fn build_set_namespace(
&self,
_input: &Ident,
_ty: &Type,
_access: Expr,
) -> Result<TokenStream> {
Ok(TokenStream::default())
}
}

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

@ -0,0 +1,135 @@
//! Infrastructure for parsing boolean fields indicating child presence.
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::*;
use crate::error_message::{self, ParentRef};
use crate::meta::{Name, NameRef, NamespaceRef, StaticNamespace};
use super::{Field, FieldParsePart};
/// A field parsed from the presence of an empty XML child.
///
/// Maps to `#[xml(flag)]`.
#[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(),
})
}
}
impl Field for FlagField {
fn build_try_from_element(
&self,
container_name: &ParentRef,
_container_namespace_expr: &Expr,
tempname: Ident,
member: &Member,
_ty: &Type,
) -> Result<FieldParsePart> {
let field_name = &self.name;
let field_namespace = &self.namespace;
let duperr = error_message::on_duplicate_child(container_name, member);
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()
})
}
fn build_into_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
_member: &Member,
_ty: &Type,
access: Expr,
) -> Result<TokenStream> {
let child_name = &self.name;
let child_namespace = &self.namespace;
Ok(quote! {
if #access {
builder.append(::xso::exports::minidom::Node::Element(::xso::exports::minidom::Element::builder(#child_name, #child_namespace).build()))
} else {
builder
}
})
}
fn build_set_namespace(
&self,
_input: &Ident,
_ty: &Type,
_access: Expr,
) -> Result<TokenStream> {
Ok(TokenStream::default())
}
}

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

@ -0,0 +1,548 @@
/*!
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.
*/
#[cfg(doc)]
pub(crate) mod attribute;
#[cfg(not(doc))]
mod attribute;
#[cfg(doc)]
pub(crate) mod child;
#[cfg(not(doc))]
mod child;
#[cfg(doc)]
pub(crate) mod element;
#[cfg(not(doc))]
mod element;
#[cfg(doc)]
pub(crate) mod flag;
#[cfg(not(doc))]
mod flag;
#[cfg(doc)]
pub(crate) mod namespace;
#[cfg(not(doc))]
mod namespace;
#[cfg(doc)]
pub(crate) mod text;
#[cfg(not(doc))]
mod text;
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, *};
use crate::error_message::ParentRef;
use crate::meta::{ChildMode, NamespaceRef, StaticNamespace, XmlFieldMeta};
use self::attribute::AttributeField;
use self::child::ChildField;
use self::element::{ElementField, ElementsField};
use self::flag::FlagField;
use self::namespace::NamespaceField;
use self::text::TextField;
pub(crate) trait Field: std::fmt::Debug {
/// Return true if and only if this field is a field collecting all XML
/// text of the element.
fn is_text(&self) -> bool {
false
}
/// Return true if and only if this field is a field collecting *all* XML
/// children of the element.
fn is_child_wildcard(&self) -> bool {
false
}
/// Return true if and only if this field is a field storing the namespace
/// of the XML element.
fn is_namespace(&self) -> bool {
false
}
/// Construct the code necessary to parse data from a `minidom::Element`
/// into the field.
///
/// - `container_name` should be the identifier or path of the containing
/// compound and is used to generate runtime error messages.
///
/// - `container_namespace_expr` may be an expression under which the
/// parent compound's `::xso::DynNamespaceEnum` value is
/// obtainable, if any. This is needed for fields and compounds
/// using `#[xml(namespace = super)]`.
///
/// - `tempname` must be an identifier which the implementation can freely
/// use for a temporary variable related to parsing.
///
/// - `member` must be the target struct member identifier or index, used
/// for error messages.
///
/// - `ty` must be the field's type.
fn build_try_from_element(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
tempname: Ident,
member: &Member,
ty: &Type,
) -> Result<FieldParsePart>;
/// Construct an expression which consumes the identifier `builder`, which
/// must be a minidom `Builder`, and returns it, modified in such a way
/// that it contains the field's data.
///
/// - `container_name` should be the identifier or path of the containing
/// compound and is used to generate runtime error messages.
///
/// - `container_namespace_expr` may be an expression under which the
/// parent compound's `::xso::DynNamespaceEnum` value is
/// obtainable, if any. This is needed for fields and compounds
/// using `#[xml(namespace = super)]`.
///
/// - `member` must be the target struct member identifier or index, used
/// for error messages.
///
/// - `ty` must be the field's type.
///
/// - `access` must be an expression to consume the field's data. It is
/// evaluated exactly once.
fn build_into_element(
&self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
member: &Member,
ty: &Type,
access: Expr,
) -> Result<TokenStream>;
/// Construct the code necessary to update the namespace on the field.
///
/// - `input` must be the identifier of the variable holding the new
/// namespace to set.
///
/// - `ty` must be the field's type.
///
/// - `access` must be an expression which can be written to, which allows
/// updating the field's value.
fn build_set_namespace(&self, input: &Ident, ty: &Type, access: Expr) -> Result<TokenStream>;
}
/// 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,
}
/// A XML namespace as declared on a field.
#[derive(Clone, Debug)]
pub(crate) enum FieldNamespace {
/// The namespace is a static string.
Static(
/// The namespace as [`Path`] pointing at the static string.
StaticNamespace,
),
/// Instead of a fixed namespace, the namespace of the parent is used.
///
/// This is only allowed inside enum variants or structs with a
/// `#[xml(namespace = dyn)]` declaration, i.e. using the
/// [`crate::structs::StructNamespace::Dyn`] variant.
Super(
/// The `super` token from the `#[xml(namespace = super)]` meta.
Token![super],
),
}
impl From<FieldNamespace> for NamespaceRef {
fn from(other: FieldNamespace) -> Self {
match other {
FieldNamespace::Static(ns) => Self::Static(ns),
FieldNamespace::Super(ns) => Self::Super(ns),
}
}
}
#[derive(Debug)]
pub(crate) struct Ignore;
impl Field for Ignore {
fn build_try_from_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
_tempname: Ident,
_member: &Member,
ty: &Type,
) -> Result<FieldParsePart> {
let ty_default = quote_spanned! {ty.span()=> <#ty as std::default::Default>::default};
Ok(FieldParsePart {
value: quote! { #ty_default() },
..FieldParsePart::default()
})
}
fn build_into_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
_member: &Member,
_ty: &Type,
_access: Expr,
) -> Result<TokenStream> {
Ok(quote! { builder })
}
fn build_set_namespace(
&self,
_input: &Ident,
_ty: &Type,
_access: Expr,
) -> Result<TokenStream> {
Ok(TokenStream::default())
}
}
/// Construct a `Field` implementation by processing the options in the meta.
///
/// *Note*: Explicit type specifications are rejected by this function.
/// Those must have been taken out of the `meta` before calling this
/// function. The compile-time error message emitted by this function is
/// not very useful in this case, so if you do not want to support
/// explicit types, you should take them out at the call site and emit
/// a more useful error there.
fn new_field(
span: &Span,
field_ident: Option<&Ident>,
field_ty: &Type,
meta: XmlFieldMeta,
) -> Result<Box<dyn Field>> {
match meta {
XmlFieldMeta::Attribute {
name,
default_,
codec,
ty,
} => {
if let Some(ty) = ty {
return Err(Error::new_spanned(ty, "cannot set attribute type here"));
};
Ok(Box::new(AttributeField::new(
span,
field_ident,
name,
default_,
codec,
)?))
}
XmlFieldMeta::Child {
mode,
namespace,
name,
extract,
default_,
skip_if,
codec,
} => Ok(Box::new(ChildField::new(
span, mode, namespace, name, extract, default_, skip_if, codec, field_ty,
)?)),
XmlFieldMeta::Text { codec, ty } => {
if let Some(ty) = ty {
return Err(Error::new_spanned(ty, "cannot set text type here"));
};
Ok(Box::new(TextField::new(span, codec)?))
}
XmlFieldMeta::Namespace => Ok(Box::new(NamespaceField::new())),
XmlFieldMeta::Element {
namespace,
name,
default_,
} => Ok(Box::new(ElementField::new(
span, namespace, name, default_,
)?)),
XmlFieldMeta::Elements { namespace, name } => {
Ok(Box::new(ElementsField::new(span, namespace, name)?))
}
XmlFieldMeta::Flag { namespace, name } => {
Ok(Box::new(FlagField::new(span, namespace, name)?))
}
XmlFieldMeta::Ignore => Ok(Box::new(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 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,
}
}
} */
/// All data necessary to generate code to convert a Rust field to or from
/// XML.
#[derive(Debug)]
pub(crate) struct FieldDef {
/// The span of the `#[xml]` meta defining this field.
pub(crate) span: Span,
/// 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: Box<dyn Field>,
}
fn try_unwrap_option_type(ty: Type) -> Type {
match ty {
syn::Type::Path(ty) => {
if ty.path.segments.len() != 1 {
return syn::Type::Path(ty);
}
let segment = &ty.path.segments[0];
if segment.ident != "Option" {
return syn::Type::Path(ty);
}
match segment.arguments {
PathArguments::AngleBracketed(ref args) => {
if args.args.len() != 1 {
return syn::Type::Path(ty);
}
let arg = &args.args[0];
match arg {
// finally found the inner of Option<T>
GenericArgument::Type(ty) => ty.clone(),
_ => return syn::Type::Path(ty),
}
}
_ => return syn::Type::Path(ty),
}
}
other => other,
}
}
impl FieldDef {
/// Generate a [`FieldDef`] as extract from an [`XmlFieldMeta`]
/// specification.
///
/// This is used to build a [`crate::compound::Compound`] used to parse
/// the extract. As it would otherwise be an insane amount of duplication,
/// the compound parsing logic is re-used.
///
/// `extract` must be the extract specification parsed from the attribute.
/// `index` must be the number of the field: all extract fields are
/// unnumbered.
///
/// `single_extract_type` should be the type of the field this extract is
/// assigned to, in case that field is a `#[xml(child)]` (i.e. a
/// non-collection field). If given and the user did not specify a type
/// on the extract, that type is used (instead of the fallback to
/// `String`).
///
/// However, there's one minor catch: If the type matches `Option<T>`
/// verbatimly (in particular not `std::option::Option<T>`), the inner type
/// `T` is used instead. This is immensely helpful for optional child
/// extracts and can be overridden by the user by explicitly specifying a
/// type.
fn from_extract(
span: Span,
mut extract: XmlFieldMeta,
index: u32,
single_extract_type: Option<Type>,
) -> Result<Self> {
let string_ty: Type =
parse_str("::std::string::String").expect("cannot construct string type");
let ty = extract.determine_type();
let ty = ty
.or(single_extract_type.map(try_unwrap_option_type))
.unwrap_or(string_ty);
let kind = new_field(
&span, None, // field_ident is always none here, we use unnamed fields.
&ty, extract,
)?;
Ok(Self {
span,
ident: Member::Unnamed(Index {
index,
span: Span::call_site(),
}),
ty,
kind,
})
}
/// Return a reference to the field's type, if and only if it is a
/// [`NamespaceField`][`crate::field::namespace::NamespaceField`].
pub(crate) fn namespace_field_type(&self) -> Option<&Type> {
if self.kind.is_namespace() {
Some(&self.ty)
} else {
None
}
}
/// Generate a [`FieldDef`] from a [`syn::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: &syn::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((mut 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(),
}),
};
if let Some(ty) = meta.take_type() {
return Err(Error::new_spanned(
ty,
"specifying the type on struct or enum variant fields is redundant and not allowed",
));
}
let kind = new_field(&span, field.ident.as_ref(), &field.ty, meta)?;
Ok(Self {
span,
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,
container_name: &ParentRef,
container_namespace_expr: &Expr,
) -> Result<FieldParsePart> {
let ident = &self.ident;
let ty = &self.ty;
let tempname = quote::format_ident!("__field_init_{}", ident);
self.kind.build_try_from_element(
container_name,
container_namespace_expr,
tempname,
ident,
ty,
)
}
/// Construct a [`TokenStream`] which propagates the parent's namespace
/// to all children with `namespace = super` as well as the namespace
/// field itself.
pub(crate) fn build_set_namespace(
&self,
input: &Ident,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
let member = access_field(self.ident.clone());
let ty = &self.ty;
self.kind.build_set_namespace(input, ty, member)
}
/// 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,
container_name: &ParentRef,
container_namespace_expr: &Expr,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
let member = &self.ident;
let ident = access_field(member.clone());
let ty = &self.ty;
self.kind
.build_into_element(container_name, container_namespace_expr, member, ty, ident)
}
}

View File

@ -0,0 +1,81 @@
//! Infrastructure for parsing dynamic namespaces.
use proc_macro2::TokenStream;
use quote::quote;
use syn::*;
use crate::error_message::ParentRef;
use super::{Field, FieldParsePart};
/// A field holding the dynamic namespace of a compound with
/// `#[xml(namespace = dyn)]`.
///
/// See also
/// [`StructNamespace::Dyn`][`crate::structs::StructNamespace::Dyn`].
///
/// Maps to `#[xml(namespace)]`.
#[derive(Debug)]
pub(crate) struct NamespaceField;
impl NamespaceField {
/// Construct a new `#[xml(namespace)]` field.
pub(super) fn new() -> Self {
Self
}
}
impl Field for NamespaceField {
fn is_namespace(&self) -> bool {
true
}
/// Construct code which copies the value from the `namespace_tempname`
/// into the value of the field.
///
/// The actual parsing is special, because it has to happen during type
/// matching, and happens in
/// [`Compound::build_try_from_element`][`crate::compound::Compound::build_try_from_element`].
fn build_try_from_element(
&self,
_container_name: &ParentRef,
container_namespace_expr: &Expr,
_tempname: Ident,
_member: &Member,
_ty: &Type,
) -> Result<FieldParsePart> {
Ok(FieldParsePart {
value: quote! { #container_namespace_expr },
..FieldParsePart::default()
})
}
/// This is a no-op, because the actual implementation is in
/// [`crate::compound::DynCompound::build_set_namespace`].
fn build_set_namespace(
&self,
_input: &Ident,
_ty: &Type,
_access: Expr,
) -> Result<TokenStream> {
// NOTE: does nothing because this is handled specially by
// Compound::build_set_namespace
Ok(quote! {})
}
/// This is a no-op.
///
/// The actual implementation of the specialities of dynamic namespace
/// fields resides in
/// [`Compound::build_into_element`][`crate::compound::Compound::build_into_element`].
fn build_into_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
_member: &Member,
_ty: &Type,
_access: Expr,
) -> Result<TokenStream> {
Ok(quote! {builder})
}
}

119
xso-proc/src/field/text.rs Normal file
View File

@ -0,0 +1,119 @@
//! Infrastructure for parsing fields from text.
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, *};
use crate::error_message::ParentRef;
use super::{Field, FieldParsePart};
/// A field parsed from a XML text.
///
/// Maps to `#[xml(text)]`.
#[derive(Debug)]
pub(crate) struct TextField {
/// The codec implementation to use.
///
/// If set, parsing does not use the `FromXmlText` / `IntoXmlText` traits
/// but instead uses the `TextCodec` trait on the given type.
pub(super) codec: Option<Type>,
}
impl TextField {
/// Construct a new `#[xml(text)]` field.
///
/// `codec` must be the value of the `codec = ..` option, which, if given
/// overrides how the text is converted to a rust value.
///
/// `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, codec: Option<Type>) -> Result<Self> {
Ok(Self { codec })
}
}
impl Field for TextField {
fn is_text(&self) -> bool {
true
}
fn build_try_from_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
tempname: Ident,
_member: &Member,
ty: &Type,
) -> Result<FieldParsePart> {
let decode = match self.codec {
Some(ref codec_ty) => {
let codec_ty_decode = quote_spanned! {codec_ty.span()=> <#codec_ty as ::xso::TextCodec::<#ty>>::decode};
quote! {
#codec_ty_decode(&residual.text())?
}
}
None => {
let ty_from_xml_text =
quote_spanned! {ty.span()=> <#ty as ::xso::FromXmlText>::from_xml_text};
quote! {
#ty_from_xml_text(&residual.text())?
}
}
};
Ok(FieldParsePart {
tempinit: quote! {
let #tempname: #ty = #decode;
},
value: quote! { #tempname },
..FieldParsePart::default()
})
}
fn build_into_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
_member: &Member,
ty: &Type,
access: Expr,
) -> Result<TokenStream> {
let encode = match self.codec {
Some(ref codec_ty) => {
let codec_ty_encode = quote_spanned! {codec_ty.span()=> <#codec_ty as ::xso::TextCodec::<#ty>>::encode};
quote! {
#codec_ty_encode(#access).unwrap_or_else(String::new)
}
}
None => {
let ty_into_xml_text =
quote_spanned! {ty.span()=> <#ty as ::xso::IntoXmlText>::into_xml_text};
quote! {
#ty_into_xml_text(#access)
}
}
};
Ok(quote! {
{
let __text = #encode;
if __text.len() > 0 {
builder.append(::xso::exports::minidom::Node::Text(__text))
} else {
builder
}
}
})
}
fn build_set_namespace(
&self,
_input: &Ident,
_ty: &Type,
_access: Expr,
) -> Result<TokenStream> {
Ok(TokenStream::default())
}
}

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

@ -0,0 +1,194 @@
/*!
# 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), a [`ItemDef`] object is
created which implements that item. The actual implementations reside in
[`crate::structs`] and [`crate::enums`] (respectively).
1. 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.
2. 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`].
3. If any wrapping was declared, the resulting `ItemDef` is wrapped using
[`crate::wrapped`].
3. 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 error_message;
mod field;
mod meta;
mod structs;
mod wrapped;
use proc_macro::TokenStream as RawTokenStream;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::*;
use self::common::bake_generics;
use self::common::ItemDef;
/// Parse any implemented [`syn::Item`] into a [`ItemDef`] object.
fn parse(
item: Item,
) -> Result<(
Box<dyn ItemDef>,
Ident,
TokenStream,
TokenStream,
Option<WhereClause>,
)> {
match item {
Item::Struct(item) => {
let def = self::structs::parse_struct(&item)?;
let ident = item.ident;
let (generics_decl, generics_ref, where_clause) = bake_generics(item.generics);
Ok((def, ident, generics_decl, generics_ref, where_clause))
}
Item::Enum(item) => {
let def = self::enums::parse_enum(&item)?;
let ident = item.ident;
let (generics_decl, generics_ref, where_clause) = bake_generics(item.generics);
Ok((def, ident, generics_decl, generics_ref, where_clause))
}
other => Err(Error::new_spanned(
other,
"can only be applied to enum and struct definitions",
)),
}
}
/// Build the FromXml implementation for a given [`syn::Item`].
fn try_from_element_impl(item: Item) -> Result<TokenStream> {
let (def, ident, generics_decl, generics_ref, where_clause) = parse(item)?;
let try_from_impl = def.build_try_from_element(
&(Path::from(ident.clone()).into()),
&Ident::new("residual", Span::call_site()),
)?;
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
}
}
})
}
/// Derive macro for `FromXml`.
#[proc_macro_derive(FromXml, attributes(xml))]
pub fn try_from_element(input: RawTokenStream) -> RawTokenStream {
let item = syn::parse_macro_input!(input as Item);
let result = try_from_element_impl(item);
match result {
Ok(v) => v.into(),
Err(e) => e.into_compile_error().into(),
}
}
/// Build the DynNamespace implementation for a given [`syn::Item`].
fn dyn_namespace_impl(item: Item) -> Result<TokenStream> {
let (def, ident, generics_decl, generics_ref, where_clause) = parse(item)?;
let dyn_namespace_impl = def.build_dyn_namespace()?;
Ok(quote! {
#[allow(non_snake_case)]
impl #generics_decl ::xso::DynNamespace for #ident #generics_ref #where_clause {
#dyn_namespace_impl
}
})
}
/// Derive macro for `DynNamespace`.
#[proc_macro_derive(DynNamespace, attributes(xml))]
pub fn dyn_namespace(input: RawTokenStream) -> RawTokenStream {
let item = syn::parse_macro_input!(input as Item);
let result = dyn_namespace_impl(item);
match result {
Ok(v) => v.into(),
Err(e) => e.into_compile_error().into(),
}
}
/// Build the IntoXml implementation for a given [`syn::Item`].
fn into_element_impl(item: Item) -> Result<TokenStream> {
let (def, ident, generics_decl, generics_ref, where_clause) = parse(item)?;
let into_element_impl = def.build_into_element(
&(Path::from(ident.clone()).into()),
&Ident::new("other", Span::call_site()),
)?;
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 {
#into_element_impl
}
}
impl #generics_decl ::xso::IntoXml for #ident #generics_ref {
fn into_tree(self) -> Option<::xso::exports::minidom::Element> {
Some(::minidom::Element::from(self))
}
}
})
}
/// Derive macro for `IntoXml`.
#[proc_macro_derive(IntoXml, attributes(xml))]
pub fn into_element(input: RawTokenStream) -> RawTokenStream {
let item = syn::parse_macro_input!(input as Item);
let result = into_element_impl(item);
match result {
Ok(v) => v.into(),
Err(e) => e.into_compile_error().into(),
}
}

1194
xso-proc/src/meta.rs Normal file

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,835 @@
/*!
# 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, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, *};
use crate::common::{build_prepare, build_validate, ItemDef};
use crate::compound::{Compound, DynCompound};
use crate::error_message::ParentRef;
use crate::meta::{
Flag, Name, NameRef, NamespaceRef, NodeFilterMeta, StaticNamespace, XmlCompoundMeta,
};
/// A XML namespace as declared on a struct.
#[derive(Debug)]
pub(crate) enum StructNamespace {
/// The namespace is a static string.
Static(
/// The namespace as [`Path`] pointing at the static string.
StaticNamespace,
),
/// Instead of a fixed namespace, the namespace is dynamic. The allowed
/// values are determined by a
/// [`NamespaceField`][`crate::field::namespace::NamespaceField`]
/// (declared using `#[xml(namespace)]`).
Dyn {
/// The `dyn` token from the `#[xml(namespace = dyn)]` meta.
#[allow(dead_code)]
dyn_tok: Token![dyn],
/// The type of the namespace field.
ty: Type,
/// The member of the namespace field.
member: Member,
},
}
/// Represent a selector for element-transparent structs.
///
/// See also [`StructInner::Element`].
#[derive(Debug)]
pub(crate) enum ElementSelector {
/// Any element will be accepted.
///
/// Corresponds to `#[xml(element)]`.
Any,
/// The element will be matched by XML name only.
///
/// Corresponds to `#[xml(element(name = ..))]`.
ByName(Name),
/// The element will be matched by XML namespace only.
///
/// Corresponds to `#[xml(element(namespace = ..))]`.
ByNamespace(StaticNamespace),
/// The element will be matched by XML namespace and name..
///
/// Corresponds to `#[xml(element(namespace = .., name = ..))]`.
Qualified {
/// The XML namespace to match.
namespace: StaticNamespace,
/// The XML name to match.
name: Name,
},
}
impl TryFrom<NodeFilterMeta> for ElementSelector {
type Error = Error;
fn try_from(other: NodeFilterMeta) -> Result<Self> {
let namespace = match other.namespace {
None => None,
Some(NamespaceRef::Static(ns)) => Some(ns),
Some(NamespaceRef::Dyn(ns)) => return Err(Error::new_spanned(
ns,
"namespace = dyn cannot be used with element-transparent structs or enum variants."
)),
Some(NamespaceRef::Super(ns)) => return Err(Error::new_spanned(
ns,
"namespace = super cannot be used with element-transparent structs or enum variants."
)),
};
let name = other.name.map(|x| Name::from(x));
match (namespace, name) {
(Some(namespace), Some(name)) => Ok(Self::Qualified { namespace, name }),
(Some(namespace), None) => Ok(Self::ByNamespace(namespace)),
(None, Some(name)) => Ok(Self::ByName(name)),
(None, None) => Ok(Self::Any),
}
}
}
impl ElementSelector {
/// Construct a token stream evaluating to bool.
///
/// If the `minidom::Element` in `residual` matches the selector, the
/// token stream will evaluate to true. Otherwise, it will evaluate to
/// false.
pub(crate) fn build_test(&self, residual: &Ident) -> TokenStream {
match self {
Self::Any => quote! { true },
Self::ByName(name) => quote! {
#residual.name() == #name
},
Self::ByNamespace(ns) => quote! {
#residual.ns() == #ns
},
Self::Qualified { namespace, name } => quote! {
#residual.is(#name, #namespace)
},
}
}
}
/// The inner parts of the struct.
///
/// This contains all data necessary for the matching logic, but does not
/// include validation/preparation of the data. The latter is handled by
/// [`StructDef`].
#[derive(Debug)]
pub(crate) enum StructInner {
/// Single-field tuple-like struct declared with `#[xml(transparent)]`.
///
/// Transparent struct delegate all parsing and serialising to their
/// single field, which is why they do not need to store a lot of
/// information and come with extra restrictions, such as:
///
/// - no XML namespace can be declared (it is determined by inner type)
/// - no XML name can be declared (it is determined by inner type)
/// - the fields must be unnamed
/// - there must be only exactly one field
/// - that field has no `#[xml]` attribute
Transparent {
/// Type of the only unnamed field.
ty: Type,
},
/// Single-field tuple-like struct declared with `#[xml(element)]`.
///
/// Element-transparent structs take the incoming XML element as-is, and
/// re-serialise it as-is.
Element {
/// Determines the set of acceptable XML elements. Elements which do
/// not match the selector will not be parsed.
selector: ElementSelector,
},
/// A compound of fields, *not* declared as transparent.
///
/// This can be a unit, tuple-like, or named struct.
Compound {
/// The XML namespace to match the struct against.
namespace: StructNamespace,
/// The XML name to match the struct against.
name: Name,
/// The contents of the struct.
inner: Compound,
},
}
impl StructInner {
/// Process the `meta` and `fields` into a [`StructInner`].
///
/// The `meta` must be "blank" except for the `transparent`, `namespace`
/// and `name` fields. If any other field has a non-`None` / non-`Absent`
/// value, this function panics!
pub(crate) fn new(meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
// These must be taken out by the caller.
assert!(!meta.exhaustive.is_set());
assert!(meta.validate.is_none());
assert!(meta.prepare.is_none());
assert!(meta.normalize_with.is_none());
assert!(!meta.debug.is_set());
assert!(!meta.fallback.is_set());
assert!(meta.attribute.is_none());
assert!(meta.value.is_none());
if let Some(element) = meta.element {
if let Flag::Present(transparent) = meta.transparent {
return Err(Error::new(
transparent,
"transparent option conflicts with element option. pick one or the other.",
));
}
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"namespace option not allowed on element-transparent structs or enum variants",
));
}
if let Some(name) = meta.name {
return Err(Error::new_spanned(
name,
"name option not allowed on element-transparent structs or enum variants",
));
}
Self::new_element(element, fields)
} else if let Flag::Present(_) = meta.transparent {
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"namespace option not allowed on transparent structs or enum variants",
));
}
if let Some(name) = meta.name {
return Err(Error::new_spanned(
name,
"name option not allowed on transparent structs or enum variants",
));
}
Self::new_transparent(fields)
} else {
let Some(namespace) = meta.namespace else {
return Err(Error::new(
meta.span,
"`namespace` option is required on non-transparent structs or enum variants",
));
};
let Some(name) = meta.name else {
return Err(Error::new(
meta.span,
"`name` option is required on non-transparent structs or enum variants",
));
};
Self::new_compound(
namespace,
name,
meta.on_unknown_child,
meta.on_unknown_attribute,
fields,
)
}
}
/// Construct a new transparent struct with the given fields.
///
/// This function ensures that only a single, unnamed field is inside the
/// struct and causes a compile-time error otherwise.
fn new_transparent(fields: &Fields) -> Result<Self> {
let field = match fields {
Fields::Unit => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
))
}
Fields::Named(_) => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must be tuple-like",
))
}
Fields::Unnamed(fields) => {
if fields.unnamed.len() == 0 {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
));
} else if fields.unnamed.len() > 1 {
return Err(Error::new_spanned(
&fields.unnamed[1],
"transparent structs or enum variants must have exactly one field",
));
}
&fields.unnamed[0]
}
};
for attr in field.attrs.iter() {
if attr.path().is_ident("xml") {
return Err(Error::new_spanned(
attr.path(),
"the field inside a #[xml(transparent)] struct or enum variant cannot have an #[xml(..)] attribute."
));
}
}
Ok(Self::Transparent {
ty: field.ty.clone(),
})
}
/// Construct a new element-transparent struct with the given fields.
///
/// This function ensures that only a single, unnamed field is inside the
/// struct and causes a compile-time error otherwise.
fn new_element(node_filter: NodeFilterMeta, fields: &Fields) -> Result<Self> {
let field = match fields {
Fields::Unit => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
))
}
Fields::Named(_) => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must be tuple-like",
))
}
Fields::Unnamed(fields) => {
if fields.unnamed.len() == 0 {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
));
} else if fields.unnamed.len() > 1 {
return Err(Error::new_spanned(
&fields.unnamed[1],
"transparent structs or enum variants must have exactly one field",
));
}
&fields.unnamed[0]
}
};
for attr in field.attrs.iter() {
if attr.path().is_ident("xml") {
return Err(Error::new_spanned(
attr.path(),
"the field inside a #[xml(transparent)] struct or enum variant cannot have an #[xml(..)] attribute."
));
}
}
Ok(Self::Element {
selector: node_filter.try_into()?,
})
}
/// Construct a new compound-based struct with the given namespace, name
/// and fields.
fn new_compound(
namespace: NamespaceRef,
name: NameRef,
on_unknown_child: Option<Ident>,
on_unknown_attribute: Option<Ident>,
fields: &Fields,
) -> Result<Self> {
let inner = Compound::from_fields(on_unknown_child, on_unknown_attribute, fields)?;
let namespace_field = inner.namespace_field();
let namespace = match namespace {
NamespaceRef::Static(namespace) => {
if let Some((span, ..)) = namespace_field {
return Err(Error::new(
span,
"struct or enum variant must be declared with #[xml(namespace = dyn, ..)] to use a #[xml(namespace)] field."
));
}
StructNamespace::Static(namespace)
}
NamespaceRef::Dyn(namespace) => {
if let Some((_, ty, member)) = namespace_field {
StructNamespace::Dyn {
dyn_tok: namespace,
ty: ty.clone(),
member: member.clone(),
}
} else {
return Err(Error::new_spanned(
namespace,
"enum variant or struct declared with #[xml(namespace = dyn)] must have a field annotated with #[xml(namespace)]"
));
}
}
NamespaceRef::Super(ns) => {
return Err(Error::new_spanned(
ns,
"#[xml(namespace = super)] not allowed on enum variant or struct.",
));
}
};
Ok(Self::Compound {
namespace,
name: name.into(),
inner,
})
}
/// Construct an expression which consumes `residual` and evaluates to
/// `Result<T, Element>`.
///
/// - `struct_name` may contain either the path necessary to construct an
/// instance of the struct or a nested parent ref. In the latter case,
/// the struct is constructed as tuple instead of a struct.
///
/// - `residual` must be the identifier of the `minidom::Element` to
/// process.
///
/// If the element does not match the selectors of this struct, it is
/// returned in the `Err` variant for further probing.
pub(crate) fn build_try_from_element(
&self,
struct_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
match self {
Self::Transparent { ty } => {
let cons = match struct_name {
ParentRef::Named(path) => quote! { #path },
ParentRef::Unnamed { .. } | ParentRef::Wrapper { .. } => quote! {},
};
let ty_from_tree = quote_spanned! {ty.span()=> <#ty as ::xso::FromXml>::from_tree};
Ok(quote! {
match #ty_from_tree(#residual) {
Ok(v) => Ok(#cons (v)),
Err(::xso::error::Error::TypeMismatch(_, _, #residual)) => Err(#residual),
Err(other) => return Err(other),
}
})
}
Self::Element { selector } => {
let test = selector.build_test(residual);
let cons = match struct_name {
ParentRef::Named(path) => quote! { #path },
ParentRef::Unnamed { .. } | ParentRef::Wrapper { .. } => quote! {},
};
Ok(quote! {
if #test {
Ok(#cons ( #residual ))
} else {
Err(#residual)
}
})
}
Self::Compound {
namespace,
name: xml_name,
inner,
} => {
let namespace_tempname = Ident::new("__struct_namespace", Span::call_site());
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: namespace_tempname.clone().into(),
});
let body =
inner.build_try_from_element(struct_name, &namespace_expr, residual, &[])?;
match namespace {
StructNamespace::Dyn { ty, .. } => {
let ty_from_xml_text = quote_spanned! {ty.span()=> <#ty as ::xso::DynNamespaceEnum>::from_xml_text};
Ok(quote! {
match #ty_from_xml_text(&#residual.ns()) {
Ok(#namespace_tempname) => if #residual.name() == #xml_name {
Ok(#body)
} else {
Err(#residual)
}
Err(::xso::error::DynNamespaceError::Invalid) => {
return Err(::xso::error::Error::ParseError(
"Invalid namespace"
));
}
Err(::xso::error::DynNamespaceError::Mismatch) => Err(#residual),
}
})
}
StructNamespace::Static(xml_namespace) => Ok(quote! {
if #residual.is(#xml_name, #xml_namespace) {
let #namespace_tempname = #xml_namespace;
Ok(#body)
} else {
Err(#residual)
}
}),
}
}
}
}
/// Construct an expression which takes the fields as accessed through
/// `access_field` and converts them into a `minidom::Element`.
///
/// - `struct_name` is used primarily for diagnostic messages.s
///
/// - `access_field` must be a function which transforms a [`syn::Member`]
/// referring to a member of the struct to an expression under which the
/// member can be accessed.
pub(crate) fn build_into_element(
&self,
struct_name: &ParentRef,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
match self {
Self::Transparent { ty } => {
let ident = access_field(Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
}));
let ty_into_tree = quote_spanned! {ty.span()=> <#ty as ::xso::IntoXml>::into_tree};
Ok(quote! {
#ty_into_tree(#ident).expect("inner element did not produce any data")
})
}
Self::Element { .. } => {
let ident = access_field(Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
}));
Ok(quote! {
#ident
})
}
Self::Compound {
namespace,
name: xml_name,
inner,
} => {
let builder = Ident::new("builder", Span::call_site());
let (builder_init, namespace_expr) = match namespace {
StructNamespace::Dyn { ref member, ty, .. } => {
let expr = access_field(member.clone());
let ty_into_xml_text = quote_spanned! {ty.span()=> <#ty as ::xso::DynNamespaceEnum>::into_xml_text};
(
quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
#ty_into_xml_text(#expr.clone()),
)
},
expr,
)
}
StructNamespace::Static(xml_namespace) => {
let expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: xml_namespace.clone(),
});
(
quote! {
::xso::exports::minidom::Element::builder(
#xml_name,
#xml_namespace,
)
},
expr,
)
}
};
let body = inner.build_into_element(
struct_name,
&namespace_expr,
&builder,
&mut access_field,
)?;
Ok(quote! {
{
let mut #builder = #builder_init;
let #builder = #body;
#builder.build()
}
})
}
}
}
/// Return an iterator which returns the [`syn::Member`] structs to access
/// the struct's fields in declaration order.
pub(crate) fn iter_members(&self) -> Box<dyn Iterator<Item = Member> + '_> {
match self {
Self::Transparent { .. } | Self::Element { .. } => Box::new(
[Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
})]
.into_iter(),
),
Self::Compound { inner, .. } => inner.iter_members(),
}
}
pub(crate) fn as_dyn(&self) -> Option<DynStructInner<'_>> {
match self {
Self::Transparent { .. } | Self::Element { .. } => None,
Self::Compound { ref inner, .. } => inner.as_dyn().map(|x| DynStructInner { inner: x }),
}
}
}
/// Reference to a [`StructInner`] which has proven that the struct is using
/// namespace = dyn.
///
/// This simplifies some checks here and there.
pub(crate) struct DynStructInner<'x> {
/// The compound with `namespace = dyn` asserted.
inner: DynCompound<'x>,
}
impl<'x> DynStructInner<'x> {
/// Return a reference to the [`Type`] of the field annotated with
/// `#[xml(namespace)]`.
pub(crate) fn namespace_ty(&self) -> &'x Type {
self.inner.namespace_ty()
}
/// Build the implementation of
/// `DynNamespace::namespace(&self) -> &Self::Namespace`.
pub(crate) fn build_get_namespace(
&self,
access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
self.inner.build_get_namespace(access_field)
}
/// Build the implementation of
/// `DynNamespace::set_namespace<T: Into<Self::Namespace>>(&mut self, ns: T)`.
pub(crate) fn build_set_namespace(
&self,
input: &Ident,
access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
self.inner.build_set_namespace(input, access_field)
}
}
/// Create an accessor function for struct fields.
///
/// `struct_path` must be the path under which the struct is accessible.
fn make_accessor(struct_path: Path) -> impl FnMut(Member) -> Expr {
move |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: struct_path.clone(),
})),
member,
})
}
}
/// Represent a struct.
#[derive(Debug)]
pub(crate) struct StructDef {
/// The `validate` value, if set on the struct.
///
/// This is called after the struct has been otherwise parsed successfully
/// with the struct value as mutable reference as only argument. It is
/// expected to return `Result<(), Error>`, the `Err(..)` variant of which
/// is forwarded correctly.
validate: Option<Path>,
/// The `prepare` value, if set on the struct.
///
/// This is called before the struct will be converted back into an XML
/// element with the struct value as mutable reference as only argument.
prepare: Option<Path>,
/// Structure of the struct.
inner: StructInner,
/// The `debug` flag if set on the struct.
#[cfg_attr(not(feature = "debug"), allow(dead_code))]
debug: Flag,
}
impl StructDef {
/// 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(
fallback,
"`fallback` is not allowed on structs",
));
}
if let Flag::Present(exhaustive) = meta.exhaustive.take() {
return Err(syn::Error::new(
exhaustive,
"`exhaustive` is not allowed on structs",
));
}
if let Some(attribute) = meta.attribute.take() {
return Err(syn::Error::new_spanned(
attribute,
"`attribute` is not allowed on structs",
));
}
if let Some(value) = meta.value.take() {
return Err(syn::Error::new_spanned(
value,
"`value` is not allowed on structs",
));
}
if let Some(normalize_with) = meta.normalize_with.take() {
return Err(syn::Error::new_spanned(
normalize_with,
"`normalize_with` is not allowed on structs",
));
}
let validate = meta.validate.take();
let prepare = meta.prepare.take();
let debug = meta.debug.take();
Ok(Self {
validate,
prepare,
debug,
inner: StructInner::new(meta, fields)?,
})
}
}
impl ItemDef for StructDef {
fn build_try_from_element(
&self,
struct_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
let validate = build_validate(self.validate.as_ref());
let try_from_impl = self.inner.build_try_from_element(struct_name, residual)?;
let result = quote! {
{
let mut result = match #try_from_impl {
Ok(v) => v,
Err(residual) => return Err(Self::Error::TypeMismatch("", "", residual)),
};
#validate;
Ok(result)
}
};
#[cfg(feature = "debug")]
if self.debug.is_set() {
println!("{}", result);
}
Ok(result)
}
fn build_into_element(
&self,
struct_name: &ParentRef,
value_ident: &Ident,
) -> Result<TokenStream> {
let prepare = build_prepare(self.prepare.as_ref(), value_ident);
let access_field = make_accessor(Path {
leading_colon: None,
segments: [PathSegment::from(value_ident.clone())]
.into_iter()
.collect(),
});
let into_impl = self.inner.build_into_element(struct_name, access_field)?;
let result = quote! {
{
#prepare
#into_impl
}
};
#[cfg(feature = "debug")]
if self.debug.is_set() {
println!("{}", result);
}
Ok(result)
}
fn build_dyn_namespace(&self) -> Result<TokenStream> {
let dyn_inner = match self.inner.as_dyn() {
Some(v) => v,
None => return Err(Error::new(
Span::call_site(),
"struct must have `namespace = dyn` and a `#[xml(namespace)]` field to derive DynNamespace"
)),
};
let set_namespace_input = Ident::new("ns", Span::call_site());
let mut accessor = make_accessor(Ident::new("self", Span::call_site()).into());
let ty = dyn_inner.namespace_ty();
let namespace_impl = dyn_inner.build_get_namespace(&mut accessor)?;
let set_namespace_impl =
dyn_inner.build_set_namespace(&set_namespace_input, &mut accessor)?;
Ok(quote! {
type Namespace = #ty;
fn namespace(&self) -> &Self::Namespace {
#namespace_impl
}
fn set_namespace<T: Into<Self::Namespace>>(&mut self, #set_namespace_input: T) {
let #set_namespace_input = #set_namespace_input.into();
#set_namespace_impl
}
})
}
}
pub(crate) fn parse_struct(item: &syn::ItemStruct) -> Result<Box<dyn ItemDef>> {
let mut meta = XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let wrapped_with = meta.wrapped_with.take();
let span = meta.span;
let mut def = Box::new(StructDef::new(meta, &item.fields)?) as Box<dyn ItemDef>;
if let Some(wrapped_with) = wrapped_with {
def = crate::wrapped::wrap(&span, wrapped_with, &item.ident, def)?;
}
Ok(def)
}

261
xso-proc/src/wrapped.rs Normal file
View File

@ -0,0 +1,261 @@
/*!
# Wrapping of any item into a single-child struct
This module provides a wrapper around any [`ItemDef`] which wraps the contents
into an XML element with a given namespace and name. No other children or
attributes are allowed on that element.
This implements `#[xml(.., wrapped_with(..))]`.
*/
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::*;
use crate::common::ItemDef;
use crate::compound::Compound;
use crate::error_message::ParentRef;
use crate::field::{Field, FieldDef, FieldParsePart};
use crate::meta::{NamespaceRef, NodeFilterMeta};
use crate::structs::{StructInner, StructNamespace};
/// The [`Field`] implementation to handle the actual type.
#[derive(Debug)]
struct WrappedField {
/// The name of the type to parse.
ty_ident: Ident,
/// The implementation of the type to parse.
inner: Box<dyn ItemDef>,
}
impl Field for WrappedField {
fn build_try_from_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
tempname: Ident,
_member: &Member,
_ty: &Type,
) -> Result<FieldParsePart> {
let ty = &self.ty_ident;
let enum_ref = ParentRef::from(Path::from(self.ty_ident.clone()));
let enum_ref_s = enum_ref.to_string();
let missingerr = quote! {
concat!("Required child missing in ", #enum_ref_s, ".")
};
let duperr = quote! {
concat!("Only one child allowed in ", #enum_ref_s, ".")
};
let try_from_impl = self.inner.build_try_from_element(
&(Path::from(self.ty_ident.clone()).into()),
&Ident::new("residual", Span::call_site()),
)?;
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Option<#ty> = None;
},
childiter: quote! {
residual = match #try_from_impl {
Ok(v) => {
if #tempname.is_some() {
return Err(::xso::error::Error::ParseError(#duperr));
}
#tempname = Some(v);
continue;
}
Err(::xso::error::Error::TypeMismatch(_, _, residual)) => residual,
Err(other) => return Err(other),
};
},
value: quote! {
if let Some(v) = #tempname {
v
} else {
return Err(Error::ParseError(#missingerr))
}
},
..FieldParsePart::default()
})
}
fn build_into_element(
&self,
_container_name: &ParentRef,
_container_namespace_expr: &Expr,
_member: &Member,
_ty: &Type,
access: Expr,
) -> Result<TokenStream> {
let tempname = Ident::new("data", Span::call_site());
let into_impl = self
.inner
.build_into_element(&(Path::from(self.ty_ident.clone()).into()), &tempname)?;
Ok(quote! {
{
let mut #tempname = #access;
let el = #into_impl;
builder.append(::xso::exports::minidom::Node::Element(el))
}
})
}
fn build_set_namespace(
&self,
_input: &Ident,
_ty: &Type,
_access: Expr,
) -> Result<TokenStream> {
// we don't allow deriving DynNamesace.
unreachable!()
}
}
/// The wrapper item.
#[derive(Debug)]
pub(crate) struct Wrapped {
/// The actual work is done (similar to how extracts are handled) by a
/// virtual struct.
///
/// This struct is tuple-style and has a single field, implemented using
/// [`WrappedField`]. That field does the deserialisation of the child
/// element, while the `StructInner` here does the matching on the parent
/// element and ensures that no stray data is inside of that.
inner: StructInner,
}
impl ItemDef for Wrapped {
fn build_try_from_element(
&self,
item_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
let try_from_impl = self
.inner
.build_try_from_element(&item_name.wrapper(), residual)?;
Ok(quote! {
match #try_from_impl {
Ok(v) => Ok(v.0),
Err(residual) => Err(::xso::error::Error::TypeMismatch("", "", residual)),
}
})
}
fn build_into_element(
&self,
item_name: &ParentRef,
value_ident: &Ident,
) -> Result<TokenStream> {
let into_impl = self
.inner
.build_into_element(&item_name.wrapper(), |member| {
Expr::Field(ExprField {
attrs: Vec::new(),
base: Box::new(Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: value_ident.clone().into(),
})),
dot_token: token::Dot {
spans: [Span::call_site()],
},
member,
})
})?;
let result = quote! {
let #value_ident = (#value_ident,);
#into_impl
};
Ok(result)
}
fn build_dyn_namespace(&self) -> Result<TokenStream> {
return Err(Error::new(
Span::call_site(),
"namespace = dyn cannot be combined with wrapped(..)",
));
}
}
/// Wrap any struct or enum into another XML element.
///
/// - `span` is used for error purposes and should point somewhere in the
/// vicinity of the wrapping specification.
///
/// - `meta` must contain the actual wrapping specification (namespace and
/// name of the outer field).
///
/// - `ty_ident` must be the identifier of the *wrapped* type.
///
/// - `inner` must be the implementation of the wrapped type.
pub(crate) fn wrap(
span: &Span,
meta: NodeFilterMeta,
ty_ident: &Ident,
inner: Box<dyn ItemDef>,
) -> Result<Box<Wrapped>> {
let namespace = match meta.namespace {
Some(NamespaceRef::Static(ns)) => ns,
Some(NamespaceRef::Dyn(ns)) => {
return Err(Error::new_spanned(
ns,
"dyn namespace is not supported for wrappers",
))
}
Some(NamespaceRef::Super(ns)) => {
return Err(Error::new_spanned(
ns,
"super namespace is not supported for wrappers",
))
}
None => {
return Err(Error::new(
*span,
"namespace is required for wrappers (inside `#[xml(.., wrapped_with(..))]`)",
))
}
};
let name = match meta.name {
Some(name) => name.into(),
None => {
return Err(Error::new(
*span,
"name is required for wrappers (inside `#[xml(.., wrapped_with(..))]`)",
))
}
};
let field = WrappedField {
ty_ident: ty_ident.clone(),
inner,
};
let inner = Compound::new(
None,
None,
[Ok(FieldDef {
span: *span,
ident: Member::Unnamed(Index {
index: 0,
span: *span,
}),
// the type is only passed down to the Field impl, where
// we will not use it.
ty: Type::Never(TypeNever {
bang_token: token::Not { spans: [*span] },
}),
kind: Box::new(field),
})]
.into_iter(),
)?;
let inner = StructInner::Compound {
namespace: StructNamespace::Static(namespace),
name,
inner,
};
Ok(Box::new(Wrapped { inner }))
}

29
xso/Cargo.toml Normal file
View File

@ -0,0 +1,29 @@
[package]
name = "xso"
version = "0.1.0"
edition = "2021"
description = "XML Streamed Objects: similar to serde, but XML-native."
homepage = "https://xmpp.rs"
repository = "https://gitlab.com/xmpp-rs/xmpp-rs"
keywords = ["xmpp", "xml", "serialization"]
categories = ["encoding"]
license = "MPL-2.0"
# 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"]

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

@ -0,0 +1,148 @@
// 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)
}
}
/// Represent the errors which can occur when matching a dynamic namespace.
pub enum DynNamespaceError {
/// The namespace doesn't match any of the possibilities.
Mismatch,
/// The namespace matches, but is somehow invalid.
///
/// The element should be rejected instead of attempting to parse it with
/// other FromXml implementations.
Invalid,
}

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

@ -0,0 +1,827 @@
#![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.
XSO is an acronym for XML Stream(ed) Objects, referring to the main field of
use of this library in parsing XML streams like specified in RFC 6120.
[^serde-note]: Though it should be said that you can combine serde and this
crate on the same struct, no problem with that!
*/
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`: allows dynamic namespace matching using the [`DynNamespaceEnum`]
trait. A struct with this namespace type needs to have exactly one field
with the `#[xml(namespace)]` annotation.
The type of that field must implement [`DynNamespaceEnum`] and will be
used to match the namespace of the XML elements.
- 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 rejected.
`validate` is allowed and will be called.
- `element`, `element(..): Only allowed on tuple-like structs with exactly one
field. That field must be of type [`minidom::Element`].
Supports the following optional inner attributes:
- `namespace`: If given, restricts the namespace of the elements to parse.
Has no influence on XML generation: If the inner element has a different
namespace when the struct is serialized, that namespace will be used.
- `name`: If given, restricts the XML name of the elements to parse.
Has no influence on XML generation: If the inner element has a different
XML name when the struct is serialized, that namespace will be used.
`validate` is allowed and will be called.
- `on_unknown_child = ..` may be set to the identifier of a member of the
[`UnknownChildPolicy`] enum (i.e. for example `on_unknown_child = Ignore`).
This configures the behavior when unknown child elements are encountered.
See [`UnknownChildPolicy`] for details.
Has no effect on grandchildren.
- `on_unknown_attribute = ..` may be set to the identifier of a member of the
[`UnknownAttributePolicy`] enum (i.e. for example
`on_unknown_attribute = Ignore`). This configures the behavior when unknown
attributes are encountered. See [`UnknownAttributePolicy`] for details.
Has no effect on children.
- `wrapped_with(namespace = .., name = ..)`: If set, the struct will be wrapped
into an XML element with the given namespace and name. That means that
instead of `<inner/>`, on the wire, `<outer><inner/></outer>` (with the
corresponding XML names and namespaces) is expected and generated.
Other than the struct itself, the wrapping element must not have any
attributes or child elements, and it only supports static namespaces.
## Enums
Enums come in multiple flavors. All flavors have the following attributes:
- `validate = ..`: See struct attributes.
- `prepare = ..`: See struct attributes.
- `wrapped_with = ..`: See struct attributes.
The following flavors exist:
- fully dynamic: The variants must be each either transparent or specify
their `namespace` and `name`.
- XML name matched: The enum itself defines a namespace. Each variant must
then specify the name. The variant is picked based on the name of the XML
element.
- XML attribute matched: The enum itself defines a namespace, a name and an
attribute name. Each variant must specify the attribute value. The variant
is picked based on the value of the given attribute, provided that XML name
and namespace of the element itself match.
The flavor is determined based on the attributes of the enum declaration.
### Dynamic enums
No additional attributes are available on dynamic enumerations.
#### Dynamic enum variants
Dynamic enum variants work exactly like structs, except that the `prepare`
and `validate` attributes are not available.
### XML name matched enums
XML name matched enums support the following attributes:
- `namespace = ..` (required): This must be a path to a `&'static str`. It is
the namespace of the enumeration.
- `exhaustive` (flag): If present, the enum considers itself authoritative for
that namespace. If it encounters an element within the namespace which does
not match any variant, a fatal parsing error is returned.
This cannot be used if a variant is set as `fallback`.
This attribute has no relation to the Rust standard `#[non_exhaustive]`
attribute.
#### XML name matched enum variants
XML name matched enum variants support the following attributes:
- `name = ..` (required): String literal with the XML name to match against.
- `fallback` (flag): If present, the variant is parsed when no other variant
matches.
*Note:* When the enum is reserialized to XML, the XML name will be the one
declared in the `name` attribute of the variant; the original XML name is
lost.
### XML attribute matched enums
XML attribute matched enums support the following attributes:
- `namespace = ..` (required): This must be a path to a `&'static str`. It is
the namespace of the enumeration.
- `name = ..` (required): This must be a string literal containing the XML
name to match against.
- `attribute = ..` (required): This must be a string literal with the name of
the XML attribute to match against.
- `exhaustive` (flag): Must currently be set unless a variant is marked as
`fallback`. Support for non-exhaustive attribute-matched enums is not
implemented yet.
This cannot be used if a variant is set as `fallback`.
This attribute has no relation to the Rust standard `#[non_exhaustive]`
attribute.
- `normalize_with = ..`: Optional path to a thing which can be called with a
`&str` and which returns a [`std::borrow::Cow`]. If present, the attribute
value will be passed through that callable before it will be matched against
the enum variants.
#### XML attribute matched enum variants
XML attribute matched enum variants support the following attributes:
- `value = ..` (required): String literal with the attribute value to match
against.
- `fallback` (flag): If present, the variant is parsed when no other variant
matches.
*Note:* When the enum is reserialized to XML, the attribute value will be
the one declared in the `value` attribute of the variant; the original value
is lost.
## 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 `FromOptionalXmlText` (for [`FromXml`]) or
`IntoOptionalXmlText` (for [`IntoXml`]), unless the `codec` option is
set.
- `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`, `default = ..`: If set, a field value is generated if the
attribute is not present and [`FromOptionalXmlText`] did not create a
value from [`None`], instead of failing to parse. If the optional
argument is present, it must be the path to a callable which returns the
field's type. Otherwise, [`std::default::Default::default`] is used.
- `codec = ..`: Path to a type implementing [`TextCodec`] to use instead
of the [`FromOptionalXmlText`] / [`IntoOptionalXmlText`] implementation
of the field's type.
If set, you need to explicitly add the `default` flag to fields of type
`Option<_>`, because the default option logic of [`FromOptionalXmlText`]
is not present.
- `child(.., extract(..))`: Extract data from a child element.
- `name = ..` (required): The XML name of the child to match.
- `namespace = ..` (required): The XML namespace of the child to match. This
can be one of the following:
- A path referring to a `&'static str` `static` which contains the
namespace URI of the XML element represented by the struct.
- `super`: Only usable inside compounds with `#[xml(namespace = dyn)]`,
using `#[xml(namespace = super)]` on an extracted field allows to match
the field's child's namespace with the dynamically determined namespace
of the parent (both during serialisation and during deserialisation).
- `extract(..)` (required): Specification of data to extract. See below
for options.
- `skip_if`: If set, this must be the path to a callable. That callable is
invoked with a reference to the field's type at serialisation time. If
the callable returns true, the field is omitted from the output
completely.
This should often be combined with `default`.
- `default`, `default = ..`: If set, a field value is generated if the
child is not present instead of failing to parse. If the optional argument
is present, it must be the path to a callable which returns the field's
type. Otherwise, [`std::default::Default::default`] is used.
*Note:* When using `extract(..)`, this is required even when the field's
type is `Option<..>`.
- `child`, `child(..)` (without `extract(..)`): Extract an entire child
element. The field type must implement [`FromXml`] (for [`FromXml`])
or `IntoXml` (for [`IntoXml`]).
- `namespace = super`: If set, the field must also implement
[`DynNamespace`] and the compound the field is in must be set to be
`namespace = dyn`. In this case, the field's child is forced to be in the
same namespace as the parent during parsing.
- `skip_if`: If set, this must be the path to a callable. That callable is
invoked with a reference to the field's type at serialisation time. If
the callable returns true, the field is omitted from the output
completely.
This should often be combined with `default`.
- `default`, `default = ..`: If set, a field value is generated if the
child is not present instead of failing to parse. If the optional argument
is present, it must be the path to a callable which returns the field's
type. Otherwise, [`std::default::Default::default`] is used.
*Note:* When using `extract(..)`, this is required even when the field's
type is `Option<..>`.
Aside from `namespace = super`, matching of the XML namespace / name is
completely delegated to the [`FromXml`] implementation of the field's type
and thus the `namespace` and `name` attributes are not allowed.
- `children(.., extract(..))`: Like `child(.., extract(..))`, with the following
differences:
- More than one clause inside `extract(..)` are allowed.
- More than one matching child is allowed
- The field type must implement [`Default`][`std::default::Default`],
[`Extend<T>`][`std::iter::Extend`] and
[`IntoIterator<Item = T>`][`std::iter::IntoIterator`].
`T`, must be a tuple type matching the types provided by the
extracted parts.
- Extracts must specify their type, because it cannot be inferred through
the collection.
- `children`, `children(..)` (without `extract(..)`): Extract zero or more
entire child elements. The field type must implement
[`Default`][`std::default::Default`],
[`Extend<T>`][`std::iter::Extend`] and
[`IntoIterator<Item = T>`][`std::iter::IntoIterator`], where `T`
implements [`FromXml`] (and [`IntoXml`] for [`IntoXml`]).
- `skip_if`: If set, this must be the path to a callable. That callable is
invoked with a reference to the field's type at serialisation time. If
the callable returns true, the field is omitted from the output
completely.
This should often be combined with `default`.
The namespace and name to match are determined by the field type, thus it
is not allowed to specify them here. `namespace = super` is not supported.
- `text`, `text(..): Extract the element's text contents. The field type must
implement `FromXmlText` (for [`FromXml`]) or `IntoXmlText`
(for [`IntoXml`]), unless the `codec` option is set.
- `codec = ..`: Path to a type implementing [`TextCodec`] to use instead
of the [`FromXmlText`] / [`IntoXmlText`] implementation of the field's
type.
- `element`, `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 = ..` (optional): The XML name of the element to match.
- `namespace = ..` (optional): The XML namespace of the element to match.
- `default`, `default = ..`: If set, a field value is generated if the
child is not present instead of failing to parse. If the optional argument
is present, it must be the path to a callable which returns the field's
type. Otherwise, [`std::default::Default::default`] is used.
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>`.
- `namespace`: Represent the parent struct/enum variant's XML namespace. This
requires that the compound is declared with `#[xml(namespace = dyn)]`. The
field type must implement [`DynNamespaceEnum`].
- `ignore`: The field is not considered during parsing or serialisation. The
type must implement [`Default`].
### Extraction specification
Inside `extract(..)`, there must be a list of type annotations as used on
fields. All annotations can be used which can also be used on fields. Because
here there is no possibility to do any inferrence on the field type, the
following field attributes support an additional, optional `type` argument:
- `attribute`
- `text`
If the `extract(..)` contains exactly one part and the type of the extract
is not specified on that one part, it is assumed to be equal to the type of
the field the extract is used on.
Otherwise, the default is `String`, which is not going to work in many cases.
This limitation could be lifted, but we need a use case for it first :). So
if you run into this file an issue please!
*/
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;
/**
# Make a struct fully dynamically namespaceable
For all supported attributes, please see [`FromXml`].
*/
pub use xso_proc::DynNamespace;
#[doc(hidden)]
pub mod exports {
pub use minidom;
}
use jid::{BareJid, FullJid, Jid};
macro_rules! from_text_via_parse {
($cons:path, $($t:ty,)+) => {
$(
impl FromXmlText for $t {
fn from_xml_text(s: &str) -> Result<Self, error::Error> {
s.parse().map_err($cons)
}
}
impl IntoXmlText for $t {
fn into_xml_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,
}
/// Convert XML text to a value.
pub trait FromXmlText: Sized {
/// Construct a value from XML text.
///
/// This is similar to [`std::str::FromStr`], but the error type is fixed.
fn from_xml_text(s: &str) -> Result<Self, error::Error>;
}
impl FromXmlText for String {
/// Copy the string from the source into the result.
fn from_xml_text(s: &str) -> Result<Self, error::Error> {
Ok(s.to_string())
}
}
/// Convert a value into XML text.
pub trait IntoXmlText {
/// Consume the value and return it as XML string.
fn into_xml_text(self) -> String;
}
impl IntoXmlText for String {
fn into_xml_text(self) -> String {
self
}
}
/// Provide construction of a value from optional XML text data.
///
/// Most likely, you don't need to implement this; implement [`FromXmlText`]
/// instead (which automatically provides a [`FromOptionalXmlText`]
/// 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 FromOptionalXmlText: 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_optional_xml_text(s: Option<&str>) -> Result<Option<Self>, error::Error>;
}
impl<T: FromXmlText> FromOptionalXmlText for T {
fn from_optional_xml_text(s: Option<&str>) -> Result<Option<Self>, error::Error> {
s.map(T::from_xml_text).transpose()
}
}
impl<T: FromOptionalXmlText> FromOptionalXmlText 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_optional_xml_text(s: Option<&str>) -> Result<Option<Self>, error::Error> {
match s {
None => Ok(Some(None)),
Some(v) => Ok(Some(T::from_optional_xml_text(Some(v))?)),
}
}
}
/// Provide destruction of a value into optional XML text data.
///
/// Most likely, you don't need to implement this; implement [`IntoXmlText`]
/// instead (which automatically provides a [`IntoOptionalXmlText`]
/// 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 IntoOptionalXmlText {
/// Destruct the value into an optional string.
///
/// Returning `None` causes the resulting XML object (likely an attribute,
/// but maybe also an extracted child) to not appear in the output.
fn into_optional_xml_text(self) -> Option<String>;
}
impl<T: IntoXmlText> IntoOptionalXmlText for T {
fn into_optional_xml_text(self) -> Option<String> {
Some(self.into_xml_text())
}
}
impl<T: IntoOptionalXmlText> IntoOptionalXmlText for Option<T> {
fn into_optional_xml_text(self) -> Option<String> {
match self {
None => None,
Some(v) => v.into_optional_xml_text(),
}
}
}
/// Discard the value and do not emit a value.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Discard;
impl FromXmlText for Discard {
fn from_xml_text(_: &str) -> Result<Self, error::Error> {
Ok(Self)
}
}
impl IntoOptionalXmlText for Discard {
fn into_optional_xml_text(self) -> Option<String> {
None
}
}
/// 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()
}
}
/// Enumeration of possible dynamically determined namespaces.
///
/// This trait must be implemented on types used on `#[xml(namespace)]`
/// fields.
///
/// It allows to specify XML elements which can be parsed in different
/// namespaces. This can be useful in weird protocols (looking at you, XMPP)
/// where the same element occurs in multiple different namespaces. The
/// namespace is then kept in the field and is thus available at serialisation
/// time.
///
/// This trait depends on `PartialEq<str>`; this is used to compare a value
/// against the value of an `xmlns` attribute without requiring to go through
/// (potentially costly and also error-inducing) [`Self::from_xml_text`].
pub trait DynNamespaceEnum: Sized + PartialEq<str> {
/// Parse the namespace from the `xmlns` attribute data.
///
/// This should return [`Mismatch`][`crate::error::DynNamespaceError`] for
/// namespaces which are not part of the enumeration of matching
/// namespaces. In such cases, the parser may attempt to parse using
/// another [`FromXml`] implementation.
///
/// You can also return [`Invalid`][`crate::error::DynNamespaceError`] to
/// abort parsing altogether; this is probably not what you want, though.
fn from_xml_text(s: &str) -> Result<Self, self::error::DynNamespaceError>;
/// Convert the namespace into the text value of an `xmlns` attribute.
fn into_xml_text(self) -> String;
}
/// Trait for structs implementing [`FromXml`] with `namespace = dyn`.
///
/// This allows to access the namespace of these structs without needing
/// knowledge about their internal structure.
pub trait DynNamespace {
/// The namespace enum for this struct.
type Namespace: DynNamespaceEnum;
/// Return a reference to the struct's namespace.
fn namespace(&self) -> &Self::Namespace;
/// Recursively set this struct's namespace.
///
/// This will set the namespace for the struct itself as well as for
/// fields referring to child structs with `namespace = super`.
fn set_namespace<T: Into<Self::Namespace>>(&mut self, ns: T);
}
/// Configure how unknown child elements are handled.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UnknownChildPolicy {
/// Fail parsing with a fatal error.
#[cfg_attr(not(feature = "disable-validation"), default)]
Fail,
/// Ignore and discard any unknown child elements.
#[cfg_attr(feature = "disable-validation", default)]
Ignore,
}
impl UnknownChildPolicy {
// TODO: once we have a more suitable error message structure, we should
// not pass the entire message down to this function.
/// Return a result describing the result of triggering the child policy.
///
/// In other words, this returns an error iff the policy is set to
/// `Fail`.
///
/// **Note:** This function is not to be considered part of the public
/// API! It is only marked `pub` because it needs to be called from
/// macro-generated code! Its signature and behavior may change without
/// notice and without major version bump at any time.
pub fn trigger(&self, msg: &'static str) -> Result<(), error::Error> {
match self {
Self::Fail => Err(error::Error::ParseError(msg)),
Self::Ignore => Ok(()),
}
}
}
/// Configure how unknown attributes are handled.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UnknownAttributePolicy {
/// Fail parsing with a fatal error.
#[cfg_attr(not(feature = "disable-validation"), default)]
Fail,
/// Ignore and discard any unknown attributes.
#[cfg_attr(feature = "disable-validation", default)]
Ignore,
}
impl UnknownAttributePolicy {
// TODO: once we have a more suitable error message structure, we should
// not pass the entire message down to this function.
/// Return a result describing the result of triggering the attribute
/// policy.
///
/// In other words, this returns an error iff the policy is set to
/// `Fail`.
///
/// **Note:** This function is not to be considered part of the public
/// API! It is only marked `pub` because it needs to be called from
/// macro-generated code! Its signature and behavior may change without
/// notice and without major version bump at any time.
pub fn trigger(&self, msg: &'static str) -> Result<(), error::Error> {
match self {
Self::Fail => Err(error::Error::ParseError(msg)),
Self::Ignore => Ok(()),
}
}
}
/// Trait to support destructuring of child structs beyond what the
/// `extract(..)` attribute can deliver.
///
/// This trait can only be sensibly implemented on types which implement both
/// `FromXml` and `IntoXml`. However, as there may be corner cases where only
/// one of these other traits is needed, they're not strictly included in the
/// trait bounds.
///
/// When used as value for `codec = ..` inside a `#[xml(child(..))]` or
/// `#[xml(children(..))]` field attribute, the field is destructured using
/// the trait implementations of `FromXml` / `IntoXml` and then converted
/// to the actual field's type by invoking the `ElementCodec<T>` methods, with
/// `T` being the field type.
pub trait ElementCodec<T> {
/// Transform the destructured value further toward the field type.
fn decode(value: Self) -> T;
/// Transform the field type back to something which can be structured
/// into XML.
fn encode(value: T) -> Self;
}
impl<T> ElementCodec<T> for T {
fn decode(value: Self) -> Self {
value
}
fn encode(value: Self) -> Self {
value
}
}
/// Trait for fallible extension.
///
/// You probably won't need to implement this---it is automatically implemented
/// for all containers which implement [`std::iter::Extend`].
///
/// The only useful case are containers where an extension may fail because of
/// data reasons, e.g. if you collect elements of multiple types but it can
/// only contain a single type at a time.
pub trait TryExtend<A> {
/// Attempt extending the container with the given iterator.
///
/// Upon error, the container may be partially extended.
fn try_extend<T: IntoIterator<Item = A>>(&mut self, iter: T) -> Result<(), self::error::Error>;
}
impl<A, T: Extend<A>> TryExtend<A> for T {
fn try_extend<I: IntoIterator<Item = A>>(&mut self, iter: I) -> Result<(), self::error::Error> {
self.extend(iter);
Ok(())
}
}

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

@ -0,0 +1,284 @@
use std::borrow::Cow;
use std::fmt::{self, Write};
use std::marker::PhantomData;
use std::ops::Deref;
use base64::engine::{general_purpose::STANDARD as StandardBase64Engine, Engine as Base64Engine};
use crate::{error::Error, FromXmlText, IntoXmlText};
/// Represent a way to encode/decode text data into a Rust type.
///
/// This trait is used to specify how a given type is serialised into and from
/// a string. This bypasses the [`FromXmlText`] / [`IntoXmlText`] traits,
/// allowing to hide the specific implementation for transforming data from
/// text into Rust types (and back) from a container's public interface.
pub trait TextCodec<T> {
/// Decode a string value into the type.
fn decode(s: &str) -> Result<T, Error>;
/// Encode the type as string value.
///
/// If this returns `None`, the string value is not emitted at all.
fn encode(value: T) -> Option<String>;
}
/// Text codec which does no transform.
pub struct Plain;
impl TextCodec<String> for Plain {
fn decode(s: &str) -> Result<String, Error> {
Ok(s.to_string())
}
fn encode(value: String) -> Option<String> {
Some(value)
}
}
/// Text codec which returns None instead of the empty string.
pub struct EmptyAsNone;
impl TextCodec<Option<String>> for EmptyAsNone {
fn decode(s: &str) -> Result<Option<String>, Error> {
if s.len() == 0 {
Ok(None)
} else {
Ok(Some(s.to_owned()))
}
}
fn encode(value: Option<String>) -> Option<String> {
match value {
Some(v) if v.len() > 0 => Some(v),
Some(_) | None => None,
}
}
}
/// Text codec which trims whitespace at the beginning and the end of the
/// text.
///
/// Optionally processes the text further using a nested codec.
pub struct Trimmed<Inner = Plain>(PhantomData<Inner>);
impl<Inner: TextCodec<String>> TextCodec<String> for Trimmed<Inner> {
fn decode(s: &str) -> Result<String, Error> {
Inner::decode(s.trim())
}
fn encode(decoded: String) -> Option<String> {
Inner::encode(decoded)
}
}
/// Trait for preprocessing text data from XML.
///
/// This may be used by codecs to allow to customize some of their behaviour.
pub trait TextFilter {
/// Process the incoming string and return the result of the processing.
///
/// This should avoid copying wherever possible.
fn preprocess(s: &str) -> Cow<'_, str>;
}
/// Text preprocessor which returns the input unchanged.
pub struct NoFilter;
impl TextFilter for NoFilter {
fn preprocess(s: &str) -> Cow<'_, str> {
Cow::Borrowed(s)
}
}
/// Text preprocessor to remove all whitespace.
pub struct StripWhitespace;
impl TextFilter for StripWhitespace {
fn preprocess(s: &str) -> Cow<'_, str> {
let s: String = s
.chars()
.filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
.collect();
Cow::Owned(s)
}
}
/// Text codec transforming text to binary using standard base64.
///
/// The `Filter` type argument can be used to employ additional preprocessing
/// of incoming text data. Most interestingly, passing [`StripWhitespace`]
/// will make the implementation ignore any whitespace within the text.
pub struct Base64<Filter: TextFilter = NoFilter>(PhantomData<Filter>);
impl<Filter: TextFilter> TextCodec<Vec<u8>> for Base64<Filter> {
fn decode(s: &str) -> Result<Vec<u8>, Error> {
let value = Filter::preprocess(s);
Ok(StandardBase64Engine.decode(value.as_ref())?)
}
fn encode(value: Vec<u8>) -> Option<String> {
Some(StandardBase64Engine.encode(&value))
}
}
impl<Filter: TextFilter> TextCodec<Option<Vec<u8>>> for Base64<Filter> {
fn decode(s: &str) -> Result<Option<Vec<u8>>, Error> {
if s.len() == 0 {
return Ok(None);
}
Ok(Some(Self::decode(s)?))
}
fn encode(decoded: Option<Vec<u8>>) -> Option<String> {
decoded.and_then(Self::encode)
}
}
/// Text codec transforming text to binary by emitting it as colon-separated
/// hex value.
pub struct ColonSeparatedHex;
impl TextCodec<Vec<u8>> for ColonSeparatedHex {
fn decode(s: &str) -> Result<Vec<u8>, Error> {
let mut bytes = vec![];
for i in 0..(1 + s.len()) / 3 {
let byte = u8::from_str_radix(&s[3 * i..3 * i + 2], 16)?;
if 3 * i + 2 < s.len() {
assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
}
bytes.push(byte);
}
Ok(bytes)
}
fn encode(decoded: Vec<u8>) -> Option<String> {
let mut bytes = vec![];
for byte in decoded {
bytes.push(format!("{:02X}", byte));
}
Some(bytes.join(":"))
}
}
/// Codec for bytes of lowercase hexadecimal, with a fixed length `N` (in bytes).
pub struct Hex;
impl<const N: usize> TextCodec<[u8; N]> for Hex {
fn decode(s: &str) -> Result<[u8; N], Error> {
if s.len() != 2 * N {
return Err(Error::ParseError("Invalid length"));
}
let mut bytes = [0u8; N];
for i in 0..N {
bytes[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16)?;
}
Ok(bytes)
}
fn encode(decoded: [u8; N]) -> Option<String> {
let mut bytes = String::with_capacity(N * 2);
for byte in decoded {
write!(&mut bytes, "{:02x}", byte).unwrap();
}
Some(bytes)
}
}
impl<const N: usize> TextCodec<Option<[u8; N]>> for Hex {
fn decode(s: &str) -> Result<Option<[u8; N]>, Error> {
if s.len() == 0 {
return Ok(None);
}
Ok(Some(Self::decode(s)?))
}
fn encode(decoded: Option<[u8; N]>) -> Option<String> {
decoded.and_then(Self::encode)
}
}
impl TextCodec<Vec<u8>> for Hex {
fn decode(s: &str) -> Result<Vec<u8>, Error> {
if s.len() % 2 != 0 {
return Err(Error::ParseError("Invalid length"));
}
let n = s.len() / 2;
let mut bytes = Vec::with_capacity(n);
bytes.resize(n, 0u8);
for i in 0..n {
bytes[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16)?;
}
Ok(bytes)
}
fn encode(decoded: Vec<u8>) -> Option<String> {
let mut bytes = String::with_capacity(decoded.len() * 2);
for byte in decoded {
write!(&mut bytes, "{:02x}", byte).unwrap();
}
Some(bytes)
}
}
/// 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 FromXmlText for NonEmptyString {
fn from_xml_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 IntoXmlText for NonEmptyString {
fn into_xml_text(self) -> String {
self.0
}
}
/// Text codec which rejects empty strings.
impl TextCodec<String> for NonEmptyString {
fn decode(s: &str) -> Result<String, Error> {
if s.len() == 0 {
return Err(Error::ParseError("string must not be empty"));
}
Ok(s.to_owned())
}
fn encode(value: String) -> Option<String> {
Some(value)
}
}