From 93ba2797bee4766f34fbccb71ff2ad5ef2447e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Thu, 11 Jul 2024 17:29:29 +0200 Subject: [PATCH] xso-proc: implement support for collections of children --- parsers/src/util/macro_tests.rs | 19 +++ xso-proc/src/field.rs | 203 +++++++++++++++++++++++++------- xso-proc/src/meta.rs | 60 +++++++++- xso-proc/src/types.rs | 123 +++++++++++++++++++ xso/ChangeLog | 2 +- xso/src/from_xml_doc.md | 38 +++++- 6 files changed, 395 insertions(+), 50 deletions(-) diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 86d0e5f2..48470ab3 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -795,3 +795,22 @@ fn exhaustive_name_switched_enum_roundtrip_variant_2() { }; roundtrip_full::("hello") } + +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = NS1, name = "parent")] +struct Children { + #[xml(child(n = ..))] + foo: Vec, +} + +#[test] +fn children_roundtrip() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::( + "", + ) +} diff --git a/xso-proc/src/field.rs b/xso-proc/src/field.rs index e19dbab4..4ba404a1 100644 --- a/xso-proc/src/field.rs +++ b/xso-proc/src/field.rs @@ -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) + } + } + }, + }) + } } } diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 81654cdc..c2d1e788 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -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 { + 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, }, } @@ -694,6 +744,7 @@ impl XmlFieldMeta { fn child_from_meta(meta: ParseNestedMeta<'_>) -> Result { 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, }) } } diff --git a/xso-proc/src/types.rs b/xso-proc/src/types.rs index 851a9f67..8053b42c 100644 --- a/xso-proc/src/types.rs +++ b/xso-proc/src/types.rs @@ -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(), + }, + }) +} diff --git a/xso/ChangeLog b/xso/ChangeLog index 6b8a346e..fcc78daf 100644 --- a/xso/ChangeLog +++ b/xso/ChangeLog @@ -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. diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index 84dce1e8..5da34861 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -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`][`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`][`std::iter::Extend`] to derive `FromXml` and the field's +reference type must implement `IntoIterator` 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, } let parent: Parent = xso::from_bytes(b"").unwrap(); +>").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() }, + ], }); ```