diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 62ef9430..4a5fd523 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 1cae55c6..e9b9be12 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 7d646661..54dcef95 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 4074ef85..744366de 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 5889c9b6..ecb3ef00 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 b2587b48..e6ce621c 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 5588adff..2392a22c 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 ef2e4300..9b56a408 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 1b980661..366bb1c8 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 ad4f443a..79c7d1c6 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 {