mirror of https://gitlab.com/xmpp-rs/xmpp-rs.git
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:
commit
170cba322e
|
@ -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" }
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
@ -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::*;
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}))
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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})
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
}
|
|
@ -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 }))
|
||||
}
|
|
@ -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"]
|
|
@ -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,
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue