xso-proc: implement support for collections of children
This commit is contained in:
parent
5c3ed1435f
commit
93ba2797be
6 changed files with 395 additions and 50 deletions
|
@ -795,3 +795,22 @@ fn exhaustive_name_switched_enum_roundtrip_variant_2() {
|
|||
};
|
||||
roundtrip_full::<ExhaustiveNameSwitchedEnum>("<b xmlns='urn:example:ns1'>hello</b>")
|
||||
}
|
||||
|
||||
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||
#[xml(namespace = NS1, name = "parent")]
|
||||
struct Children {
|
||||
#[xml(child(n = ..))]
|
||||
foo: Vec<RequiredAttribute>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn children_roundtrip() {
|
||||
#[allow(unused_imports)]
|
||||
use std::{
|
||||
option::Option::{None, Some},
|
||||
result::Result::{Err, Ok},
|
||||
};
|
||||
roundtrip_full::<Children>(
|
||||
"<parent xmlns='urn:example:ns1'><attr foo='X'/><attr foo='Y'/><attr foo='Z'/></parent>",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,12 +13,13 @@ use syn::{spanned::Spanned, *};
|
|||
use rxml_validation::NcName;
|
||||
|
||||
use crate::error_message::{self, ParentRef};
|
||||
use crate::meta::{Flag, NameRef, NamespaceRef, XmlFieldMeta};
|
||||
use crate::meta::{AmountConstraint, Flag, NameRef, NamespaceRef, XmlFieldMeta};
|
||||
use crate::scope::{AsItemsScope, FromEventsScope};
|
||||
use crate::types::{
|
||||
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,
|
||||
as_optional_xml_text_fn, as_xml_iter_fn, as_xml_text_fn, default_fn, extend_fn, from_events_fn,
|
||||
from_xml_builder_ty, from_xml_text_fn, into_iterator_into_iter_fn, into_iterator_item_ty,
|
||||
into_iterator_iter_ty, item_iter_ty, option_ty, ref_ty, string_ty, text_codec_decode_fn,
|
||||
text_codec_encode_fn,
|
||||
};
|
||||
|
||||
/// Code slices necessary for declaring and initializing a temporary variable
|
||||
|
@ -140,8 +141,8 @@ enum FieldKind {
|
|||
/// The XML name of the attribute.
|
||||
xml_name: NameRef,
|
||||
|
||||
// Flag indicating whether the value should be defaulted if the
|
||||
// attribute is absent.
|
||||
/// Flag indicating whether the value should be defaulted if the
|
||||
/// attribute is absent.
|
||||
default_: Flag,
|
||||
},
|
||||
|
||||
|
@ -153,9 +154,12 @@ enum FieldKind {
|
|||
|
||||
/// The field maps to a child
|
||||
Child {
|
||||
// Flag indicating whether the value should be defaulted if the
|
||||
// child is absent.
|
||||
/// Flag indicating whether the value should be defaulted if the
|
||||
/// child is absent.
|
||||
default_: Flag,
|
||||
|
||||
/// Number of child elements allowed.
|
||||
amount: AmountConstraint,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -203,7 +207,26 @@ impl FieldKind {
|
|||
|
||||
XmlFieldMeta::Text { codec } => Ok(Self::Text { codec }),
|
||||
|
||||
XmlFieldMeta::Child { default_ } => Ok(Self::Child { default_ }),
|
||||
XmlFieldMeta::Child { default_, amount } => {
|
||||
if let Some(AmountConstraint::Any(ref amount_span)) = amount {
|
||||
if let Flag::Present(ref flag_span) = default_ {
|
||||
let mut err = Error::new(
|
||||
*flag_span,
|
||||
"`default` has no meaning for child collections",
|
||||
);
|
||||
err.combine(Error::new(
|
||||
*amount_span,
|
||||
"the field is treated as a collection because of this `n` value",
|
||||
));
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self::Child {
|
||||
default_,
|
||||
amount: amount.unwrap_or(AmountConstraint::FixedSingle(Span::call_site())),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -345,17 +368,31 @@ impl FieldDef {
|
|||
})
|
||||
}
|
||||
|
||||
FieldKind::Child { ref default_ } => {
|
||||
FieldKind::Child {
|
||||
ref default_,
|
||||
ref amount,
|
||||
} => {
|
||||
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 element_ty = match amount {
|
||||
AmountConstraint::FixedSingle(_) => self.ty.clone(),
|
||||
AmountConstraint::Any(_) => into_iterator_item_ty(self.ty.clone()),
|
||||
};
|
||||
|
||||
let from_events = from_events_fn(self.ty.clone());
|
||||
let from_xml_builder = from_xml_builder_ty(self.ty.clone());
|
||||
let from_events = from_events_fn(element_ty.clone());
|
||||
let from_xml_builder = from_xml_builder_ty(element_ty.clone());
|
||||
|
||||
let matcher = quote! { #from_events(name, attrs) };
|
||||
let builder = from_xml_builder;
|
||||
|
||||
match amount {
|
||||
AmountConstraint::FixedSingle(_) => {
|
||||
let missing_msg =
|
||||
error_message::on_missing_child(container_name, &self.member);
|
||||
|
||||
let on_absent = match default_ {
|
||||
Flag::Absent => quote! {
|
||||
|
@ -374,10 +411,8 @@ impl FieldDef {
|
|||
init: quote! { ::std::option::Option::None },
|
||||
ty: option_ty(self.ty.clone()),
|
||||
},
|
||||
matcher: quote! {
|
||||
#from_events(name, attrs)
|
||||
},
|
||||
builder: from_xml_builder,
|
||||
matcher,
|
||||
builder,
|
||||
collect: quote! {
|
||||
#field_access = ::std::option::Option::Some(#substate_result);
|
||||
},
|
||||
|
@ -389,6 +424,24 @@ impl FieldDef {
|
|||
},
|
||||
})
|
||||
}
|
||||
AmountConstraint::Any(_) => {
|
||||
let ty_extend = extend_fn(self.ty.clone(), element_ty.clone());
|
||||
let ty_default = default_fn(self.ty.clone());
|
||||
Ok(FieldBuilderPart::Nested {
|
||||
value: FieldTempInit {
|
||||
init: quote! { #ty_default() },
|
||||
ty: self.ty.clone(),
|
||||
},
|
||||
matcher,
|
||||
builder,
|
||||
collect: quote! {
|
||||
#ty_extend(&mut #field_access, [#substate_result]);
|
||||
},
|
||||
finalize: quote! { #field_access },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -442,7 +495,10 @@ impl FieldDef {
|
|||
Ok(FieldIteratorPart::Text { generator })
|
||||
}
|
||||
|
||||
FieldKind::Child { default_: _ } => {
|
||||
FieldKind::Child {
|
||||
default_: _,
|
||||
amount: AmountConstraint::FixedSingle(_),
|
||||
} => {
|
||||
let AsItemsScope { ref lifetime, .. } = scope;
|
||||
|
||||
let as_xml_iter = as_xml_iter_fn(self.ty.clone());
|
||||
|
@ -460,6 +516,63 @@ impl FieldDef {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
FieldKind::Child {
|
||||
default_: _,
|
||||
amount: AmountConstraint::Any(_),
|
||||
} => {
|
||||
let AsItemsScope { ref lifetime, .. } = scope;
|
||||
|
||||
// This should give us the type of element stored in the
|
||||
// collection.
|
||||
let element_ty = into_iterator_item_ty(self.ty.clone());
|
||||
|
||||
// And this is the collection type we actually work with --
|
||||
// as_xml_iter uses references after all.
|
||||
let ty = ref_ty(self.ty.clone(), lifetime.clone());
|
||||
|
||||
// as_xml_iter is called on the bare type (not the ref type)
|
||||
let as_xml_iter = as_xml_iter_fn(element_ty.clone());
|
||||
|
||||
// And thus the iterator associated with AsXml is also derived
|
||||
// from the bare type.
|
||||
let item_iter = item_iter_ty(element_ty.clone(), lifetime.clone());
|
||||
|
||||
// But the iterator for iterating over the elements inside the
|
||||
// collection must use the ref type.
|
||||
let element_iter = into_iterator_iter_ty(ty.clone());
|
||||
|
||||
// And likewise the into_iter impl.
|
||||
let into_iter = into_iterator_into_iter_fn(ty.clone());
|
||||
|
||||
let state_ty = Type::Tuple(TypeTuple {
|
||||
paren_token: token::Paren::default(),
|
||||
elems: [element_iter, option_ty(item_iter)].into_iter().collect(),
|
||||
});
|
||||
|
||||
Ok(FieldIteratorPart::Content {
|
||||
value: FieldTempInit {
|
||||
init: quote! {
|
||||
(#into_iter(#bound_name), ::core::option::Option::None)
|
||||
},
|
||||
ty: state_ty,
|
||||
},
|
||||
generator: quote! {
|
||||
loop {
|
||||
if let ::core::option::Option::Some(current) = #bound_name.1.as_mut() {
|
||||
if let ::core::option::Option::Some(item) = current.next() {
|
||||
break ::core::option::Option::Some(item).transpose();
|
||||
}
|
||||
}
|
||||
if let ::core::option::Option::Some(item) = #bound_name.0.next() {
|
||||
#bound_name.1 = ::core::option::Option::Some(#as_xml_iter(item)?)
|
||||
} else {
|
||||
break ::core::result::Result::Ok(::core::option::Option::None)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -180,6 +180,53 @@ impl quote::ToTokens for NameRef {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents the amount constraint used with child elements.
|
||||
///
|
||||
/// Currently, this only supports "one" (literal `1`) or "any amount" (`..`).
|
||||
/// In the future, we might want to add support for any range pattern for
|
||||
/// `usize` and any positive integer literal.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AmountConstraint {
|
||||
/// Equivalent to `1`
|
||||
#[allow(dead_code)]
|
||||
FixedSingle(Span),
|
||||
|
||||
/// Equivalent to `..`.
|
||||
Any(Span),
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for AmountConstraint {
|
||||
fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
|
||||
if input.peek(LitInt) && !input.peek2(token::DotDot) && !input.peek2(token::DotDotEq) {
|
||||
let lit: LitInt = input.parse()?;
|
||||
let value: usize = lit.base10_parse()?;
|
||||
if value == 1 {
|
||||
Ok(Self::FixedSingle(lit.span()))
|
||||
} else {
|
||||
Err(Error::new(lit.span(), "only `1` and `..` are allowed here"))
|
||||
}
|
||||
} else {
|
||||
let p: PatRange = input.parse()?;
|
||||
if let Some(attr) = p.attrs.first() {
|
||||
return Err(Error::new_spanned(attr, "attributes not allowed here"));
|
||||
}
|
||||
if let Some(start) = p.start.as_ref() {
|
||||
return Err(Error::new_spanned(
|
||||
start,
|
||||
"only full ranges (`..`) are allowed here",
|
||||
));
|
||||
}
|
||||
if let Some(end) = p.end.as_ref() {
|
||||
return Err(Error::new_spanned(
|
||||
end,
|
||||
"only full ranges (`..`) are allowed here",
|
||||
));
|
||||
}
|
||||
Ok(Self::Any(p.span()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a boolean flag from a `#[xml(..)]` attribute meta.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum Flag {
|
||||
|
@ -558,6 +605,9 @@ pub(crate) enum XmlFieldMeta {
|
|||
Child {
|
||||
/// The `default` flag.
|
||||
default_: Flag,
|
||||
|
||||
/// The `n` flag.
|
||||
amount: Option<AmountConstraint>,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -694,6 +744,7 @@ impl XmlFieldMeta {
|
|||
fn child_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
|
||||
if meta.input.peek(syn::token::Paren) {
|
||||
let mut default_ = Flag::Absent;
|
||||
let mut amount = None;
|
||||
meta.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("default") {
|
||||
if default_.is_set() {
|
||||
|
@ -701,14 +752,21 @@ impl XmlFieldMeta {
|
|||
}
|
||||
default_ = (&meta.path).into();
|
||||
Ok(())
|
||||
} else if meta.path.is_ident("n") {
|
||||
if amount.is_some() {
|
||||
return Err(Error::new_spanned(meta.path, "duplicate `n` key"));
|
||||
}
|
||||
amount = Some(meta.value()?.parse()?);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new_spanned(meta.path, "unsupported key"))
|
||||
}
|
||||
})?;
|
||||
Ok(Self::Child { default_ })
|
||||
Ok(Self::Child { default_, amount })
|
||||
} else {
|
||||
Ok(Self::Child {
|
||||
default_: Flag::Absent,
|
||||
amount: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -622,3 +622,126 @@ pub(crate) fn item_iter_ty(of_ty: Type, lifetime: Lifetime) -> Type {
|
|||
});
|
||||
Type::Path(ty)
|
||||
}
|
||||
|
||||
/// Construct a [`syn::TypePath`] referring to `<#of_ty as IntoIterator>`.
|
||||
fn into_iterator_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: 3,
|
||||
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("std", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("iter", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("IntoIterator", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Construct a [`syn::Type`] referring to
|
||||
/// `<#of_ty as IntoIterator>::IntoIter`.
|
||||
pub(crate) fn into_iterator_iter_ty(of_ty: Type) -> Type {
|
||||
let (span, mut ty) = into_iterator_of(of_ty);
|
||||
ty.path.segments.push(PathSegment {
|
||||
ident: Ident::new("IntoIter", span),
|
||||
arguments: PathArguments::None,
|
||||
});
|
||||
Type::Path(ty)
|
||||
}
|
||||
|
||||
/// Construct a [`syn::Type`] referring to
|
||||
/// `<#of_ty as IntoIterator>::Item`.
|
||||
pub(crate) fn into_iterator_item_ty(of_ty: Type) -> Type {
|
||||
let (span, mut ty) = into_iterator_of(of_ty);
|
||||
ty.path.segments.push(PathSegment {
|
||||
ident: Ident::new("Item", span),
|
||||
arguments: PathArguments::None,
|
||||
});
|
||||
Type::Path(ty)
|
||||
}
|
||||
|
||||
/// Construct a [`syn::Expr`] referring to
|
||||
/// `<#of_ty as IntoIterator>::into_iter`.
|
||||
pub(crate) fn into_iterator_into_iter_fn(of_ty: Type) -> Expr {
|
||||
let (span, mut ty) = into_iterator_of(of_ty);
|
||||
ty.path.segments.push(PathSegment {
|
||||
ident: Ident::new("into_iter", span),
|
||||
arguments: PathArguments::None,
|
||||
});
|
||||
Expr::Path(ExprPath {
|
||||
attrs: Vec::new(),
|
||||
qself: ty.qself,
|
||||
path: ty.path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a [`syn::Expr`] referring to
|
||||
/// `<#of_ty as ::std::iter::Extend>::extend`.
|
||||
pub(crate) fn extend_fn(of_ty: Type, item_ty: Type) -> Expr {
|
||||
let span = of_ty.span();
|
||||
Expr::Path(ExprPath {
|
||||
attrs: Vec::new(),
|
||||
qself: Some(QSelf {
|
||||
lt_token: syn::token::Lt { spans: [span] },
|
||||
ty: Box::new(of_ty),
|
||||
position: 3,
|
||||
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("std", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("iter", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("Extend", span),
|
||||
arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments {
|
||||
colon2_token: Some(syn::token::PathSep {
|
||||
spans: [span, span],
|
||||
}),
|
||||
lt_token: syn::token::Lt { spans: [span] },
|
||||
args: [GenericArgument::Type(item_ty)].into_iter().collect(),
|
||||
gt_token: syn::token::Gt { spans: [span] },
|
||||
}),
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("extend", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ Version NEXT:
|
|||
of text codecs.
|
||||
* Added
|
||||
- Support for child elements in derive macros. Child elements may also
|
||||
be wrapped in Option or Box.
|
||||
be wrapped in Option or Box or in containers like Vec or HashSet.
|
||||
- Support for overriding the names of the types generated by the derive
|
||||
macros.
|
||||
- Support for deriving FromXml and AsXml on enums.
|
||||
|
|
|
@ -219,17 +219,29 @@ assert_eq!(foo, Foo {
|
|||
The `child` meta causes the field to be mapped to a child element of the
|
||||
element.
|
||||
|
||||
The following keys can be used inside the `#[xml(child(..))]` meta:
|
||||
|
||||
| Key | Value type | Description |
|
||||
| --- | --- | --- |
|
||||
| `default` | flag | If present, an absent child will substitute the default value instead of raising an error. |
|
||||
| `n` | `1` or `..` | If `1`, a single element is parsed. If `..`, a collection is parsed. Defaults to `1`. |
|
||||
|
||||
The field's type must implement [`FromXml`] in order to derive `FromXml` and
|
||||
When parsing a single child element (i.e. `n = 1` or no `n` value set at all),
|
||||
the field's type must implement [`FromXml`] in order to derive `FromXml` and
|
||||
[`AsXml`] in order to derive `AsXml`.
|
||||
|
||||
When parsing a collection (with `n = ..`), the field's type must implement
|
||||
[`IntoIterator<Item = T>`][`std::iter::IntoIterator`], where `T` must
|
||||
implement [`FromXml`] in order to derive `FromXml` and [`AsXml`] in order to
|
||||
derive `AsXml`. In addition, the field's type must implement
|
||||
[`Extend<T>`][`std::iter::Extend`] to derive `FromXml` and the field's
|
||||
reference type must implement `IntoIterator<Item = &'_ T>` to derive `AsXml`.
|
||||
|
||||
If `default` is specified and the child is absent in the source, the value
|
||||
is generated using [`std::default::Default`], requiring the field type to
|
||||
implement the `Default` trait for a `FromXml` derivation. `default` has no
|
||||
influence on `AsXml`.
|
||||
influence on `AsXml`. Combining `default` and `n` where `n` is not set to `1`
|
||||
is not supported and will cause a compile-time error.
|
||||
|
||||
##### Example
|
||||
|
||||
|
@ -242,6 +254,13 @@ struct Child {
|
|||
some_attr: String,
|
||||
}
|
||||
|
||||
#[derive(FromXml, Debug, PartialEq)]
|
||||
#[xml(namespace = "urn:example", name = "other-child")]
|
||||
struct OtherChild {
|
||||
#[xml(attribute = "some-attr")]
|
||||
some_attr: String,
|
||||
}
|
||||
|
||||
#[derive(FromXml, Debug, PartialEq)]
|
||||
#[xml(namespace = "urn:example", name = "parent")]
|
||||
struct Parent {
|
||||
|
@ -250,15 +269,28 @@ struct Parent {
|
|||
|
||||
#[xml(child)]
|
||||
bar: Child,
|
||||
|
||||
#[xml(child(n = ..))]
|
||||
baz: Vec<OtherChild>,
|
||||
}
|
||||
|
||||
let parent: Parent = xso::from_bytes(b"<parent
|
||||
xmlns='urn:example'
|
||||
foo='hello world!'
|
||||
><child some-attr='within'/></parent>").unwrap();
|
||||
><child
|
||||
some-attr='within'
|
||||
/><other-child
|
||||
some-attr='c1'
|
||||
/><other-child
|
||||
some-attr='c2'
|
||||
/></parent>").unwrap();
|
||||
assert_eq!(parent, Parent {
|
||||
foo: "hello world!".to_owned(),
|
||||
bar: Child { some_attr: "within".to_owned() },
|
||||
baz: vec! [
|
||||
OtherChild { some_attr: "c1".to_owned() },
|
||||
OtherChild { some_attr: "c2".to_owned() },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in a new issue