From 290460ba9de7ca4e5d7972212c3186f3fe1f762a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Thu, 3 Oct 2024 12:29:10 +0200 Subject: [PATCH] xso: add support for ignoring unknown attributes --- 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(-) diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index ae10c866faadb15ed8eb0c38e0f5d5af518076b8..62ef9430f016759a06b803a644ff586b2de54525 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -1779,3 +1779,45 @@ fn extract_tuple_to_map_roundtrip() { "hello worldhallo welt", ); } + +#[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::(""); +} + +#[test] +fn ignore_unknown_attributes_positive() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + match parse_str::("") { + 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::("") { + Err(xso::error::FromElementError::Invalid(xso::error::Error::Other(e))) => { + assert_eq!(e, "Unknown child in IgnoreUnknownAttributes element."); + } + other => panic!("unexpected result: {:?}", other), + } +} diff --git a/xso-proc/src/compound.rs b/xso-proc/src/compound.rs index 6287a340b580d75c3d65c88ab2728b6d5f837b8f..1cae55c691bef6227c385604f4a50d120099b995 100644 --- a/xso-proc/src/compound.rs +++ b/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, 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, + + /// 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>>( compound_fields: I, + unknown_attribute_policy: Option, ) -> Result { + 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, ) -> Result { - 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 }) }, diff --git a/xso-proc/src/enums.rs b/xso-proc/src/enums.rs index ddd9af16e92dc59c3263171d536e0f41d127ee32..7d646661dbce0d6984fc6f3b5a2f7b998641a4d6 100644 --- a/xso-proc/src/enums.rs +++ b/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( diff --git a/xso-proc/src/field/mod.rs b/xso-proc/src/field/mod.rs index e9d995b37f8f217270523578b5fad37df49a1baa..4074ef85c8ad1c6790dd7e406bbb62edeee78b7f 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)?; + let parts = Compound::from_field_defs(field_defs, None)?; Ok(Box::new(ChildField { default_, diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index acbde83e546631b4363dff45473cc0f9ef1d7391..5889c9b6d26cd1a4fb46f5648cf59d6398b2fcde 100644 --- a/xso-proc/src/meta.rs +++ b/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, + /// The value assigned to `on_unknown_attribute` inside `#[xml(..)]`, if + /// any. + pub(crate) on_unknown_attribute: Option, + /// 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, }) diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index ef49a7f8786e3ddf37537f7d48e9ea663d59ad77..b2587b48a72610244829c26ba075d4e563310668 100644 --- a/xso-proc/src/structs.rs +++ b/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, }) diff --git a/xso-proc/src/types.rs b/xso-proc/src/types.rs index cd74b133acd64f6454acc9bb266e6496d2e9d08b..5588adff3abf3d40fa5894f99b67bee1f7416ce2 100644 --- a/xso-proc/src/types.rs +++ b/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(), + } +} diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index 77aa3dc38038bb54ec0d880a91c561bfec0e74a5..ef2e430034116cd91f9bedc192f79dddd8950c1a 100644 --- a/xso/src/from_xml_doc.md +++ b/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 diff --git a/xso/src/lib.rs b/xso/src/lib.rs index 1e2a2eacfad089f4a851bb2b5ac440c35418f175..ad4f443a1ee70f6fc74af95e775f98a5b8a1649d 100644 --- a/xso/src/lib.rs +++ b/xso/src/lib.rs @@ -291,6 +291,38 @@ impl AsOptionalXmlText for Option { } } +/// 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(from: F) -> Result {