xso: add support for ignoring unknown attributes

Jonas SchΓ€fer created

Change summary

parsers/src/util/macro_tests.rs | 42 ++++++++++++++++++
xso-proc/src/compound.rs        | 81 ++++++++++++++++++++++++++--------
xso-proc/src/enums.rs           |  8 ++
xso-proc/src/field/mod.rs       |  2 
xso-proc/src/meta.rs            | 15 ++++++
xso-proc/src/structs.rs         |  4 +
xso-proc/src/types.rs           | 21 +++++++++
xso/src/from_xml_doc.md         |  3 +
xso/src/lib.rs                  | 32 +++++++++++++
9 files changed, 185 insertions(+), 23 deletions(-)

Detailed changes

parsers/src/util/macro_tests.rs πŸ”—

@@ -1779,3 +1779,45 @@ fn extract_tuple_to_map_roundtrip() {
         "<parent xmlns='urn:example:ns1'><text>hello world</text><text xml:lang='de'>hallo welt</text></parent>",
     );
 }
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "foo", on_unknown_attribute = Discard)]
+struct IgnoreUnknownAttributes;
+
+#[test]
+fn ignore_unknown_attributes_empty_roundtrip() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<IgnoreUnknownAttributes>("<foo xmlns='urn:example:ns1'/>");
+}
+
+#[test]
+fn ignore_unknown_attributes_positive() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<IgnoreUnknownAttributes>("<foo xmlns='urn:example:ns1' fnord='bar'/>") {
+        Ok(IgnoreUnknownAttributes) => (),
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
+#[test]
+fn ignore_unknown_attributes_negative_unexpected_child() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<IgnoreUnknownAttributes>("<foo xmlns='urn:example:ns1'><coucou/></foo>") {
+        Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => {
+            assert_eq!(e, "Unknown child in IgnoreUnknownAttributes element.");
+        }
+        other => panic!("unexpected result: {:?}", other),
+    }
+}

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

@@ -15,19 +15,55 @@ use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit,
 use crate::meta::NamespaceRef;
 use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
 use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State};
-use crate::types::{feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty, ref_ty};
+use crate::types::{
+    default_fn, feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty, ref_ty,
+    unknown_attribute_policy_path,
+};
+
+fn resolve_policy(policy: Option<Ident>, mut enum_ref: Path) -> Expr {
+    match policy {
+        Some(ident) => {
+            enum_ref.segments.push(ident.into());
+            Expr::Path(ExprPath {
+                attrs: Vec::new(),
+                qself: None,
+                path: enum_ref,
+            })
+        }
+        None => {
+            let default_fn = default_fn(Type::Path(TypePath {
+                qself: None,
+                path: enum_ref,
+            }));
+            Expr::Call(ExprCall {
+                attrs: Vec::new(),
+                func: Box::new(default_fn),
+                paren_token: token::Paren::default(),
+                args: punctuated::Punctuated::new(),
+            })
+        }
+    }
+}
 
 /// A struct or enum variant's contents.
 pub(crate) struct Compound {
     /// The fields of this compound.
     fields: Vec<FieldDef>,
+
+    /// Policy defining how to handle unknown attributes.
+    unknown_attribute_policy: Expr,
 }
 
 impl Compound {
     /// Construct a compound from processed field definitions.
     pub(crate) fn from_field_defs<I: IntoIterator<Item = Result<FieldDef>>>(
         compound_fields: I,
+        unknown_attribute_policy: Option<Ident>,
     ) -> Result<Self> {
+        let unknown_attribute_policy = resolve_policy(
+            unknown_attribute_policy,
+            unknown_attribute_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));
@@ -52,28 +88,35 @@ impl Compound {
 
             fields.push(field);
         }
-        Ok(Self { fields })
+        Ok(Self {
+            fields,
+            unknown_attribute_policy,
+        })
     }
 
     /// Construct a compound from fields.
     pub(crate) fn from_fields(
         compound_fields: &Fields,
         container_namespace: &NamespaceRef,
+        unknown_attribute_policy: Option<Ident>,
     ) -> Result<Self> {
-        Self::from_field_defs(compound_fields.iter().enumerate().map(|(i, field)| {
-            let index = match i.try_into() {
-                Ok(v) => v,
-                // we are converting to u32, are you crazy?!
-                // (u32, because syn::Member::Index needs that.)
-                Err(_) => {
-                    return Err(Error::new_spanned(
-                        field,
-                        "okay, mate, that are way too many fields. get your life together.",
-                    ))
-                }
-            };
-            FieldDef::from_field(field, index, container_namespace)
-        }))
+        Self::from_field_defs(
+            compound_fields.iter().enumerate().map(|(i, field)| {
+                let index = match i.try_into() {
+                    Ok(v) => v,
+                    // we are converting to u32, are you crazy?!
+                    // (u32, because syn::Member::Index needs that.)
+                    Err(_) => {
+                        return Err(Error::new_spanned(
+                            field,
+                            "okay, mate, that are way too many fields. get your life together.",
+                        ))
+                    }
+                };
+                FieldDef::from_field(field, index, container_namespace)
+            }),
+            unknown_attribute_policy,
+        )
     }
 
     /// Make and return a set of states which is used to construct the target
@@ -342,6 +385,8 @@ impl Compound {
             }
         }));
 
+        let unknown_attribute_policy = &self.unknown_attribute_policy;
+
         Ok(FromEventsSubmachine {
             defs: quote! {
                 #extra_defs
@@ -356,9 +401,7 @@ impl Compound {
                     #builder_data_init
                 };
                 if #attrs.len() > 0 {
-                    return ::core::result::Result::Err(::xso::error::Error::Other(
-                        #unknown_attr_err,
-                    ).into());
+                    let _: () = #unknown_attribute_policy.apply_policy(#unknown_attr_err)?;
                 }
                 ::core::result::Result::Ok(#state_ty_ident::#default_state_ident { #builder_data_ident })
             },

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

@@ -46,6 +46,7 @@ impl NameVariant {
             debug,
             builder,
             iterator,
+            on_unknown_attribute,
             transparent,
         } = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;
 
@@ -63,7 +64,7 @@ impl NameVariant {
         Ok(Self {
             name,
             ident: decl.ident.clone(),
-            inner: Compound::from_fields(&decl.fields, enum_namespace)?,
+            inner: Compound::from_fields(&decl.fields, enum_namespace, on_unknown_attribute)?,
         })
     }
 
@@ -265,7 +266,8 @@ impl DynamicVariant {
             ref debug,
             ref builder,
             ref iterator,
-            transparent: _, // used by StructInner
+            on_unknown_attribute: _, // used by StructInner
+            transparent: _,          // used by StructInner
         } = meta;
 
         reject_key!(debug flag not on "enum variants" only on "enums and structs");
@@ -379,6 +381,7 @@ impl EnumInner {
             debug,
             builder,
             iterator,
+            on_unknown_attribute,
             transparent,
         } = meta;
 
@@ -391,6 +394,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");
 
         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)?;
+            let parts = Compound::from_field_defs(field_defs, None)?;
 
             Ok(Box::new(ChildField {
                 default_,

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

@@ -349,6 +349,10 @@ pub(crate) struct XmlCompoundMeta {
     /// The value assigned to `iterator` inside `#[xml(..)]`, if any.
     pub(crate) iterator: Option<Ident>,
 
+    /// The value assigned to `on_unknown_attribute` inside `#[xml(..)]`, if
+    /// any.
+    pub(crate) on_unknown_attribute: Option<Ident>,
+
     /// The exhaustive flag.
     pub(crate) exhaustive: Flag,
 
@@ -365,6 +369,7 @@ impl XmlCompoundMeta {
         let mut qname = QNameRef::default();
         let mut builder = None;
         let mut iterator = None;
+        let mut on_unknown_attribute = None;
         let mut debug = Flag::Absent;
         let mut exhaustive = Flag::Absent;
         let mut transparent = Flag::Absent;
@@ -388,6 +393,15 @@ impl XmlCompoundMeta {
                 }
                 iterator = Some(meta.value()?.parse()?);
                 Ok(())
+            } else if meta.path.is_ident("on_unknown_attribute") {
+                if on_unknown_attribute.is_some() {
+                    return Err(Error::new_spanned(
+                        meta.path,
+                        "duplicate `on_unknown_attribute` key",
+                    ));
+                }
+                on_unknown_attribute = 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"));
@@ -414,6 +428,7 @@ impl XmlCompoundMeta {
             debug,
             builder,
             iterator,
+            on_unknown_attribute,
             exhaustive,
             transparent,
         })

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

@@ -69,6 +69,7 @@ impl StructInner {
             debug,
             builder,
             iterator,
+            on_unknown_attribute,
             transparent,
         } = meta;
 
@@ -84,6 +85,7 @@ impl StructInner {
         if let Flag::Present(_) = transparent {
             reject_key!(namespace not on "transparent structs");
             reject_key!(name not on "transparent structs");
+            reject_key!(on_unknown_attribute not on "transparent structs");
 
             let fields_span = fields.span();
             let fields = match fields {
@@ -143,7 +145,7 @@ impl StructInner {
             };
 
             Ok(Self::Compound {
-                inner: Compound::from_fields(fields, &xml_namespace)?,
+                inner: Compound::from_fields(fields, &xml_namespace, on_unknown_attribute)?,
                 xml_namespace,
                 xml_name,
             })

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

@@ -820,3 +820,24 @@ pub(crate) fn element_ty(span: Span) -> Type {
         },
     })
 }
+
+/// Construct a [`syn::Path`] referring to `::xso::UnknownAttributePolicy`.
+pub(crate) fn unknown_attribute_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("UnknownAttributePolicy", span),
+                arguments: PathArguments::None,
+            },
+        ]
+        .into_iter()
+        .collect(),
+    }
+}

xso/src/from_xml_doc.md πŸ”—

@@ -47,6 +47,7 @@ such:
 
 - *path*: A Rust path, like `some_crate::foo::Bar`. Note that `foo` on its own
   is also a path.
+- *identifier*: A single Rust identifier.
 - *string literal*: A string literal, like `"hello world!"`.
 - *type*: A Rust type.
 - *expression*: A Rust expression.
@@ -67,6 +68,7 @@ The following keys are defined on structs:
 | `transparent` | *flag* | If present, declares the struct as *transparent* struct (see below) |
 | `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. |
 
 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
@@ -146,6 +148,7 @@ documentation above.
 | Key | Value type | Description |
 | --- | --- | --- |
 | `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. |
 
 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/lib.rs πŸ”—

@@ -291,6 +291,38 @@ impl<T: AsXmlText> AsOptionalXmlText for Option<T> {
     }
 }
 
+/// Control how unknown attributes are handled.
+///
+/// The variants of this enum are referenced in the
+/// `#[xml(on_unknown_attribute = ..)]` which can be used on structs and
+/// enum variants. The specified variant controls how attributes, 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 UnknownAttributePolicy {
+    /// All unknown attributes are discarded.
+    Discard,
+
+    /// The first unknown attribute which is encountered generates a fatal
+    /// parsing error.
+    ///
+    /// This is the default policy.
+    #[default]
+    Fail,
+}
+
+impl UnknownAttributePolicy {
+    #[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> {