xso_proc: implement generic wrapping mechanism

That way, any `#[xml(..)]` declared struct or enum can be declared to be
wrapped into another XML element on the wire. This is especially useful
in PubSub, which for some reason wraps its stuff in useless `<pubsub/>`
elements.
This commit is contained in:
Jonas Schäfer 2024-04-03 17:55:58 +02:00
parent 06732ca721
commit a60542af91
10 changed files with 682 additions and 307 deletions

View File

@ -1177,8 +1177,8 @@ fn element_codec_roundtrip() {
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(namespace = self::TEST_NS1, name = "child-switched", child = self::TEST_NS2)]
pub enum ChildSwitchedEnum {
#[xml(namespace = self::TEST_NS2, wrapped_with(namespace = self::TEST_NS1, name = "child-switched"))]
pub enum WrappedEnum {
#[xml(name = "variant-1")]
Variant1 {
#[xml(text)]
@ -1192,15 +1192,59 @@ pub enum ChildSwitchedEnum {
}
#[test]
fn child_switched_enum_roundtrip_1() {
crate::util::test::roundtrip_full::<ChildSwitchedEnum>(
fn wrapped_enum_roundtrip_1() {
crate::util::test::roundtrip_full::<WrappedEnum>(
"<child-switched xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'><variant-1 xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f'>some data</variant-1></child-switched>",
);
}
#[test]
fn child_switched_enum_roundtrip_2() {
crate::util::test::roundtrip_full::<ChildSwitchedEnum>(
fn wrapped_enum_enum_roundtrip_2() {
crate::util::test::roundtrip_full::<WrappedEnum>(
"<child-switched xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'><variant-2 xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f' data='other data'/></child-switched>",
);
}
#[test]
fn wrapped_enum_does_not_leak_inner_type_mismatch_1() {
match crate::util::test::parse_str::<WrappedEnum>(
"<child-switched xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'><foo/></child-switched>",
) {
Err(Error::ParseError(msg)) if msg.find("Unknown child").is_some() => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn wrapped_enum_does_not_leak_inner_type_mismatch_2() {
match crate::util::test::parse_str::<WrappedEnum>(
"<child-switched xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'><foo xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f'/></child-switched>",
) {
Err(Error::ParseError(msg)) if msg.find("Unknown child").is_some() => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(namespace = self::TEST_NS2, name = "inner", wrapped_with(namespace = self::TEST_NS1, name = "outer"))]
pub struct WrappedStruct {
#[xml(text)]
pub data: String,
}
#[test]
fn wrapped_struct_roundtrip() {
crate::util::test::roundtrip_full::<WrappedStruct>(
"<outer xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'><inner xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f'>some data</inner></outer>",
);
}
#[test]
fn wrapped_struct_does_not_leak_inner_type_mismatch() {
match crate::util::test::parse_str::<WrappedEnum>(
"<child-switched xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'><foo/></child-switched>",
) {
Err(Error::ParseError(msg)) if msg.find("Unknown child").is_some() => (),
other => panic!("unexpected result: {:?}", other),
}
}

View File

@ -6,6 +6,8 @@ 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.
///
@ -57,7 +59,7 @@ pub(crate) fn bake_generics(generics: Generics) -> (TokenStream, TokenStream, Op
/// 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 {
pub(crate) fn build_validate(validate: Option<&Path>) -> Stmt {
syn::parse2(if let Some(validate) = validate {
quote! {
#validate(&mut result)?;
@ -73,7 +75,7 @@ pub(crate) fn build_validate(validate: Option<Path>) -> Stmt {
/// 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 {
pub(crate) fn build_prepare(prepare: Option<&Path>, value_ident: &Ident) -> TokenStream {
if let Some(prepare) = prepare {
quote! {
#prepare(&mut #value_ident);
@ -84,3 +86,59 @@ pub(crate) fn build_prepare(prepare: Option<Path>, value_ident: &Ident) -> Token
}
}
}
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()
}
}

View File

@ -261,7 +261,7 @@ impl Compound {
#init
}
},
ParentRef::Unnamed { .. } => quote! {
ParentRef::Unnamed { .. } | ParentRef::Wrapper { .. } => quote! {
( #tupinit )
},
};

View File

@ -12,8 +12,9 @@ use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::*;
use crate::common::{bake_generics, build_prepare, build_validate};
use crate::common::{build_prepare, build_validate, ItemDef};
use crate::compound::Compound;
use crate::error_message::ParentRef;
use crate::meta::{Flag, Name, NamespaceRef, StaticNamespace, XmlCompoundMeta};
use crate::structs::StructInner;
@ -323,12 +324,12 @@ impl XmlNameSwitched {
/// acceptable.
/// - `residual` must be the identifier at which the element is found.
fn build_try_from_element(
self,
&self,
enum_ident: &Ident,
validate: Stmt,
residual: &Ident,
) -> Result<TokenStream> {
let xml_namespace = self.namespace;
let xml_namespace = &self.namespace;
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
@ -336,13 +337,13 @@ impl XmlNameSwitched {
});
let mut fallback = quote! {
_ => return Err(::xso::error::Error::TypeMismatch("", "", #residual)),
_ => Err(::xso::error::Error::TypeMismatch("", "", #residual)),
};
let mut iter = quote! {};
for variant in self.variants {
let ident = variant.ident;
let xml_name = variant.value;
for variant in self.variants.iter() {
let ident = &variant.ident;
let xml_name = &variant.value;
let variant_impl = variant.inner.build_try_from_element(
&(Path {
@ -403,21 +404,21 @@ impl XmlNameSwitched {
/// for each variant.
///
/// `ty_ident` must be the identifier of the enum's type.
fn build_into_element(self, ty_ident: &Ident) -> Result<TokenStream> {
fn build_into_element(&self, ty_ident: &Ident) -> Result<TokenStream> {
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: self.namespace,
path: self.namespace.clone(),
});
let builder = Ident::new("builder", Span::call_site());
let mut matchers = quote! {};
for variant in self.variants {
let xml_name = variant.value;
for variant in self.variants.iter() {
let xml_name = &variant.value;
let path = Path {
leading_colon: None,
segments: [
PathSegment::from(ty_ident.clone()),
PathSegment::from(variant.ident),
PathSegment::from(variant.ident.clone()),
]
.into_iter()
.collect(),
@ -534,13 +535,13 @@ impl XmlAttributeSwitched {
/// acceptable.
/// - `residual` must be the identifier at which the element is found.
fn build_try_from_element(
self,
&self,
enum_ident: &Ident,
validate: Stmt,
residual: &Ident,
) -> Result<TokenStream> {
let xml_namespace = self.namespace;
let xml_name = self.name;
let xml_namespace = &self.namespace;
let xml_name = &self.name;
let attribute_name = self.attribute_name.value();
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
@ -551,9 +552,9 @@ impl XmlAttributeSwitched {
let mut fallback: Option<TokenStream> = None;
let mut iter = quote! {};
for variant in self.variants {
let ident = variant.ident;
let xml_name = variant.value;
for variant in self.variants.iter() {
let ident = &variant.ident;
let xml_name = &variant.value;
let variant_impl = variant.inner.build_try_from_element(
&(Path {
@ -599,7 +600,7 @@ impl XmlAttributeSwitched {
);
let normalize = match self.normalize_with {
Some(normalize_with) => quote! {
Some(ref normalize_with) => quote! {
let attr_value = attr_value.map(|value| #normalize_with(value));
let attr_value = attr_value.as_ref().map(|value| ::std::borrow::Borrow::<str>::borrow(value));
},
@ -628,10 +629,10 @@ impl XmlAttributeSwitched {
/// for each variant.
///
/// `ty_ident` must be the identifier of the enum's type.
fn build_into_element(self, ty_ident: &Ident) -> Result<TokenStream> {
let xml_namespace = self.namespace;
let xml_name = self.name;
let attribute_name = self.attribute_name;
fn build_into_element(&self, ty_ident: &Ident) -> Result<TokenStream> {
let xml_namespace = &self.namespace;
let xml_name = &self.name;
let attribute_name = &self.attribute_name;
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
@ -639,13 +640,13 @@ impl XmlAttributeSwitched {
});
let builder = Ident::new("builder", Span::call_site());
let mut matchers = quote! {};
for variant in self.variants {
let attribute_value = variant.value;
for variant in self.variants.iter() {
let attribute_value = &variant.value;
let path = Path {
leading_colon: None,
segments: [
PathSegment::from(ty_ident.clone()),
PathSegment::from(variant.ident),
PathSegment::from(variant.ident.clone()),
]
.into_iter()
.collect(),
@ -705,14 +706,14 @@ impl Dynamic {
/// acceptable.
/// - `residual` must be the identifier at which the element is found.
fn build_try_from_element(
self,
&self,
enum_ident: &Ident,
validate: Stmt,
residual: &Ident,
) -> Result<TokenStream> {
let mut matchers = quote! {};
for variant in self.variants {
let ident = variant.ident;
for variant in self.variants.iter() {
let ident = &variant.ident;
let try_from_impl = variant.inner.build_try_from_element(
&(Path {
leading_colon: None,
@ -752,10 +753,10 @@ impl Dynamic {
/// for each variant.
///
/// `ty_ident` must be the identifier of the enum's type.
fn build_into_element(self, enum_ident: &Ident) -> Result<TokenStream> {
fn build_into_element(&self, enum_ident: &Ident) -> Result<TokenStream> {
let mut matchers = quote! {};
for variant in self.variants {
let ident = variant.ident;
for variant in self.variants.iter() {
let ident = &variant.ident;
let path = Path {
leading_colon: None,
segments: [
@ -809,7 +810,7 @@ impl EnumInner {
/// acceptable.
/// - `residual` must be the identifier at which the element is found.
fn build_try_from_element(
self,
&self,
enum_ident: &Ident,
validate: Stmt,
residual: &Ident,
@ -830,7 +831,7 @@ impl EnumInner {
/// for each variant.
///
/// `ty_ident` must be the identifier of the enum's type.
fn build_into_element(self, ty_ident: &Ident) -> Result<TokenStream> {
fn build_into_element(&self, ty_ident: &Ident) -> Result<TokenStream> {
match self {
Self::XmlNameSwitched(inner) => inner.build_into_element(ty_ident),
Self::XmlAttributeSwitched(inner) => inner.build_into_element(ty_ident),
@ -841,7 +842,7 @@ impl EnumInner {
/// Represent an enum.
#[derive(Debug)]
struct EnumDef {
pub(crate) struct EnumDef {
/// The `validate` value, if set on the enum.
///
/// This is called after the enum has been otherwise parsed successfully
@ -989,15 +990,22 @@ impl EnumDef {
)?),
})
}
}
/// Construct the entire implementation of
/// `TryFrom<minidom::Element>::try_from`.
fn build_try_from_element(self, enum_ident: &Ident, residual: &Ident) -> Result<TokenStream> {
let validate = build_validate(self.validate);
impl ItemDef for EnumDef {
fn build_try_from_element(
&self,
item_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
let Some(ty_ident) = item_name.try_as_ident() else {
panic!("EnumDef::build_try_from_element cannot be called with non-ident ParentRef");
};
let validate = build_validate(self.validate.as_ref());
let result = self
.inner
.build_try_from_element(enum_ident, validate, residual)?;
.build_try_from_element(ty_ident, validate, residual)?;
#[cfg(feature = "debug")]
if self.debug.is_set() {
@ -1006,17 +1014,24 @@ impl EnumDef {
Ok(result)
}
/// Construct the entire implementation of
/// `<From<T> for minidom::Element>::from`.
fn build_into_element(self, ty_ident: &Ident, value_ident: &Ident) -> Result<TokenStream> {
let prepare = build_prepare(self.prepare, value_ident);
fn build_into_element(
&self,
item_name: &ParentRef,
value_ident: &Ident,
) -> Result<TokenStream> {
let Some(ty_ident) = item_name.try_as_ident() else {
panic!("EnumDef::build_try_from_element cannot be called with non-ident ParentRef");
};
let prepare = build_prepare(self.prepare.as_ref(), value_ident);
let matchers = self.inner.build_into_element(ty_ident)?;
let result = quote! {
#prepare
match #value_ident {
#matchers
{
#prepare
match #value_ident {
#matchers
}
}
};
@ -1026,76 +1041,21 @@ impl EnumDef {
}
Ok(result)
}
fn build_dyn_namespace(&self) -> Result<TokenStream> {
Err(Error::new(
Span::call_site(),
"DynNamespace cannot be derived on enums (yet)",
))
}
}
/// `FromXml` derive macro implementation for enumerations.
pub(crate) fn try_from_element(item: syn::ItemEnum) -> Result<proc_macro2::TokenStream> {
let meta = XmlCompoundMeta::try_parse_from_attributes(&item.attrs)?;
let ident = item.ident;
let def = EnumDef::new(meta, item.variants.iter())?;
let try_from_impl =
def.build_try_from_element(&ident, &Ident::new("residual", Span::call_site()))?;
let (generics_decl, generics_ref, where_clause) = bake_generics(item.generics);
Ok(quote! {
#[allow(non_snake_case)]
impl #generics_decl ::std::convert::TryFrom<::xso::exports::minidom::Element> for #ident #generics_ref #where_clause {
type Error = ::xso::error::Error;
fn try_from(mut residual: ::xso::exports::minidom::Element) -> Result<Self, Self::Error> {
#try_from_impl
}
}
impl #generics_decl ::xso::FromXml for #ident #generics_ref #where_clause {
fn from_tree(elem: ::xso::exports::minidom::Element) -> Result<Self, ::xso::error::Error> {
Self::try_from(elem)
}
fn absent() -> Option<Self> {
None
}
}
})
}
/// `DynNamespace` derive macro implementation for enumerations.
pub(crate) fn dyn_namespace(item: syn::ItemEnum) -> Result<proc_macro2::TokenStream> {
Err(Error::new_spanned(
item,
"DynNamespace cannot be derived on enums (yet)",
))
}
/// `IntoXml` derive macro implementation for enumerations.
pub(crate) fn into_element(item: syn::ItemEnum) -> Result<proc_macro2::TokenStream> {
let meta = XmlCompoundMeta::try_parse_from_attributes(&item.attrs)?;
let ident = item.ident;
let mut def = EnumDef::new(meta, item.variants.iter())?;
let prepare = if let Some(prepare) = def.prepare.take() {
quote! {
let _: () = #prepare(&mut other);
}
} else {
quote! {
{ let _ = &mut other; };
}
};
let into_impl = def.build_into_element(&ident, &Ident::new("other", Span::call_site()))?;
let (generics_decl, generics_ref, where_clause) = bake_generics(item.generics);
Ok(quote! {
#[allow(non_snake_case)]
impl #generics_decl ::std::convert::From<#ident #generics_ref> for ::xso::exports::minidom::Element #where_clause {
fn from(mut other: #ident #generics_ref) -> Self {
#prepare
#into_impl
}
}
impl #generics_decl ::xso::IntoXml for #ident #generics_ref {
fn into_tree(self) -> Option<::xso::exports::minidom::Element> {
Some(::minidom::Element::from(self))
}
}
})
pub(crate) fn parse_enum(item: &syn::ItemEnum) -> Result<Box<dyn ItemDef>> {
let mut meta = XmlCompoundMeta::try_parse_from_attributes(&item.attrs)?;
let wrapped_with = meta.as_mut().map(|x| (x.wrapped_with.take(), x.span));
let mut def = Box::new(EnumDef::new(meta, item.variants.iter())?) as Box<dyn ItemDef>;
if let Some((Some(wrapped_with), span)) = wrapped_with {
def = crate::wrapped::wrap(&span, wrapped_with, &item.ident, def)?;
}
Ok(def)
}

View File

@ -3,7 +3,7 @@ use std::fmt;
use proc_macro2::Span;
use syn::{spanned::Spanned, Member, Path};
use syn::{spanned::Spanned, Ident, Member, Path};
/// Reference to a compound's parent
///
@ -31,6 +31,15 @@ pub(super) enum ParentRef {
/// 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 {
@ -57,6 +66,15 @@ impl ParentRef {
}
}
/// 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
@ -66,6 +84,27 @@ impl ParentRef {
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,
}
}
}
@ -87,6 +126,9 @@ impl fmt::Display for ParentRef {
Self::Unnamed { parent, field } => {
write!(f, "extraction for {} in {}", FieldName(field), parent)
}
Self::Wrapper { inner } => {
write!(f, "wrapper element of {}", inner)
}
}
}
}

View File

@ -15,25 +15,29 @@ The processing is roughly grouped in the following stages:
Based on that, the decision is made whether a struct or an enum is being
derived on.
2. Depending on the item type (enum vs. struct), the further processing is
delegated to the function matching the invoked derive from
[`crate::structs`] or [`crate::enums`]. Nontheless, much of the remaining
processing is equivalent.
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).
3. The [`crate::meta::XmlCompoundMeta`] type is used to convert the
raw token streams from the `#[xml(..)]` attributes into structs/enums for
easier handling.
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.
That stage only does syntax checks, no (or just little) semantics. This
separation of concerns helps with simplifying the code both in `meta`
and the following modules.
4. Enum variants and structs are processed using
[`crate::compound::Compound`], their fields being converted from [`crate::meta::XmlFieldMeta`] to [`crate::field::FieldDef`]. For enums,
additional processing on the enum itself takes place in [`crate::enums`].
Likewise there's special handling for structs in [`crate::structs`].
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`].
5. After all data has been structured, action is taken depending on the
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)]
@ -45,71 +49,144 @@ mod error_message;
mod field;
mod meta;
mod structs;
mod wrapped;
use proc_macro::TokenStream;
use proc_macro::TokenStream as RawTokenStream;
use proc_macro2::{Span, TokenStream};
use syn::Item;
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 from_element(input: TokenStream) -> TokenStream {
pub fn try_from_element(input: RawTokenStream) -> RawTokenStream {
let item = syn::parse_macro_input!(input as Item);
let result = match item {
Item::Struct(item) => self::structs::try_from_element(item),
Item::Enum(item) => self::enums::try_from_element(item),
other => {
return syn::Error::new_spanned(
other,
"FromXml can only be applied to enum and struct definitions",
)
.into_compile_error()
.into()
}
};
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: TokenStream) -> TokenStream {
pub fn dyn_namespace(input: RawTokenStream) -> RawTokenStream {
let item = syn::parse_macro_input!(input as Item);
let result = match item {
Item::Struct(item) => self::structs::dyn_namespace(item),
Item::Enum(item) => self::enums::dyn_namespace(item),
other => {
return syn::Error::new_spanned(
other,
"DynNamespace can only be applied to enum and struct definitions",
)
.into_compile_error()
.into()
}
};
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: TokenStream) -> TokenStream {
pub fn into_element(input: RawTokenStream) -> RawTokenStream {
let item = syn::parse_macro_input!(input as Item);
let result = match item {
Item::Struct(item) => self::structs::into_element(item),
Item::Enum(item) => self::enums::into_element(item),
other => {
return syn::Error::new_spanned(
other,
"IntoXml can only be applied to enum and struct definitions",
)
.into_compile_error()
.into()
}
};
let result = into_element_impl(item);
match result {
Ok(v) => v.into(),
Err(e) => e.into_compile_error().into(),

View File

@ -500,6 +500,9 @@ pub(crate) struct XmlCompoundMeta {
/// The options set inside `element`, if any.
pub(crate) element: Option<NodeFilterMeta>,
/// The options set inside `wrapped_with(..)`, if any.
pub(crate) wrapped_with: Option<NodeFilterMeta>,
/// Member of the `UnknownChildPolicy` enum to use when handling unknown
/// children.
pub(crate) on_unknown_child: Option<Ident>,
@ -522,6 +525,7 @@ impl XmlCompoundMeta {
exhaustive: Flag::Absent,
validate: None,
prepare: None,
wrapped_with: None,
normalize_with: None,
debug: Flag::Absent,
element: None,
@ -547,6 +551,7 @@ impl XmlCompoundMeta {
let mut prepare: Option<Path> = None;
let mut normalize_with: Option<Path> = None;
let mut element: Option<NodeFilterMeta> = None;
let mut wrapped_with: Option<NodeFilterMeta> = None;
let mut on_unknown_attribute: Option<Ident> = None;
let mut on_unknown_child: Option<Ident> = None;
@ -566,7 +571,7 @@ impl XmlCompoundMeta {
ParseValue("normalize_with", &mut normalize_with),
ParseValue("normalise_with", &mut normalize_with),
ParseNodeFilter("element", &mut element),
ParseNodeFilter("wrapped", &mut wrapped),
ParseNodeFilter("wrapped_with", &mut wrapped_with),
ParseValue("on_unknown_attribute", &mut on_unknown_attribute),
ParseValue("on_unknown_child", &mut on_unknown_child),
)
@ -583,6 +588,7 @@ impl XmlCompoundMeta {
validate,
exhaustive,
prepare,
wrapped_with,
debug,
normalize_with,
element,

View File

@ -11,7 +11,7 @@ use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, *};
use crate::common::{bake_generics, build_prepare, build_validate};
use crate::common::{build_prepare, build_validate, ItemDef};
use crate::compound::{Compound, DynCompound};
use crate::error_message::ParentRef;
use crate::meta::{
@ -411,7 +411,7 @@ impl StructInner {
/// 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,
&self,
struct_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
@ -419,7 +419,7 @@ impl StructInner {
Self::Transparent { ty } => {
let cons = match struct_name {
ParentRef::Named(path) => quote! { #path },
ParentRef::Unnamed { .. } => quote! {},
ParentRef::Unnamed { .. } | ParentRef::Wrapper { .. } => quote! {},
};
let ty_from_tree = quote_spanned! {ty.span()=> <#ty as ::xso::FromXml>::from_tree};
Ok(quote! {
@ -434,7 +434,7 @@ impl StructInner {
let test = selector.build_test(residual);
let cons = match struct_name {
ParentRef::Named(path) => quote! { #path },
ParentRef::Unnamed { .. } => quote! {},
ParentRef::Unnamed { .. } | ParentRef::Wrapper { .. } => quote! {},
};
Ok(quote! {
if #test {
@ -500,7 +500,7 @@ impl StructInner {
/// referring to a member of the struct to an expression under which the
/// member can be accessed.
pub(crate) fn build_into_element(
self,
&self,
struct_name: &ParentRef,
mut access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
@ -531,8 +531,8 @@ impl StructInner {
} => {
let builder = Ident::new("builder", Span::call_site());
let (builder_init, namespace_expr) = match namespace {
StructNamespace::Dyn { member, ty, .. } => {
let expr = access_field(member);
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! {
@ -734,32 +734,27 @@ impl StructDef {
inner: StructInner::new(meta, fields)?,
})
}
}
/// Construct an expression which consumes `residual` and evaluates to
/// `Result<T, Error>`.
///
/// - `struct_name` may contain either the path necessary to construct an
/// instance of the struct or a nested parent ref. In the latter case,
/// the struct is constructed as tuple instead of a struct.
///
/// - `residual` must be the identifier of the `minidom::Element` to
/// process.
impl ItemDef for StructDef {
fn build_try_from_element(
self,
&self,
struct_name: &ParentRef,
residual: &Ident,
) -> Result<TokenStream> {
let validate = build_validate(self.validate);
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)
{
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() {
@ -768,30 +763,26 @@ impl StructDef {
Ok(result)
}
/// Construct an expression which consumes the `T` value at `value_ident`
/// and returns a `minidom::Element`.
///
/// - `struct_name` is used primarily for diagnostic messages.
///
/// - `value_ident` must be the identifier at which the entire struct can
/// be reached. It is used during preparation.
///
/// - `access_field` must be a function which transforms a [`syn::Member`]
/// referring to a member of the struct to an expression under which the
/// member can be accessed.
fn build_into_element(
self,
&self,
struct_name: &ParentRef,
value_ident: &Ident,
access_field: impl FnMut(Member) -> Expr,
) -> Result<TokenStream> {
let prepare = build_prepare(self.prepare, value_ident);
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
{
#prepare
#into_impl
}
};
#[cfg(feature = "debug")]
if self.debug.is_set() {
@ -800,12 +791,7 @@ impl StructDef {
Ok(result)
}
/// Construct a token stream containing the entire body of the
/// `impl DynNamespace` block.
///
/// Can only be used on `namespace = dyn` structs; any other variants will
/// cause an appropriate compile-time error.
fn build_dyn_namespace(self) -> Result<TokenStream> {
fn build_dyn_namespace(&self) -> Result<TokenStream> {
let dyn_inner = match self.inner.as_dyn() {
Some(v) => v,
None => return Err(Error::new(
@ -837,81 +823,13 @@ impl StructDef {
}
}
/// `FromXml` derive macro implementation for structs.
pub(crate) fn try_from_element(item: syn::ItemStruct) -> Result<proc_macro2::TokenStream> {
let meta = XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let ident = item.ident;
let def = StructDef::new(meta, &item.fields)?;
let try_from_impl = def.build_try_from_element(
&(Path::from(ident.clone()).into()),
&Ident::new("residual", Span::call_site()),
)?;
let (generics_decl, generics_ref, where_clause) = bake_generics(item.generics);
Ok(quote! {
#[allow(non_snake_case)]
impl #generics_decl ::std::convert::TryFrom<::xso::exports::minidom::Element> for #ident #generics_ref #where_clause {
type Error = ::xso::error::Error;
fn try_from(mut residual: ::xso::exports::minidom::Element) -> Result<Self, Self::Error> {
#try_from_impl
}
}
impl #generics_decl ::xso::FromXml for #ident #generics_ref #where_clause {
fn from_tree(elem: ::xso::exports::minidom::Element) -> Result<Self, ::xso::error::Error> {
Self::try_from(elem)
}
fn absent() -> Option<Self> {
None
}
}
})
}
/// `DynNamespace` derive macro implementation for structs.
pub(crate) fn dyn_namespace(item: syn::ItemStruct) -> Result<proc_macro2::TokenStream> {
let meta = XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let ident = item.ident;
let def = StructDef::new(meta, &item.fields)?;
let dyn_namespace_impl = def.build_dyn_namespace()?;
let (generics_decl, generics_ref, where_clause) = bake_generics(item.generics);
Ok(quote! {
#[allow(non_snake_case)]
impl #generics_decl ::xso::DynNamespace for #ident #generics_ref #where_clause {
#dyn_namespace_impl
}
})
}
/// `IntoXml` derive macro implementation for structs.
pub(crate) fn into_element(item: syn::ItemStruct) -> Result<proc_macro2::TokenStream> {
let meta = XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let ident = item.ident;
let def = StructDef::new(meta, &item.fields)?;
let into_impl = def.build_into_element(
&(Path::from(ident.clone()).into()),
&Ident::new("other", Span::call_site()),
make_accessor(Path {
leading_colon: None,
segments: [PathSegment::from(Ident::new("other", Span::call_site()))]
.into_iter()
.collect(),
}),
)?;
let (generics_decl, generics_ref, where_clause) = bake_generics(item.generics);
Ok(quote! {
#[allow(non_snake_case)]
impl #generics_decl ::std::convert::From<#ident #generics_ref> for ::xso::exports::minidom::Element #where_clause {
fn from(mut other: #ident #generics_ref) -> Self {
#into_impl
}
}
impl #generics_decl ::xso::IntoXml for #ident #generics_ref {
fn into_tree(self) -> Option<::xso::exports::minidom::Element> {
Some(::minidom::Element::from(self))
}
}
})
pub(crate) fn parse_struct(item: &syn::ItemStruct) -> Result<Box<dyn ItemDef>> {
let mut meta = XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let wrapped_with = meta.wrapped_with.take();
let span = meta.span;
let mut def = Box::new(StructDef::new(meta, &item.fields)?) as Box<dyn ItemDef>;
if let Some(wrapped_with) = wrapped_with {
def = crate::wrapped::wrap(&span, wrapped_with, &item.ident, def)?;
}
Ok(def)
}

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

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

View File

@ -127,12 +127,21 @@ preserved, if the container preserves sort order on insertion.
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: