xso: add support for ignoring unknown children

Jonas SchΓ€fer created

Change summary

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(-)

Detailed changes

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::<IgnoreUnknownChildren>("<foo xmlns='urn:example:ns1'/>");
+}
+
+#[test]
+fn ignore_unknown_children_positive() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<IgnoreUnknownChildren>("<foo xmlns='urn:example:ns1'><coucou/></foo>") {
+        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::<IgnoreUnknownChildren>("<foo xmlns='urn:example:ns1' fnord='bar'/>") {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => {
+            assert_eq!(e, "Unknown attribute in IgnoreUnknownChildren element.");
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}

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<Ident>, 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<I: IntoIterator<Item = Result<FieldDef>>>(
         compound_fields: I,
         unknown_attribute_policy: Option<Ident>,
+        unknown_child_policy: Option<Ident>,
     ) -> Result<Self> {
         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<Ident>,
+        unknown_child_policy: Option<Ident>,
     ) -> Result<Self> {
         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,

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(

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_,

xso-proc/src/meta.rs πŸ”—

@@ -353,6 +353,10 @@ pub(crate) struct XmlCompoundMeta {
     /// any.
     pub(crate) on_unknown_attribute: Option<Ident>,
 
+    /// The value assigned to `on_unknown_child` inside `#[xml(..)]`, if
+    /// any.
+    pub(crate) on_unknown_child: Option<Ident>,
+
     /// 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,
         })

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

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

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

xso/src/fromxml.rs πŸ”—

@@ -229,6 +229,44 @@ impl<T: FromXml, E: From<Error>> FromXml for Result<T, E> {
     }
 }
 
+/// 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<Option<Self::Output>, 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::*;

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<T: FromXml, F: AsXml>(from: F) -> Result<T, self::error::Error> {