From 66233b0150433c8c95c7201da5fd0238a1eb4397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Thu, 3 Oct 2024 12:51:57 +0200 Subject: [PATCH] xso: add support for ignoring unknown children --- parsers/src/util/macro_tests.rs | 42 ++++++++++++++++++++++++++++ xso-proc/src/compound.rs | 48 ++++++++++++++++++++++++++++++-- xso-proc/src/enums.rs | 11 +++++++- xso-proc/src/field/mod.rs | 2 +- xso-proc/src/meta.rs | 15 ++++++++++ xso-proc/src/structs.rs | 9 +++++- xso-proc/src/types.rs | 49 +++++++++++++++++++++++++++++++++ xso/src/from_xml_doc.md | 2 ++ xso/src/fromxml.rs | 38 +++++++++++++++++++++++++ xso/src/lib.rs | 32 +++++++++++++++++++++ 10 files changed, 242 insertions(+), 6 deletions(-) diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 62ef9430f016759a06b803a644ff586b2de54525..4a5fd52328343f11ec58953444e986ee0bc1aaa8 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -1821,3 +1821,45 @@ fn ignore_unknown_attributes_negative_unexpected_child() { other => panic!("unexpected result: {:?}", other), } } + +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = NS1, name = "foo", on_unknown_child = Discard)] +struct IgnoreUnknownChildren; + +#[test] +fn ignore_unknown_children_empty_roundtrip() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::(""); +} + +#[test] +fn ignore_unknown_children_positive() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + match parse_str::("") { + Ok(IgnoreUnknownChildren) => (), + other => panic!("unexpected result: {:?}", other), + } +} + +#[test] +fn ignore_unknown_children_negative_unexpected_attribute() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + match parse_str::("") { + Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => { + assert_eq!(e, "Unknown attribute in IgnoreUnknownChildren element."); + } + other => panic!("unexpected result: {:?}", other), + } +} diff --git a/xso-proc/src/compound.rs b/xso-proc/src/compound.rs index 1cae55c691bef6227c385604f4a50d120099b995..e9b9be120c1e4230c9a7180ceaa839d6e33b4b55 100644 --- a/xso-proc/src/compound.rs +++ b/xso-proc/src/compound.rs @@ -16,8 +16,8 @@ use crate::meta::NamespaceRef; use crate::scope::{mangle_member, AsItemsScope, FromEventsScope}; use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State}; use crate::types::{ - default_fn, feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty, ref_ty, - unknown_attribute_policy_path, + default_fn, discard_builder_ty, feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty, + ref_ty, unknown_attribute_policy_path, unknown_child_policy_path, }; fn resolve_policy(policy: Option, mut enum_ref: Path) -> Expr { @@ -52,6 +52,9 @@ pub(crate) struct Compound { /// Policy defining how to handle unknown attributes. unknown_attribute_policy: Expr, + + /// Policy defining how to handle unknown children. + unknown_child_policy: Expr, } impl Compound { @@ -59,11 +62,16 @@ impl Compound { pub(crate) fn from_field_defs>>( compound_fields: I, unknown_attribute_policy: Option, + unknown_child_policy: Option, ) -> Result { let unknown_attribute_policy = resolve_policy( unknown_attribute_policy, unknown_attribute_policy_path(Span::call_site()), ); + let unknown_child_policy = resolve_policy( + unknown_child_policy, + unknown_child_policy_path(Span::call_site()), + ); let compound_fields = compound_fields.into_iter(); let size_hint = compound_fields.size_hint(); let mut fields = Vec::with_capacity(size_hint.1.unwrap_or(size_hint.0)); @@ -91,6 +99,7 @@ impl Compound { Ok(Self { fields, unknown_attribute_policy, + unknown_child_policy, }) } @@ -99,6 +108,7 @@ impl Compound { compound_fields: &Fields, container_namespace: &NamespaceRef, unknown_attribute_policy: Option, + unknown_child_policy: Option, ) -> Result { Self::from_field_defs( compound_fields.iter().enumerate().map(|(i, field)| { @@ -116,6 +126,7 @@ impl Compound { FieldDef::from_field(field, index, container_namespace) }), unknown_attribute_policy, + unknown_child_policy, ) } @@ -141,6 +152,7 @@ impl Compound { } = scope; let default_state_ident = quote::format_ident!("{}Default", state_prefix); + let discard_state_ident = quote::format_ident!("{}Discard", state_prefix); let builder_data_ty: Type = TypePath { qself: None, path: quote::format_ident!("{}Data{}", state_ty_ident, state_prefix).into(), @@ -334,6 +346,7 @@ impl Compound { let unknown_attr_err = format!("Unknown attribute in {}.", output_name); let unknown_child_err = format!("Unknown child in {}.", output_name); + let unknown_child_policy = &self.unknown_child_policy; let output_cons = match output_name { ParentRef::Named(ref path) => { @@ -348,14 +361,43 @@ impl Compound { } }; + let discard_builder_ty = discard_builder_ty(Span::call_site()); + let discard_feed = feed_fn(discard_builder_ty.clone()); let child_fallback = match fallback_child_matcher { Some((_, matcher)) => matcher, None => quote! { let _ = (name, attrs); - ::core::result::Result::Err(::xso::error::Error::Other(#unknown_child_err)) + let _: () = #unknown_child_policy.apply_policy(#unknown_child_err)?; + ::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#discard_state_ident { + #builder_data_ident, + #substate_data: #discard_builder_ty::new(), + })) }, }; + states.push(State::new_with_builder( + discard_state_ident.clone(), + &builder_data_ident, + &builder_data_ty, + ).with_field( + substate_data, + &discard_builder_ty, + ).with_mut(substate_data).with_impl(quote! { + match #discard_feed(&mut #substate_data, ev)? { + ::core::option::Option::Some(#substate_result) => { + ::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#default_state_ident { + #builder_data_ident, + })) + } + ::core::option::Option::None => { + ::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#discard_state_ident { + #builder_data_ident, + #substate_data, + })) + } + } + })); + states.push(State::new_with_builder( default_state_ident.clone(), builder_data_ident, diff --git a/xso-proc/src/enums.rs b/xso-proc/src/enums.rs index 7d646661dbce0d6984fc6f3b5a2f7b998641a4d6..54dcef9502ee32ae15802e09cc90c7ca45d0ce1b 100644 --- a/xso-proc/src/enums.rs +++ b/xso-proc/src/enums.rs @@ -47,6 +47,7 @@ impl NameVariant { builder, iterator, on_unknown_attribute, + on_unknown_child, transparent, } = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?; @@ -64,7 +65,12 @@ impl NameVariant { Ok(Self { name, ident: decl.ident.clone(), - inner: Compound::from_fields(&decl.fields, enum_namespace, on_unknown_attribute)?, + inner: Compound::from_fields( + &decl.fields, + enum_namespace, + on_unknown_attribute, + on_unknown_child, + )?, }) } @@ -267,6 +273,7 @@ impl DynamicVariant { ref builder, ref iterator, on_unknown_attribute: _, // used by StructInner + on_unknown_child: _, // used by StructInner transparent: _, // used by StructInner } = meta; @@ -382,6 +389,7 @@ impl EnumInner { builder, iterator, on_unknown_attribute, + on_unknown_child, transparent, } = meta; @@ -395,6 +403,7 @@ impl EnumInner { reject_key!(name not on "enums" only on "their variants"); reject_key!(transparent flag not on "enums" only on "structs"); reject_key!(on_unknown_attribute not on "enums" only on "enum variants and structs"); + reject_key!(on_unknown_child not on "enums" only on "enum variants and structs"); if let Some(namespace) = namespace { Ok(Self::NameSwitched(NameSwitchedEnum::new( diff --git a/xso-proc/src/field/mod.rs b/xso-proc/src/field/mod.rs index 4074ef85c8ad1c6790dd7e406bbb62edeee78b7f..744366dea1fa9f66fa996c58ae7f0e47912fe3d9 100644 --- a/xso-proc/src/field/mod.rs +++ b/xso-proc/src/field/mod.rs @@ -366,7 +366,7 @@ fn new_field( &xml_namespace, )); } - let parts = Compound::from_field_defs(field_defs, None)?; + let parts = Compound::from_field_defs(field_defs, None, None)?; Ok(Box::new(ChildField { default_, diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 5889c9b6d26cd1a4fb46f5648cf59d6398b2fcde..ecb3ef00386fac9dc4f79e93af81cf1a7e30444a 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -353,6 +353,10 @@ pub(crate) struct XmlCompoundMeta { /// any. pub(crate) on_unknown_attribute: Option, + /// The value assigned to `on_unknown_child` inside `#[xml(..)]`, if + /// any. + pub(crate) on_unknown_child: Option, + /// The exhaustive flag. pub(crate) exhaustive: Flag, @@ -370,6 +374,7 @@ impl XmlCompoundMeta { let mut builder = None; let mut iterator = None; let mut on_unknown_attribute = None; + let mut on_unknown_child = None; let mut debug = Flag::Absent; let mut exhaustive = Flag::Absent; let mut transparent = Flag::Absent; @@ -402,6 +407,15 @@ impl XmlCompoundMeta { } on_unknown_attribute = Some(meta.value()?.parse()?); Ok(()) + } else if meta.path.is_ident("on_unknown_child") { + if on_unknown_child.is_some() { + return Err(Error::new_spanned( + meta.path, + "duplicate `on_unknown_child` key", + )); + } + on_unknown_child = Some(meta.value()?.parse()?); + Ok(()) } else if meta.path.is_ident("exhaustive") { if exhaustive.is_set() { return Err(Error::new_spanned(meta.path, "duplicate `exhaustive` key")); @@ -429,6 +443,7 @@ impl XmlCompoundMeta { builder, iterator, on_unknown_attribute, + on_unknown_child, exhaustive, transparent, }) diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index b2587b48a72610244829c26ba075d4e563310668..e6ce621cd077fbf6956922b64b7d9cf55c771db0 100644 --- a/xso-proc/src/structs.rs +++ b/xso-proc/src/structs.rs @@ -70,6 +70,7 @@ impl StructInner { builder, iterator, on_unknown_attribute, + on_unknown_child, transparent, } = meta; @@ -86,6 +87,7 @@ impl StructInner { reject_key!(namespace not on "transparent structs"); reject_key!(name not on "transparent structs"); reject_key!(on_unknown_attribute not on "transparent structs"); + reject_key!(on_unknown_child not on "transparent structs"); let fields_span = fields.span(); let fields = match fields { @@ -145,7 +147,12 @@ impl StructInner { }; Ok(Self::Compound { - inner: Compound::from_fields(fields, &xml_namespace, on_unknown_attribute)?, + inner: Compound::from_fields( + fields, + &xml_namespace, + on_unknown_attribute, + on_unknown_child, + )?, xml_namespace, xml_name, }) diff --git a/xso-proc/src/types.rs b/xso-proc/src/types.rs index 5588adff3abf3d40fa5894f99b67bee1f7416ce2..2392a22c11ce9018cc2d1f82868965f4354bdb22 100644 --- a/xso-proc/src/types.rs +++ b/xso-proc/src/types.rs @@ -841,3 +841,52 @@ pub(crate) fn unknown_attribute_policy_path(span: Span) -> Path { .collect(), } } + +/// Construct a [`syn::Path`] referring to `::xso::UnknownChildPolicy`. +pub(crate) fn unknown_child_policy_path(span: 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("UnknownChildPolicy", span), + arguments: PathArguments::None, + }, + ] + .into_iter() + .collect(), + } +} + +/// Construct a [`syn::Type`] referring to `::xso::fromxml::Discard`. +pub(crate) fn discard_builder_ty(span: Span) -> Type { + Type::Path(TypePath { + qself: None, + 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, + }, + PathSegment { + ident: Ident::new("Discard", span), + arguments: PathArguments::None, + }, + ] + .into_iter() + .collect(), + }, + }) +} diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index ef2e430034116cd91f9bedc192f79dddd8950c1a..9b56a40854c49a1b4f58ffe5faedec1982031eea 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -69,6 +69,7 @@ The following keys are defined on structs: | `builder` | optional *ident* | The name to use for the generated builder type. | | `iterator` | optional *ident* | The name to use for the generated iterator type. | | `on_unknown_attribute` | *identifier* | Name of an [`UnknownAttributePolicy`] member, controlling how unknown attributes are handled. | +| `on_unknown_child` | *identifier* | Name of an [`UnknownChildPolicy`] member, controlling how unknown children are handled. | Note that the `name` value must be a valid XML element name, without colons. The namespace prefix, if any, is assigned automatically at serialisation time @@ -149,6 +150,7 @@ documentation above. | --- | --- | --- | | `name` | *string literal* or *path* | The XML element name to match for this variant. If it is a *path*, it must point at a `&'static NcNameStr`. | | `on_unknown_attribute` | *identifier* | Name of an [`UnknownAttributePolicy`] member, controlling how unknown attributes are handled. | +| `on_unknown_child` | *identifier* | Name of an [`UnknownChildPolicy`] member, controlling how unknown children are handled. | Note that the `name` value must be a valid XML element name, without colons. The namespace prefix, if any, is assigned automatically at serialisation time diff --git a/xso/src/fromxml.rs b/xso/src/fromxml.rs index 1b980661b0f21a2c998f3546617597ef8c4ea84a..366bb1c82d201dd49f24cb396fa981df4eff8ee2 100644 --- a/xso/src/fromxml.rs +++ b/xso/src/fromxml.rs @@ -229,6 +229,44 @@ impl> FromXml for Result { } } +/// Builder which discards an entire child tree without inspecting the +/// contents. +#[derive(Debug)] +pub struct Discard { + depth: usize, +} + +impl Discard { + /// Create a new discarding builder. + pub fn new() -> Self { + Self { depth: 0 } + } +} + +impl FromEventsBuilder for Discard { + type Output = (); + + fn feed(&mut self, ev: rxml::Event) -> Result, Error> { + match ev { + rxml::Event::StartElement(..) => { + self.depth = match self.depth.checked_add(1) { + Some(v) => v, + None => return Err(Error::Other("maximum XML nesting depth exceeded")), + }; + Ok(None) + } + rxml::Event::EndElement(..) => match self.depth.checked_sub(1) { + None => Ok(Some(())), + Some(v) => { + self.depth = v; + Ok(None) + } + }, + _ => Ok(None), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/xso/src/lib.rs b/xso/src/lib.rs index ad4f443a1ee70f6fc74af95e775f98a5b8a1649d..79c7d1c6a87bbd3bb5a9319f736afa34717fe62f 100644 --- a/xso/src/lib.rs +++ b/xso/src/lib.rs @@ -323,6 +323,38 @@ impl UnknownAttributePolicy { } } +/// Control how unknown children are handled. +/// +/// The variants of this enum are referenced in the +/// `#[xml(on_unknown_child = ..)]` which can be used on structs and +/// enum variants. The specified variant controls how children, which are not +/// handled by any member of the compound, are handled during parsing. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] +pub enum UnknownChildPolicy { + /// All unknown children are discarded. + Discard, + + /// The first unknown child which is encountered generates a fatal + /// parsing error. + /// + /// This is the default policy. + #[default] + Fail, +} + +impl UnknownChildPolicy { + #[doc(hidden)] + /// Implementation of the policy. + /// + /// This is an internal API and not subject to semver versioning. + pub fn apply_policy(&self, msg: &'static str) -> Result<(), self::error::Error> { + match self { + Self::Fail => Err(self::error::Error::Other(msg)), + Self::Discard => Ok(()), + } + } +} + /// Attempt to transform a type implementing [`AsXml`] into another /// type which implements [`FromXml`]. pub fn transform(from: F) -> Result {