xso-proc: add support for child elements
This commit is contained in:
parent
1265f4bb67
commit
5bd36eccfc
11 changed files with 576 additions and 13 deletions
|
|
@ -491,3 +491,33 @@ fn text_with_codec_roundtrip_non_empty() {
|
||||||
};
|
};
|
||||||
roundtrip_full::<TextWithCodec>("<text xmlns='urn:example:ns1'>hello</text>");
|
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:
|
Version 0.1.0:
|
||||||
2024-07-25 Jonas Schäfer <jonas@zombofant.net>
|
2024-07-25 Jonas Schäfer <jonas@zombofant.net>
|
||||||
* Initial release of this crate
|
* Initial release of this crate
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use crate::error_message::ParentRef;
|
||||||
use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit};
|
use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit};
|
||||||
use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
|
use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
|
||||||
use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State};
|
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.
|
/// A struct or enum variant's contents.
|
||||||
pub(crate) struct Compound {
|
pub(crate) struct Compound {
|
||||||
|
|
@ -78,6 +78,8 @@ impl Compound {
|
||||||
ref attrs,
|
ref attrs,
|
||||||
ref builder_data_ident,
|
ref builder_data_ident,
|
||||||
ref text,
|
ref text,
|
||||||
|
ref substate_data,
|
||||||
|
ref substate_result,
|
||||||
..
|
..
|
||||||
} = scope;
|
} = scope;
|
||||||
|
|
||||||
|
|
@ -92,12 +94,14 @@ impl Compound {
|
||||||
let mut builder_data_def = TokenStream::default();
|
let mut builder_data_def = TokenStream::default();
|
||||||
let mut builder_data_init = TokenStream::default();
|
let mut builder_data_init = TokenStream::default();
|
||||||
let mut output_cons = TokenStream::default();
|
let mut output_cons = TokenStream::default();
|
||||||
|
let mut child_matchers = TokenStream::default();
|
||||||
let mut text_handler = None;
|
let mut text_handler = None;
|
||||||
|
|
||||||
for field in self.fields.iter() {
|
for (i, field) in self.fields.iter().enumerate() {
|
||||||
let member = field.member();
|
let member = field.member();
|
||||||
let builder_field_name = mangle_member(member);
|
let builder_field_name = mangle_member(member);
|
||||||
let part = field.make_builder_part(&scope, output_name)?;
|
let part = field.make_builder_part(&scope, output_name)?;
|
||||||
|
let state_name = quote::format_ident!("{}Field{}", state_prefix, i);
|
||||||
|
|
||||||
match part {
|
match part {
|
||||||
FieldBuilderPart::Init {
|
FieldBuilderPart::Init {
|
||||||
|
|
@ -143,6 +147,65 @@ impl Compound {
|
||||||
#member: #finalize,
|
#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
|
#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))
|
::core::result::Result::Err(::xso::error::Error::Other(#unknown_child_err))
|
||||||
}
|
}
|
||||||
::xso::exports::rxml::Event::Text(_, #text) => {
|
::xso::exports::rxml::Event::Text(_, #text) => {
|
||||||
|
|
@ -270,7 +335,7 @@ impl Compound {
|
||||||
for (i, field) in self.fields.iter().enumerate() {
|
for (i, field) in self.fields.iter().enumerate() {
|
||||||
let member = field.member();
|
let member = field.member();
|
||||||
let bound_name = mangle_member(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 state_name = quote::format_ident!("{}Field{}", state_prefix, i);
|
||||||
let ty = scope.borrow(field.ty().clone());
|
let ty = scope.borrow(field.ty().clone());
|
||||||
|
|
||||||
|
|
@ -321,6 +386,32 @@ impl Compound {
|
||||||
#bound_name,
|
#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
|
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::error_message::{self, ParentRef};
|
||||||
use crate::meta::{Flag, NameRef, NamespaceRef, XmlFieldMeta};
|
use crate::meta::{Flag, NameRef, NamespaceRef, XmlFieldMeta};
|
||||||
use crate::scope::FromEventsScope;
|
use crate::scope::{AsItemsScope, FromEventsScope};
|
||||||
use crate::types::{
|
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,
|
text_codec_decode_fn, text_codec_encode_fn,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -58,6 +59,38 @@ pub(crate) enum FieldBuilderPart {
|
||||||
/// temporary value.
|
/// temporary value.
|
||||||
finalize: TokenStream,
|
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.
|
/// 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.
|
/// String, which is then emitted as text data.
|
||||||
generator: TokenStream,
|
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.
|
/// Specify how the field is mapped to XML.
|
||||||
|
|
@ -102,6 +150,9 @@ enum FieldKind {
|
||||||
/// Optional codec to use
|
/// Optional codec to use
|
||||||
codec: Option<Type>,
|
codec: Option<Type>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// The field maps to a child
|
||||||
|
Child,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FieldKind {
|
impl FieldKind {
|
||||||
|
|
@ -147,6 +198,8 @@ impl FieldKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
XmlFieldMeta::Text { codec } => Ok(Self::Text { codec }),
|
XmlFieldMeta::Text { codec } => Ok(Self::Text { codec }),
|
||||||
|
|
||||||
|
XmlFieldMeta::Child => Ok(Self::Child),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -287,6 +340,39 @@ impl FieldDef {
|
||||||
finalize,
|
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
|
/// `bound_name` must be the name to which the field's value is bound in
|
||||||
/// the iterator code.
|
/// 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 {
|
match self.kind {
|
||||||
FieldKind::Attribute {
|
FieldKind::Attribute {
|
||||||
ref xml_name,
|
ref xml_name,
|
||||||
|
|
@ -335,6 +425,25 @@ impl FieldDef {
|
||||||
|
|
||||||
Ok(FieldIteratorPart::Text { generator })
|
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.
|
/// The path to the optional codec type.
|
||||||
codec: Option<Type>,
|
codec: Option<Type>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// `#[xml(child)`
|
||||||
|
Child,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XmlFieldMeta {
|
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
|
/// Parse [`Self`] from a nestd meta, switching on the identifier
|
||||||
/// of that nested meta.
|
/// of that nested meta.
|
||||||
fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
|
fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
|
||||||
|
|
@ -427,6 +435,8 @@ impl XmlFieldMeta {
|
||||||
Self::attribute_from_meta(meta)
|
Self::attribute_from_meta(meta)
|
||||||
} else if meta.path.is_ident("text") {
|
} else if meta.path.is_ident("text") {
|
||||||
Self::text_from_meta(meta)
|
Self::text_from_meta(meta)
|
||||||
|
} else if meta.path.is_ident("child") {
|
||||||
|
Self::child_from_meta(meta)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::new_spanned(meta.path, "unsupported field meta"))
|
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 time, using [`Self::access_field`] is the correct way to access
|
||||||
/// the builder data.
|
/// the builder data.
|
||||||
pub(crate) builder_data_ident: Ident,
|
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 {
|
impl FromEventsScope {
|
||||||
|
|
@ -53,6 +63,8 @@ impl FromEventsScope {
|
||||||
attrs: Ident::new("attrs", Span::call_site()),
|
attrs: Ident::new("attrs", Span::call_site()),
|
||||||
text: Ident::new("__xso_proc_macro_text_data", 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()),
|
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.
|
/// Right-hand-side of the match arm for this state.
|
||||||
advance_body: TokenStream,
|
advance_body: TokenStream,
|
||||||
|
|
||||||
|
/// If set, that identifier will be bound mutably.
|
||||||
|
uses_mut: Option<Ident>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
|
@ -54,6 +57,7 @@ impl State {
|
||||||
decl: TokenStream::default(),
|
decl: TokenStream::default(),
|
||||||
destructure: TokenStream::default(),
|
destructure: TokenStream::default(),
|
||||||
advance_body: TokenStream::default(),
|
advance_body: TokenStream::default(),
|
||||||
|
uses_mut: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +99,14 @@ impl State {
|
||||||
pub(crate) fn set_impl(&mut self, body: TokenStream) {
|
pub(crate) fn set_impl(&mut self, body: TokenStream) {
|
||||||
self.advance_body = body;
|
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
|
/// A partial [`FromEventsStateMachine`] which only covers the builder for a
|
||||||
|
|
@ -132,18 +144,28 @@ impl FromEventsSubmachine {
|
||||||
decl,
|
decl,
|
||||||
destructure,
|
destructure,
|
||||||
advance_body,
|
advance_body,
|
||||||
|
uses_mut,
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
state_defs.extend(quote! {
|
state_defs.extend(quote! {
|
||||||
#name { #decl },
|
#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
|
// XXX: nasty hack, but works: the first member of the enum always
|
||||||
// exists and it always is the builder data, which we always need
|
// exists and it always is the builder data, which we always need
|
||||||
// mutably available. So we can just prefix the destructuring
|
// mutably available. So we can just prefix the destructuring
|
||||||
// token stream with `mut` to make that first member mutable.
|
// token stream with `mut` to make that first member mutable.
|
||||||
advance_match_arms.extend(quote! {
|
advance_match_arms.extend(quote! {
|
||||||
Self::#name { mut #destructure } => {
|
Self::#name { mut #destructure } => {
|
||||||
|
#binding
|
||||||
#advance_body
|
#advance_body
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -218,6 +240,7 @@ impl AsItemsSubmachine {
|
||||||
ref decl,
|
ref decl,
|
||||||
ref destructure,
|
ref destructure,
|
||||||
ref advance_body,
|
ref advance_body,
|
||||||
|
ref uses_mut,
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
let footer = match self.states.get(i + 1) {
|
let footer = match self.states.get(i + 1) {
|
||||||
|
|
@ -242,6 +265,30 @@ impl AsItemsSubmachine {
|
||||||
#name { #decl },
|
#name { #decl },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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! {
|
advance_match_arms.extend(quote! {
|
||||||
Self::#name { #destructure } => {
|
Self::#name { #destructure } => {
|
||||||
let item = #advance_body;
|
let item = #advance_body;
|
||||||
|
|
@ -249,6 +296,7 @@ impl AsItemsSubmachine {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AsItemsStateMachine {
|
AsItemsStateMachine {
|
||||||
defs: self.defs,
|
defs: self.defs,
|
||||||
|
|
|
||||||
|
|
@ -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
|
All this is to avoid triggering the camel case lint on the types we
|
||||||
generate.
|
generate.
|
||||||
|
* Added
|
||||||
|
- Support for child elements in derive macros.
|
||||||
|
|
||||||
Version 0.1.2:
|
Version 0.1.2:
|
||||||
2024-07-26 Jonas Schäfer <jonas@zombofant.net>
|
2024-07-26 Jonas Schäfer <jonas@zombofant.net>
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ The following mapping types are defined:
|
||||||
| Type | Description |
|
| Type | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [`attribute`](#attribute-meta) | Map the field to an XML attribute on the struct's element |
|
| [`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 |
|
| [`text`](#text-meta) | Map the field to the text content of the struct's element |
|
||||||
|
|
||||||
#### `attribute` meta
|
#### `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
|
#### `text` meta
|
||||||
|
|
||||||
The `text` meta causes the field to be mapped to the text content of the
|
The `text` meta causes the field to be mapped to the text content of the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue