From a60542af9136cf09a659287f8bfc4012545efd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Wed, 3 Apr 2024 17:55:58 +0200 Subject: [PATCH] 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 `` elements. --- parsers/src/macro_tests/mod.rs | 56 ++++++- xso-proc/src/common.rs | 62 +++++++- xso-proc/src/compound.rs | 2 +- xso-proc/src/enums.rs | 194 ++++++++++-------------- xso-proc/src/error_message.rs | 44 +++++- xso-proc/src/lib.rs | 189 +++++++++++++++++------- xso-proc/src/meta.rs | 8 +- xso-proc/src/structs.rs | 164 ++++++--------------- xso-proc/src/wrapped.rs | 261 +++++++++++++++++++++++++++++++++ xso/src/lib.rs | 9 ++ 10 files changed, 682 insertions(+), 307 deletions(-) create mode 100644 xso-proc/src/wrapped.rs diff --git a/parsers/src/macro_tests/mod.rs b/parsers/src/macro_tests/mod.rs index 0cd454c..278e4ad 100644 --- a/parsers/src/macro_tests/mod.rs +++ b/parsers/src/macro_tests/mod.rs @@ -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::( +fn wrapped_enum_roundtrip_1() { + crate::util::test::roundtrip_full::( "some data", ); } #[test] -fn child_switched_enum_roundtrip_2() { - crate::util::test::roundtrip_full::( +fn wrapped_enum_enum_roundtrip_2() { + crate::util::test::roundtrip_full::( "", ); } + +#[test] +fn wrapped_enum_does_not_leak_inner_type_mismatch_1() { + match crate::util::test::parse_str::( + "", + ) { + 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::( + "", + ) { + 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::( + "some data", + ); +} + +#[test] +fn wrapped_struct_does_not_leak_inner_type_mismatch() { + match crate::util::test::parse_str::( + "", + ) { + Err(Error::ParseError(msg)) if msg.find("Unknown child").is_some() => (), + other => panic!("unexpected result: {:?}", other), + } +} diff --git a/xso-proc/src/common.rs b/xso-proc/src/common.rs index 82a62de..57a2890 100644 --- a/xso-proc/src/common.rs +++ b/xso-proc/src/common.rs @@ -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) -> 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) -> 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, 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, value_ident: &Ident) -> Token } } } + +pub trait ItemDef: std::fmt::Debug { + /// Construct an expression which consumes `residual` and evaluates to + /// `Result`. + /// + /// - `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; + + /// 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; + + /// 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; +} + +impl ItemDef for Box { + fn build_try_from_element( + &self, + item_name: &ParentRef, + residual: &Ident, + ) -> Result { + (**self).build_try_from_element(item_name, residual) + } + + fn build_into_element( + &self, + item_name: &ParentRef, + value_ident: &Ident, + ) -> Result { + (**self).build_into_element(item_name, value_ident) + } + + fn build_dyn_namespace(&self) -> Result { + (**self).build_dyn_namespace() + } +} diff --git a/xso-proc/src/compound.rs b/xso-proc/src/compound.rs index ca5d3eb..4791b3e 100644 --- a/xso-proc/src/compound.rs +++ b/xso-proc/src/compound.rs @@ -261,7 +261,7 @@ impl Compound { #init } }, - ParentRef::Unnamed { .. } => quote! { + ParentRef::Unnamed { .. } | ParentRef::Wrapper { .. } => quote! { ( #tupinit ) }, }; diff --git a/xso-proc/src/enums.rs b/xso-proc/src/enums.rs index d41b26a..20dc2e3 100644 --- a/xso-proc/src/enums.rs +++ b/xso-proc/src/enums.rs @@ -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 { - 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 { + fn build_into_element(&self, ty_ident: &Ident) -> Result { 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 { - 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 = 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::::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 { - 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 { + 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 { 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 { + fn build_into_element(&self, enum_ident: &Ident) -> Result { 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 { + fn build_into_element(&self, ty_ident: &Ident) -> Result { 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::try_from`. - fn build_try_from_element(self, enum_ident: &Ident, residual: &Ident) -> Result { - let validate = build_validate(self.validate); +impl ItemDef for EnumDef { + fn build_try_from_element( + &self, + item_name: &ParentRef, + residual: &Ident, + ) -> Result { + 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 - /// ` for minidom::Element>::from`. - fn build_into_element(self, ty_ident: &Ident, value_ident: &Ident) -> Result { - let prepare = build_prepare(self.prepare, value_ident); + fn build_into_element( + &self, + item_name: &ParentRef, + value_ident: &Ident, + ) -> Result { + 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 { + 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 { - 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 { - #try_from_impl - } - } - - impl #generics_decl ::xso::FromXml for #ident #generics_ref #where_clause { - fn from_tree(elem: ::xso::exports::minidom::Element) -> Result { - Self::try_from(elem) - } - - fn absent() -> Option { - None - } - } - }) -} - -/// `DynNamespace` derive macro implementation for enumerations. -pub(crate) fn dyn_namespace(item: syn::ItemEnum) -> Result { - 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 { - 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> { + 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; + if let Some((Some(wrapped_with), span)) = wrapped_with { + def = crate::wrapped::wrap(&span, wrapped_with, &item.ident, def)?; + } + Ok(def) } diff --git a/xso-proc/src/error_message.rs b/xso-proc/src/error_message.rs index b051fd1..103cdd5 100644 --- a/xso-proc/src/error_message.rs +++ b/xso-proc/src/error_message.rs @@ -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, + }, } impl From 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) + } } } } diff --git a/xso-proc/src/lib.rs b/xso-proc/src/lib.rs index 0ddc5aa..41b2bb3 100644 --- a/xso-proc/src/lib.rs +++ b/xso-proc/src/lib.rs @@ -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, + Ident, + TokenStream, + TokenStream, + Option, +)> { + 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 { + 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 { + #try_from_impl + } + } + + impl #generics_decl ::xso::FromXml for #ident #generics_ref #where_clause { + fn from_tree(elem: ::xso::exports::minidom::Element) -> Result { + Self::try_from(elem) + } + + fn absent() -> Option { + 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 { + 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 { + 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(), diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 433ae59..f47b04e 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -500,6 +500,9 @@ pub(crate) struct XmlCompoundMeta { /// The options set inside `element`, if any. pub(crate) element: Option, + /// The options set inside `wrapped_with(..)`, if any. + pub(crate) wrapped_with: Option, + /// Member of the `UnknownChildPolicy` enum to use when handling unknown /// children. pub(crate) on_unknown_child: Option, @@ -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 = None; let mut normalize_with: Option = None; let mut element: Option = None; + let mut wrapped_with: Option = None; let mut on_unknown_attribute: Option = None; let mut on_unknown_child: Option = 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, diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index 03fb44f..9dc577f 100644 --- a/xso-proc/src/structs.rs +++ b/xso-proc/src/structs.rs @@ -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 { @@ -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 { @@ -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`. - /// - /// - `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 { - 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 { - 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 { + fn build_dyn_namespace(&self) -> Result { 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 { - 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 { - #try_from_impl - } - } - - impl #generics_decl ::xso::FromXml for #ident #generics_ref #where_clause { - fn from_tree(elem: ::xso::exports::minidom::Element) -> Result { - Self::try_from(elem) - } - - fn absent() -> Option { - None - } - } - }) -} - -/// `DynNamespace` derive macro implementation for structs. -pub(crate) fn dyn_namespace(item: syn::ItemStruct) -> Result { - 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 { - 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> { + 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; + if let Some(wrapped_with) = wrapped_with { + def = crate::wrapped::wrap(&span, wrapped_with, &item.ident, def)?; + } + Ok(def) } diff --git a/xso-proc/src/wrapped.rs b/xso-proc/src/wrapped.rs new file mode 100644 index 0000000..9e5ee70 --- /dev/null +++ b/xso-proc/src/wrapped.rs @@ -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, +} + +impl Field for WrappedField { + fn build_try_from_element( + &self, + _container_name: &ParentRef, + _container_namespace_expr: &Expr, + tempname: Ident, + _member: &Member, + _ty: &Type, + ) -> Result { + 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 { + 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 { + // 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 { + 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 { + 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 { + 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, +) -> Result> { + 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 })) +} diff --git a/xso/src/lib.rs b/xso/src/lib.rs index 989ee11..0d5d613 100644 --- a/xso/src/lib.rs +++ b/xso/src/lib.rs @@ -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 ``, on the wire, `` (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: