xso-proc: implement support for collections of children

This commit is contained in:
Jonas Schäfer 2024-07-11 17:29:29 +02:00
parent 5c3ed1435f
commit 93ba2797be
6 changed files with 395 additions and 50 deletions

View file

@ -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>",
)
}

View file

@ -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,49 +368,79 @@ 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 from_events = from_events_fn(self.ty.clone());
let from_xml_builder = from_xml_builder_ty(self.ty.clone());
let on_absent = match default_ {
Flag::Absent => quote! {
return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into())
},
Flag::Present(_) => {
let default_ = default_fn(self.ty.clone());
quote! {
#default_()
}
}
let element_ty = match amount {
AmountConstraint::FixedSingle(_) => self.ty.clone(),
AmountConstraint::Any(_) => into_iterator_item_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 => #on_absent,
}
},
})
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! {
return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into())
},
Flag::Present(_) => {
let default_ = default_fn(self.ty.clone());
quote! {
#default_()
}
}
};
Ok(FieldBuilderPart::Nested {
value: FieldTempInit {
init: quote! { ::std::option::Option::None },
ty: option_ty(self.ty.clone()),
},
matcher,
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 => #on_absent,
}
},
})
}
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)
}
}
},
})
}
}
}

View file

@ -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,
})
}
}

View file

@ -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(),
},
})
}

View file

@ -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.

View file

@ -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() },
],
});
```