Detailed changes
@@ -491,3 +491,33 @@ fn text_with_codec_roundtrip_non_empty() {
};
roundtrip_full::<TextWithCodec>("<text xmlns='urn:example:ns1'>hello</text>");
}
+
+#[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::<Parent>("<parent xmlns='urn:example:ns1'><attr foo='hello world!'/></parent>")
+}
+
+#[test]
+fn parent_positive() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ let v =
+ parse_str::<Parent>("<parent xmlns='urn:example:ns1'><attr foo='hello world!'/></parent>")
+ .unwrap();
+ assert_eq!(v.child.foo, "hello world!");
+}
@@ -1,3 +1,8 @@
+Version NEXT:
+0000-00-00 Jonas Schรคfer <jonas@zombofant.net>
+ * 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 <jonas@zombofant.net>
* Initial release of this crate
@@ -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,
+ });
+ }
}
}
@@ -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)
+}
@@ -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<T, FromEventsError>`,
+ /// 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<Option<Item>, 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<Type>,
},
+
+ /// 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<FieldIteratorPart> {
+ pub(crate) fn make_iterator_part(
+ &self,
+ scope: &AsItemsScope,
+ bound_name: &Ident,
+ ) -> Result<FieldIteratorPart> {
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()
+ },
+ })
+ }
}
}
@@ -316,6 +316,9 @@ pub(crate) enum XmlFieldMeta {
/// The path to the optional codec type.
codec: Option<Type>,
},
+
+ /// `#[xml(child)`
+ Child,
}
impl XmlFieldMeta {
@@ -420,6 +423,11 @@ impl XmlFieldMeta {
}
}
+ /// Parse a `#[xml(child)]` meta.
+ fn child_from_meta(_: ParseNestedMeta<'_>) -> Result<Self> {
+ Ok(Self::Child)
+ }
+
/// Parse [`Self`] from a nestd meta, switching on the identifier
/// of that nested meta.
fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
@@ -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"))
}
@@ -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()),
}
}
@@ -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<Ident>,
}
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 {
@@ -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)
+}
@@ -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 <jonas@zombofant.net>
@@ -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"<parent
+ xmlns='urn:example'
+ foo='hello world!'
+><child some-attr='within'/></parent>").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