diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 8cd42a2d..19775103 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -491,3 +491,33 @@ fn text_with_codec_roundtrip_non_empty() { }; roundtrip_full::("hello"); } + +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = NS1, name = "parent")] +struct Parent { + #[xml(child)] + child: RequiredAttribute, +} + +#[test] +fn parent_roundtrip() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::("") +} + +#[test] +fn parent_positive() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + let v = + parse_str::("") + .unwrap(); + assert_eq!(v.child.foo, "hello world!"); +} diff --git a/xso-proc/ChangeLog b/xso-proc/ChangeLog index b2d7bc70..911eb608 100644 --- a/xso-proc/ChangeLog +++ b/xso-proc/ChangeLog @@ -1,3 +1,8 @@ +Version NEXT: +0000-00-00 Jonas Schäfer + * Please see the `xso` crate for the changelog of `xso-proc`. + For discoverability, the changes to the derive macros are listed there. + Version 0.1.0: 2024-07-25 Jonas Schäfer * Initial release of this crate diff --git a/xso-proc/src/compound.rs b/xso-proc/src/compound.rs index cfec95a1..d1193b17 100644 --- a/xso-proc/src/compound.rs +++ b/xso-proc/src/compound.rs @@ -14,7 +14,7 @@ use crate::error_message::ParentRef; use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit}; use crate::scope::{mangle_member, AsItemsScope, FromEventsScope}; use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State}; -use crate::types::{namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty}; +use crate::types::{feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty}; /// A struct or enum variant's contents. pub(crate) struct Compound { @@ -78,6 +78,8 @@ impl Compound { ref attrs, ref builder_data_ident, ref text, + ref substate_data, + ref substate_result, .. } = scope; @@ -92,12 +94,14 @@ impl Compound { let mut builder_data_def = TokenStream::default(); let mut builder_data_init = TokenStream::default(); let mut output_cons = TokenStream::default(); + let mut child_matchers = TokenStream::default(); let mut text_handler = None; - for field in self.fields.iter() { + for (i, field) in self.fields.iter().enumerate() { let member = field.member(); let builder_field_name = mangle_member(member); let part = field.make_builder_part(&scope, output_name)?; + let state_name = quote::format_ident!("{}Field{}", state_prefix, i); match part { FieldBuilderPart::Init { @@ -143,6 +147,65 @@ impl Compound { #member: #finalize, }); } + + FieldBuilderPart::Nested { + value: FieldTempInit { ty, init }, + matcher, + builder, + collect, + finalize, + } => { + let feed = feed_fn(builder.clone()); + + states.push(State::new_with_builder( + state_name.clone(), + &builder_data_ident, + &builder_data_ty, + ).with_field( + substate_data, + &builder, + ).with_mut(substate_data).with_impl(quote! { + match #feed(&mut #substate_data, ev)? { + ::std::option::Option::Some(#substate_result) => { + #collect + ::std::result::Result::Ok(::std::ops::ControlFlow::Break(Self::#default_state_ident { + #builder_data_ident, + })) + } + ::std::option::Option::None => { + ::std::result::Result::Ok(::std::ops::ControlFlow::Break(Self::#state_name { + #builder_data_ident, + #substate_data, + })) + } + } + })); + + builder_data_def.extend(quote! { + #builder_field_name: #ty, + }); + + builder_data_init.extend(quote! { + #builder_field_name: #init, + }); + + child_matchers.extend(quote! { + let (name, attrs) = match #matcher { + ::std::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs }) => (name, attrs), + ::std::result::Result::Err(::xso::error::FromEventsError::Invalid(e)) => return ::std::result::Result::Err(e), + ::std::result::Result::Ok(#substate_data) => { + return ::std::result::Result::Ok(::std::ops::ControlFlow::Break(Self::#state_name { + #builder_data_ident, + #substate_data, + })) + } + }; + }); + + output_cons.extend(quote! { + #member: #finalize, + }); + } } } @@ -184,7 +247,9 @@ impl Compound { #output_cons )) } - ::xso::exports::rxml::Event::StartElement(..) => { + ::xso::exports::rxml::Event::StartElement(_, name, attrs) => { + #child_matchers + let _ = (name, attrs); ::core::result::Result::Err(::xso::error::Error::Other(#unknown_child_err)) } ::xso::exports::rxml::Event::Text(_, #text) => { @@ -270,7 +335,7 @@ impl Compound { for (i, field) in self.fields.iter().enumerate() { let member = field.member(); let bound_name = mangle_member(member); - let part = field.make_iterator_part(&bound_name)?; + let part = field.make_iterator_part(&scope, &bound_name)?; let state_name = quote::format_ident!("{}Field{}", state_prefix, i); let ty = scope.borrow(field.ty().clone()); @@ -321,6 +386,32 @@ impl Compound { #bound_name, }); } + + FieldIteratorPart::Content { + value: FieldTempInit { ty, init }, + generator, + } => { + // we have to make sure that we carry our data around in + // all the previous states. + for state in states.iter_mut() { + state.add_field(&bound_name, &ty); + } + + states.push( + State::new(state_name.clone()) + .with_field(&bound_name, &ty) + .with_mut(&bound_name) + .with_impl(quote! { + #generator? + }), + ); + destructure.extend(quote! { + #member: #bound_name, + }); + start_init.extend(quote! { + #bound_name: #init, + }); + } } } diff --git a/xso-proc/src/error_message.rs b/xso-proc/src/error_message.rs index 0c32db83..dbfc7fc1 100644 --- a/xso-proc/src/error_message.rs +++ b/xso-proc/src/error_message.rs @@ -79,3 +79,11 @@ pub(super) fn on_missing_attribute(parent_name: &ParentRef, field: &Member) -> S parent_name ) } + +/// 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) +} diff --git a/xso-proc/src/field.rs b/xso-proc/src/field.rs index de7c48c7..50414cb8 100644 --- a/xso-proc/src/field.rs +++ b/xso-proc/src/field.rs @@ -14,9 +14,10 @@ use rxml_validation::NcName; use crate::error_message::{self, ParentRef}; use crate::meta::{Flag, NameRef, NamespaceRef, XmlFieldMeta}; -use crate::scope::FromEventsScope; +use crate::scope::{AsItemsScope, FromEventsScope}; use crate::types::{ - as_optional_xml_text_fn, as_xml_text_fn, default_fn, from_xml_text_fn, string_ty, + as_optional_xml_text_fn, as_xml_iter_fn, as_xml_text_fn, default_fn, from_events_fn, + from_xml_builder_ty, from_xml_text_fn, item_iter_ty, option_ty, string_ty, text_codec_decode_fn, text_codec_encode_fn, }; @@ -58,6 +59,38 @@ pub(crate) enum FieldBuilderPart { /// temporary value. finalize: TokenStream, }, + + /// Parse a field from child element events. + Nested { + /// Expression and type which initializes a buffer to use during + /// parsing. + value: FieldTempInit, + + /// Expression which evaluates to `Result`, + /// consuming `name: rxml::QName` and `attrs: rxml::AttrMap`. + /// + /// `T` must be the type specified in the + /// [`Self::Nested::builder`] field. + matcher: TokenStream, + + /// Type implementing `xso::FromEventsBuilder` which parses the child + /// element. + /// + /// This type is returned by the expressions in + /// [`matcher`][`Self::Nested::matcher`]. + builder: Type, + + /// Expression which consumes the value stored in the identifier + /// [`crate::common::FromEventsScope::substate_result`][`FromEventsScope::substate_result`] + /// and somehow collects it into the field declared with + /// [`value`][`Self::Nested::value`]. + collect: TokenStream, + + /// Expression which consumes the data from the field declared with + /// [`value`][`Self::Nested::value`] and converts it into the field's + /// type. + finalize: TokenStream, + }, } /// Describe how a struct or enum variant's member is converted to XML data. @@ -80,6 +113,21 @@ pub(crate) enum FieldIteratorPart { /// String, which is then emitted as text data. generator: TokenStream, }, + + /// The field is emitted as series of items which form a child element. + Content { + /// Expression and type which initializes the nested iterator. + /// + /// Note that this is evaluated at construction time of the iterator. + /// Fields of this variant do not get access to their original data, + /// unless they carry it in the contents of this `value`. + value: FieldTempInit, + + /// An expression which uses the value (mutably) and evaluates to + /// a Result, Error>. Once the state returns None, the + /// processing will advance to the next state. + generator: TokenStream, + }, } /// Specify how the field is mapped to XML. @@ -102,6 +150,9 @@ enum FieldKind { /// Optional codec to use codec: Option, }, + + /// The field maps to a child + Child, } impl FieldKind { @@ -147,6 +198,8 @@ impl FieldKind { } XmlFieldMeta::Text { codec } => Ok(Self::Text { codec }), + + XmlFieldMeta::Child => Ok(Self::Child), } } } @@ -287,6 +340,39 @@ impl FieldDef { finalize, }) } + + FieldKind::Child => { + let FromEventsScope { + ref substate_result, + .. + } = scope; + let field_access = scope.access_field(&self.member); + + let missing_msg = error_message::on_missing_child(container_name, &self.member); + + let from_events = from_events_fn(self.ty.clone()); + let from_xml_builder = from_xml_builder_ty(self.ty.clone()); + + Ok(FieldBuilderPart::Nested { + value: FieldTempInit { + init: quote! { ::std::option::Option::None }, + ty: option_ty(self.ty.clone()), + }, + matcher: quote! { + #from_events(name, attrs) + }, + builder: from_xml_builder, + collect: quote! { + #field_access = ::std::option::Option::Some(#substate_result); + }, + finalize: quote! { + match #field_access { + ::std::option::Option::Some(value) => value, + ::std::option::Option::None => return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into()), + } + }, + }) + } } } @@ -294,7 +380,11 @@ impl FieldDef { /// /// `bound_name` must be the name to which the field's value is bound in /// the iterator code. - pub(crate) fn make_iterator_part(&self, bound_name: &Ident) -> Result { + pub(crate) fn make_iterator_part( + &self, + scope: &AsItemsScope, + bound_name: &Ident, + ) -> Result { match self.kind { FieldKind::Attribute { ref xml_name, @@ -335,6 +425,25 @@ impl FieldDef { Ok(FieldIteratorPart::Text { generator }) } + + FieldKind::Child => { + let AsItemsScope { ref lifetime, .. } = scope; + + let as_xml_iter = as_xml_iter_fn(self.ty.clone()); + let item_iter = item_iter_ty(self.ty.clone(), lifetime.clone()); + + Ok(FieldIteratorPart::Content { + value: FieldTempInit { + init: quote! { + #as_xml_iter(#bound_name)? + }, + ty: item_iter, + }, + generator: quote! { + #bound_name.next().transpose() + }, + }) + } } } diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 0b5fee4b..ff844cad 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -316,6 +316,9 @@ pub(crate) enum XmlFieldMeta { /// The path to the optional codec type. codec: Option, }, + + /// `#[xml(child)` + Child, } impl XmlFieldMeta { @@ -420,6 +423,11 @@ impl XmlFieldMeta { } } + /// Parse a `#[xml(child)]` meta. + fn child_from_meta(_: ParseNestedMeta<'_>) -> Result { + Ok(Self::Child) + } + /// Parse [`Self`] from a nestd meta, switching on the identifier /// of that nested meta. fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result { @@ -427,6 +435,8 @@ impl XmlFieldMeta { Self::attribute_from_meta(meta) } else if meta.path.is_ident("text") { Self::text_from_meta(meta) + } else if meta.path.is_ident("child") { + Self::child_from_meta(meta) } else { Err(Error::new_spanned(meta.path, "unsupported field meta")) } diff --git a/xso-proc/src/scope.rs b/xso-proc/src/scope.rs index bfe4c594..67fd6ba4 100644 --- a/xso-proc/src/scope.rs +++ b/xso-proc/src/scope.rs @@ -42,6 +42,16 @@ pub(crate) struct FromEventsScope { /// the time, using [`Self::access_field`] is the correct way to access /// the builder data. pub(crate) builder_data_ident: Ident, + + /// Accesses the result produced by a nested state's builder type. + /// + /// See [`crate::field::FieldBuilderPart::Nested`]. + pub(crate) substate_data: Ident, + + /// Accesses the result produced by a nested state's builder type. + /// + /// See [`crate::field::FieldBuilderPart::Nested`]. + pub(crate) substate_result: Ident, } impl FromEventsScope { @@ -53,6 +63,8 @@ impl FromEventsScope { attrs: Ident::new("attrs", Span::call_site()), text: Ident::new("__xso_proc_macro_text_data", Span::call_site()), builder_data_ident: Ident::new("__xso_proc_macro_builder_data", Span::call_site()), + substate_data: Ident::new("__xso_proc_macro_substate_data", Span::call_site()), + substate_result: Ident::new("__xso_proc_macro_substate_result", Span::call_site()), } } diff --git a/xso-proc/src/state.rs b/xso-proc/src/state.rs index b7899a07..6611b984 100644 --- a/xso-proc/src/state.rs +++ b/xso-proc/src/state.rs @@ -23,6 +23,9 @@ pub(crate) struct State { /// Right-hand-side of the match arm for this state. advance_body: TokenStream, + + /// If set, that identifier will be bound mutably. + uses_mut: Option, } impl State { @@ -54,6 +57,7 @@ impl State { decl: TokenStream::default(), destructure: TokenStream::default(), advance_body: TokenStream::default(), + uses_mut: None, } } @@ -95,6 +99,14 @@ impl State { pub(crate) fn set_impl(&mut self, body: TokenStream) { self.advance_body = body; } + + /// Modify the state to mark the given field as mutable and return the + /// modified state. + pub(crate) fn with_mut(mut self, ident: &Ident) -> Self { + assert!(self.uses_mut.is_none()); + self.uses_mut = Some(ident.clone()); + self + } } /// A partial [`FromEventsStateMachine`] which only covers the builder for a @@ -132,18 +144,28 @@ impl FromEventsSubmachine { decl, destructure, advance_body, + uses_mut, } = state; state_defs.extend(quote! { #name { #decl }, }); + let binding = if let Some(uses_mut) = uses_mut.as_ref() { + quote! { + let mut #uses_mut = #uses_mut; + } + } else { + TokenStream::default() + }; + // XXX: nasty hack, but works: the first member of the enum always // exists and it always is the builder data, which we always need // mutably available. So we can just prefix the destructuring // token stream with `mut` to make that first member mutable. advance_match_arms.extend(quote! { Self::#name { mut #destructure } => { + #binding #advance_body } }); @@ -218,6 +240,7 @@ impl AsItemsSubmachine { ref decl, ref destructure, ref advance_body, + ref uses_mut, } = state; let footer = match self.states.get(i + 1) { @@ -242,12 +265,37 @@ impl AsItemsSubmachine { #name { #decl }, }); - advance_match_arms.extend(quote! { - Self::#name { #destructure } => { - let item = #advance_body; - #footer - } - }); + if let Some(uses_mut) = uses_mut.as_ref() { + // the variant is non-consuming, meaning it can be called + // multiple times and it uses the identifier in `uses_mut` + // mutably. + // the transition is only triggered when it emits a None + // item + // (we cannot do this at the place the `State` is constructed, + // because we don't yet know all its fields then; it must be + // done here.) + advance_match_arms.extend(quote! { + Self::#name { #destructure } => { + let mut #uses_mut = #uses_mut; + match #advance_body { + ::std::option::Option::Some(item) => { + ::std::result::Result::Ok((::std::option::Option::Some(Self::#name { #destructure }), ::std::option::Option::Some(item))) + }, + item => { #footer }, + } + } + }); + } else { + // if the variant is consuming, it can only be called once. + // it may or may not emit an event, but the transition is + // always triggered + advance_match_arms.extend(quote! { + Self::#name { #destructure } => { + let item = #advance_body; + #footer + } + }); + } } AsItemsStateMachine { diff --git a/xso-proc/src/types.rs b/xso-proc/src/types.rs index 70b9f93b..692caaa8 100644 --- a/xso-proc/src/types.rs +++ b/xso-proc/src/types.rs @@ -422,3 +422,213 @@ pub(crate) fn phantom_lifetime_ty(lifetime: Lifetime) -> Type { }, }) } + +/// Construct a [`syn::TypePath`] referring to +/// `<#of_ty as ::xso::FromXml>`. +fn from_xml_of(of_ty: Type) -> (Span, TypePath) { + let span = of_ty.span(); + ( + span, + TypePath { + qself: Some(QSelf { + lt_token: syn::token::Lt { spans: [span] }, + ty: Box::new(of_ty), + position: 2, + as_token: Some(syn::token::As { span }), + gt_token: syn::token::Gt { spans: [span] }, + }), + path: Path { + leading_colon: Some(syn::token::PathSep { + spans: [span, span], + }), + segments: [ + PathSegment { + ident: Ident::new("xso", span), + arguments: PathArguments::None, + }, + PathSegment { + ident: Ident::new("FromXml", span), + arguments: PathArguments::None, + }, + ] + .into_iter() + .collect(), + }, + }, + ) +} + +/// Construct a [`syn::Type`] referring to +/// `<#of_ty as ::xso::FromXml>::Builder`. +pub(crate) fn from_xml_builder_ty(of_ty: Type) -> Type { + let (span, mut ty) = from_xml_of(of_ty); + ty.path.segments.push(PathSegment { + ident: Ident::new("Builder", span), + arguments: PathArguments::None, + }); + Type::Path(ty) +} + +/// Construct a [`syn::Expr`] referring to +/// `<#of_ty as ::xso::FromXml>::from_events`. +pub(crate) fn from_events_fn(of_ty: Type) -> Expr { + let (span, mut ty) = from_xml_of(of_ty); + ty.path.segments.push(PathSegment { + ident: Ident::new("from_events", span), + arguments: PathArguments::None, + }); + Expr::Path(ExprPath { + attrs: Vec::new(), + qself: ty.qself, + path: ty.path, + }) +} + +/// Construct a [`syn::Type`] which wraps the given `ty` in +/// `::std::option::Option<_>`. +pub(crate) fn option_ty(ty: Type) -> Type { + let span = ty.span(); + Type::Path(TypePath { + qself: None, + path: Path { + leading_colon: Some(syn::token::PathSep { + spans: [span, span], + }), + segments: [ + PathSegment { + ident: Ident::new("std", span), + arguments: PathArguments::None, + }, + PathSegment { + ident: Ident::new("option", span), + arguments: PathArguments::None, + }, + PathSegment { + ident: Ident::new("Option", span), + arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { + colon2_token: None, + lt_token: syn::token::Lt { spans: [span] }, + args: [GenericArgument::Type(ty)].into_iter().collect(), + gt_token: syn::token::Gt { spans: [span] }, + }), + }, + ] + .into_iter() + .collect(), + }, + }) +} + +/// Construct a [`syn::TypePath`] referring to +/// `<#of_ty as ::xso::FromEventsBuilder>`. +fn from_events_builder_of(of_ty: Type) -> (Span, TypePath) { + let span = of_ty.span(); + ( + span, + TypePath { + qself: Some(QSelf { + lt_token: syn::token::Lt { spans: [span] }, + ty: Box::new(of_ty), + position: 2, + as_token: Some(syn::token::As { span }), + gt_token: syn::token::Gt { spans: [span] }, + }), + path: Path { + leading_colon: Some(syn::token::PathSep { + spans: [span, span], + }), + segments: [ + PathSegment { + ident: Ident::new("xso", span), + arguments: PathArguments::None, + }, + PathSegment { + ident: Ident::new("FromEventsBuilder", span), + arguments: PathArguments::None, + }, + ] + .into_iter() + .collect(), + }, + }, + ) +} + +/// Construct a [`syn::Expr`] referring to +/// `<#of_ty as ::xso::FromEventsBuilder>::feed`. +pub(crate) fn feed_fn(of_ty: Type) -> Expr { + let (span, mut ty) = from_events_builder_of(of_ty); + ty.path.segments.push(PathSegment { + ident: Ident::new("feed", span), + arguments: PathArguments::None, + }); + Expr::Path(ExprPath { + attrs: Vec::new(), + qself: ty.qself, + path: ty.path, + }) +} + +fn as_xml_of(of_ty: Type) -> (Span, TypePath) { + let span = of_ty.span(); + ( + span, + TypePath { + qself: Some(QSelf { + lt_token: syn::token::Lt { spans: [span] }, + ty: Box::new(of_ty), + position: 2, + as_token: Some(syn::token::As { span }), + gt_token: syn::token::Gt { spans: [span] }, + }), + path: Path { + leading_colon: Some(syn::token::PathSep { + spans: [span, span], + }), + segments: [ + PathSegment { + ident: Ident::new("xso", span), + arguments: PathArguments::None, + }, + PathSegment { + ident: Ident::new("AsXml", span), + arguments: PathArguments::None, + }, + ] + .into_iter() + .collect(), + }, + }, + ) +} + +/// Construct a [`syn::Expr`] referring to +/// `<#of_ty as ::xso::AsXml>::as_xml_iter`. +pub(crate) fn as_xml_iter_fn(of_ty: Type) -> Expr { + let (span, mut ty) = as_xml_of(of_ty); + ty.path.segments.push(PathSegment { + ident: Ident::new("as_xml_iter", span), + arguments: PathArguments::None, + }); + Expr::Path(ExprPath { + attrs: Vec::new(), + qself: ty.qself, + path: ty.path, + }) +} + +/// Construct a [`syn::Type`] referring to +/// `<#of_ty as ::xso::AsXml>::ItemIter`. +pub(crate) fn item_iter_ty(of_ty: Type, lifetime: Lifetime) -> Type { + let (span, mut ty) = as_xml_of(of_ty); + ty.path.segments.push(PathSegment { + ident: Ident::new("ItemIter", span), + arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { + colon2_token: None, + lt_token: token::Lt { spans: [span] }, + args: [GenericArgument::Lifetime(lifetime)].into_iter().collect(), + gt_token: token::Gt { spans: [span] }, + }), + }); + Type::Path(ty) +} diff --git a/xso/ChangeLog b/xso/ChangeLog index ec84dc57..c3987e59 100644 --- a/xso/ChangeLog +++ b/xso/ChangeLog @@ -13,6 +13,8 @@ Version NEXT: All this is to avoid triggering the camel case lint on the types we generate. + * Added + - Support for child elements in derive macros. Version 0.1.2: 2024-07-26 Jonas Schäfer diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index 7ff83bd0..5fb57697 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -69,6 +69,7 @@ The following mapping types are defined: | Type | Description | | --- | --- | | [`attribute`](#attribute-meta) | Map the field to an XML attribute on the struct's element | +| [`child`](#child-meta) | Map the field to a child element | | [`text`](#text-meta) | Map the field to the text content of the struct's element | #### `attribute` meta @@ -135,6 +136,43 @@ assert_eq!(foo, Foo { }); ``` +#### `child` meta + +The `child` meta causes the field to be mapped to a child element of the +element. It supports no options. The field's type must implement [`FromXml`] +in order to derive `FromXml` and [`AsXml`] in order to derive `AsXml`. + +##### Example + +```rust +# use xso::FromXml; +#[derive(FromXml, Debug, PartialEq)] +#[xml(namespace = "urn:example", name = "child")] +struct Child { + #[xml(attribute = "some-attr")] + some_attr: String, +} + +#[derive(FromXml, Debug, PartialEq)] +#[xml(namespace = "urn:example", name = "parent")] +struct Parent { + #[xml(attribute)] + foo: String, + + #[xml(child)] + bar: Child, +} + +let parent: Parent = xso::from_bytes(b"").unwrap(); +assert_eq!(parent, Parent { + foo: "hello world!".to_owned(), + bar: Child { some_attr: "within".to_owned() }, +}); +``` + #### `text` meta The `text` meta causes the field to be mapped to the text content of the