Detailed changes
@@ -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,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 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 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 matcher = quote! { #from_events(name, attrs) };
+ let builder = from_xml_builder;
- 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,
- }
- },
- })
+ 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)
+ }
+ }
+ },
+ })
+ }
}
}
@@ -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() },
+ ],
});
```