From c0ea48f22fe17975df41e056f06f21cb9fb7a936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Mon, 1 Apr 2024 13:11:20 +0200 Subject: [PATCH] parsers-macros: add options to override behavior for unknown things --- parsers-core/src/lib.rs | 79 +++++++++++++++++++++++++++++ parsers-macros/src/compound.rs | 82 +++++++++++++++++++++++++++++-- parsers-macros/src/enums.rs | 12 ++++- parsers-macros/src/field/child.rs | 10 ++-- parsers-macros/src/meta.rs | 16 ++++++ parsers-macros/src/structs.rs | 18 +++++-- parsers/src/macro_tests/mod.rs | 48 ++++++++++++++++++ 7 files changed, 253 insertions(+), 12 deletions(-) diff --git a/parsers-core/src/lib.rs b/parsers-core/src/lib.rs index 534e684f..1ad2e53b 100644 --- a/parsers-core/src/lib.rs +++ b/parsers-core/src/lib.rs @@ -113,6 +113,20 @@ preserved, if the container preserves sort order on insertion. `validate` is allowed and will be called. +- `on_unknown_child = ..` may be set to the identifier of a member of the + [`UnknownChildPolicy`] enum (i.e. for example `on_unknown_child = Ignore`). + This configures the behavior when unknown child elements are encountered. + See [`UnknownChildPolicy`] for details. + + Has no effect on grandchildren. + +- `on_unknown_attribute = ..` may be set to the identifier of a member of the + [`UnknownAttributePolicy`] enum (i.e. for example + `on_unknown_attribute = Ignore`). This configures the behavior when unknown + attributes are encountered. See [`UnknownAttributePolicy`] for details. + + Has no effect on children. + ## Enums Enums come in multiple flavors. All flavors have the following attributes: @@ -680,3 +694,68 @@ pub trait DynNamespace { /// fields referring to child structs with `namespace = super`. fn set_namespace>(&mut self, ns: T); } + +/// Configure how unknown child elements are handled. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum UnknownChildPolicy { + /// Fail parsing with a fatal error. + #[cfg_attr(not(feature = "disable-validation"), default)] + Fail, + + /// Ignore and discard any unknown child elements. + #[cfg_attr(feature = "disable-validation", default)] + Ignore, +} + +impl UnknownChildPolicy { + // TODO: once we have a more suitable error message structure, we should + // not pass the entire message down to this function. + /// Return a result describing the result of triggering the child policy. + /// + /// In other words, this returns an error iff the policy is set to + /// `Fail`. + /// + /// **Note:** This function is not to be considered part of the public + /// API! It is only marked `pub` because it needs to be called from + /// macro-generated code! Its signature and behavior may change without + /// notice and without major version bump at any time. + pub fn trigger(&self, msg: &'static str) -> Result<(), error::Error> { + match self { + Self::Fail => Err(error::Error::ParseError(msg)), + Self::Ignore => Ok(()), + } + } +} + +/// Configure how unknown attributes are handled. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum UnknownAttributePolicy { + /// Fail parsing with a fatal error. + #[cfg_attr(not(feature = "disable-validation"), default)] + Fail, + + /// Ignore and discard any unknown attributes. + #[cfg_attr(feature = "disable-validation", default)] + Ignore, +} + +impl UnknownAttributePolicy { + // TODO: once we have a more suitable error message structure, we should + // not pass the entire message down to this function. + /// Return a result describing the result of triggering the attribute + /// policy. + /// + /// In other words, this returns an error iff the policy is set to + /// `Fail`. + /// + /// **Note:** This function is not to be considered part of the public + /// API! It is only marked `pub` because it needs to be called from + /// macro-generated code! Its signature and behavior may change without + /// notice and without major version bump at any time. + pub fn trigger(&self, msg: &'static str) -> Result<(), error::Error> { + match self { + Self::Fail => Err(error::Error::ParseError(msg)), + Self::Ignore => Ok(()), + } + } +} diff --git a/parsers-macros/src/compound.rs b/parsers-macros/src/compound.rs index 583a4cae..46d0a179 100644 --- a/parsers-macros/src/compound.rs +++ b/parsers-macros/src/compound.rs @@ -13,6 +13,58 @@ use syn::{spanned::Spanned, *}; use crate::error_message::ParentRef; use crate::field::{FieldDef, FieldParsePart}; +/// Expand the given identifier to resolve as member of the +/// `UnknownChildPolicy` enum. If the identifier is absent, invoke +/// `UnknownChildPolicy::default()` instead. +fn default_on_unknown_child(p: Option) -> Expr { + match p { + Some(v) => Expr::Path(ExprPath { + attrs: Vec::new(), + qself: None, + path: Path { + leading_colon: Some(token::PathSep { + spans: [Span::call_site(), Span::call_site()], + }), + segments: [ + PathSegment::from(Ident::new("xmpp_parsers_core", Span::call_site())), + PathSegment::from(Ident::new("UnknownChildPolicy", Span::call_site())), + PathSegment::from(v), + ] + .into_iter() + .collect(), + }, + }), + None => syn::parse2(quote! { ::xmpp_parsers_core::UnknownChildPolicy::default() }) + .expect("failed to construct default unknown child policy"), + } +} + +/// Expand the given identifier to resolve as member of the +/// `UnknownAttributePolicy` enum. If the identifier is absent, invoke +/// `UnknownAttributePolicy::default()` instead. +fn default_on_unknown_attribute(p: Option) -> Expr { + match p { + Some(v) => Expr::Path(ExprPath { + attrs: Vec::new(), + qself: None, + path: Path { + leading_colon: Some(token::PathSep { + spans: [Span::call_site(), Span::call_site()], + }), + segments: [ + PathSegment::from(Ident::new("xmpp_parsers_core", Span::call_site())), + PathSegment::from(Ident::new("UnknownAttributePolicy", Span::call_site())), + PathSegment::from(v), + ] + .into_iter() + .collect(), + }, + }), + None => syn::parse2(quote! { ::xmpp_parsers_core::UnknownAttributePolicy::default() }) + .expect("failed to construct default unknown attribute policy"), + } +} + /// An struct or enum variant's contents. /// /// This struct is used to generate the parsing/serialisation loop, but most @@ -26,11 +78,23 @@ pub(crate) struct Compound { /// Information about the field annotated with `#[xml(namespace)]`, if /// any. namespace_field: Option<(Span, Type, Member)>, + + /// Member of the `UnknownChildPolicy` enum to use when handling unknown + /// children. + on_unknown_child: Expr, + + /// Member of the `UnknownAttributePolicy` enum to use when handling + /// unknown attributes. + on_unknown_attribute: Expr, } impl Compound { /// Construct a new compound from an iterator of [`FieldDef`] structs. - pub(crate) fn new>>(input: T) -> Result { + pub(crate) fn new>>( + on_unknown_child: Option, + on_unknown_attribute: Option, + input: T, + ) -> Result { let mut fields = Vec::with_capacity(input.size_hint().1.unwrap_or(0)); let mut text_field: Option = None; let mut namespace_field: Option<(Span, Type, Member)> = None; @@ -74,6 +138,8 @@ impl Compound { Ok(Self { fields, namespace_field, + on_unknown_child: default_on_unknown_child(on_unknown_child), + on_unknown_attribute: default_on_unknown_attribute(on_unknown_attribute), }) } @@ -81,8 +147,14 @@ impl Compound { /// /// This a convenience wrapper around [`Self::new`], converting the /// [`syn::Field`] structs to [`FieldDef`]. - pub(crate) fn from_fields(fields: &Fields) -> Result { + pub(crate) fn from_fields( + on_unknown_child: Option, + on_unknown_attribute: Option, + fields: &Fields, + ) -> Result { Self::new( + on_unknown_child, + on_unknown_attribute, fields.iter().enumerate().map(|(i, field)| { FieldDef::from_field(field, i.try_into().expect("too many fields")) }), @@ -130,6 +202,8 @@ impl Compound { forgive_attributes: &[&str], ) -> Result { let readable_name = container_name.to_string(); + let on_unknown_child = self.on_unknown_child; + let on_unknown_attribute = self.on_unknown_attribute; let mut init = quote! {}; let mut tupinit = quote! {}; @@ -143,7 +217,7 @@ impl Compound { let mut tempinit = quote! {}; let mut childiter = quote! {}; let mut childfallback = quote! { - return Err(::xmpp_parsers_core::error::Error::ParseError(concat!("Unknown child in ", #readable_name, "."))); + #on_unknown_child.trigger(concat!("Unknown child in ", #readable_name, "."))?; }; let mut had_fallback: bool = false; @@ -196,7 +270,7 @@ impl Compound { { for (key, _) in #residual.attrs() { #attrcheck - return Err(::xmpp_parsers_core::error::Error::ParseError(concat!("Unknown attribute in ", #readable_name, "."))); + #on_unknown_attribute.trigger(concat!("Unknown attribute in ", #readable_name, "."))?; } #tempinit diff --git a/parsers-macros/src/enums.rs b/parsers-macros/src/enums.rs index d35abaef..f7e3bd48 100644 --- a/parsers-macros/src/enums.rs +++ b/parsers-macros/src/enums.rs @@ -150,7 +150,11 @@ impl StrMatchedVariant { value: name, ident: variant.ident.clone(), fallback, - inner: Compound::from_fields(&variant.fields)?, + inner: Compound::from_fields( + meta.on_unknown_child, + meta.on_unknown_attribute, + &variant.fields, + )?, }) } @@ -179,7 +183,11 @@ impl StrMatchedVariant { value, ident: variant.ident.clone(), fallback, - inner: Compound::from_fields(&variant.fields)?, + inner: Compound::from_fields( + meta.on_unknown_child, + meta.on_unknown_attribute, + &variant.fields, + )?, }) } } diff --git a/parsers-macros/src/field/child.rs b/parsers-macros/src/field/child.rs index 73c2c642..074fa629 100644 --- a/parsers-macros/src/field/child.rs +++ b/parsers-macros/src/field/child.rs @@ -67,9 +67,13 @@ impl ExtractDef { if parts.len() != 1 { single_extract_type = None; } - let parts = Compound::new(parts.into_iter().enumerate().map(|(i, x)| { - FieldDef::from_extract(span.clone(), *x, i as u32, single_extract_type.take()) - }))?; + let parts = Compound::new( + None, + None, + parts.into_iter().enumerate().map(|(i, x)| { + FieldDef::from_extract(span.clone(), *x, i as u32, single_extract_type.take()) + }), + )?; Ok(Self { namespace, name, diff --git a/parsers-macros/src/meta.rs b/parsers-macros/src/meta.rs index 73d34416..ed089d21 100644 --- a/parsers-macros/src/meta.rs +++ b/parsers-macros/src/meta.rs @@ -502,6 +502,14 @@ pub(crate) struct XmlCompoundMeta { /// The options set inside `element`, if any. pub(crate) element: Option, + + /// Member of the `UnknownChildPolicy` enum to use when handling unknown + /// children. + pub(crate) on_unknown_child: Option, + + /// Member of the `UnknownAttributePolicy` enum to use when handling + /// unknown attributes. + pub(crate) on_unknown_attribute: Option, } impl XmlCompoundMeta { @@ -520,6 +528,8 @@ impl XmlCompoundMeta { normalize_with: None, debug: Flag::Absent, element: None, + on_unknown_attribute: None, + on_unknown_child: None, } } @@ -540,6 +550,8 @@ impl XmlCompoundMeta { let mut prepare: Option = None; let mut normalize_with: Option = None; let mut element: Option = None; + let mut on_unknown_attribute: Option = None; + let mut on_unknown_child: Option = None; attr.parse_nested_meta(|meta| { parse_meta!( @@ -557,6 +569,8 @@ impl XmlCompoundMeta { ParseValue("normalize_with", &mut normalize_with), ParseValue("normalise_with", &mut normalize_with), ParseNodeFilter("element", &mut element), + ParseValue("on_unknown_attribute", &mut on_unknown_attribute), + ParseValue("on_unknown_child", &mut on_unknown_child), ) })?; @@ -582,6 +596,8 @@ impl XmlCompoundMeta { debug, normalize_with, element, + on_unknown_child, + on_unknown_attribute, }) } diff --git a/parsers-macros/src/structs.rs b/parsers-macros/src/structs.rs index 3f39f2a1..72395f73 100644 --- a/parsers-macros/src/structs.rs +++ b/parsers-macros/src/structs.rs @@ -241,7 +241,13 @@ impl StructInner { )); }; - Self::new_compound(namespace, name, fields) + Self::new_compound( + namespace, + name, + meta.on_unknown_child, + meta.on_unknown_attribute, + fields, + ) } } @@ -343,8 +349,14 @@ impl StructInner { /// Construct a new compound-based struct with the given namespace, name /// and fields. - fn new_compound(namespace: NamespaceRef, name: NameRef, fields: &Fields) -> Result { - let inner = Compound::from_fields(fields)?; + fn new_compound( + namespace: NamespaceRef, + name: NameRef, + on_unknown_child: Option, + on_unknown_attribute: Option, + fields: &Fields, + ) -> Result { + let inner = Compound::from_fields(on_unknown_child, on_unknown_attribute, fields)?; let namespace_field = inner.namespace_field(); let namespace = match namespace { diff --git a/parsers/src/macro_tests/mod.rs b/parsers/src/macro_tests/mod.rs index f7bafdf4..ca70bc96 100644 --- a/parsers/src/macro_tests/mod.rs +++ b/parsers/src/macro_tests/mod.rs @@ -1073,3 +1073,51 @@ fn nested_dyn_namespace_serializes_inner_with_correct_namespace() { assert_eq!(v2.namespace, NamespaceEnum::Ns2); assert_eq!(v2.inner.namespace, NamespaceEnum::Ns2); } + +#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)] +#[xml(namespace = self::TEST_NS1, name = "ignore-unknown-children", on_unknown_child = Ignore)] +pub struct IgnoreUnknownChildren; + +#[test] +fn ignore_unknown_children_ignores_children() { + match crate::util::test::parse_str::( + "", + ) { + Ok(_) => (), + other => panic!("unexpected result: {:?}", other), + } +} + +#[test] +fn ignore_unknown_children_rejects_attributes() { + match crate::util::test::parse_str::( + "", + ) { + Err(Error::ParseError(msg)) if msg.find("Unknown attribute").is_some() => (), + other => panic!("unexpected result: {:?}", other), + } +} + +#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)] +#[xml(namespace = self::TEST_NS1, name = "ignore-unknown-attributes", on_unknown_attribute = Ignore)] +pub struct IgnoreUnknownAttributes; + +#[test] +fn ignore_unknown_attributes_ignores_attributes() { + match crate::util::test::parse_str::( + "", + ) { + Ok(_) => (), + other => panic!("unexpected result: {:?}", other), + } +} + +#[test] +fn ignore_unknown_attributes_rejects_children() { + match crate::util::test::parse_str::( + "", + ) { + Err(Error::ParseError(msg)) if msg.find("Unknown child").is_some() => (), + other => panic!("unexpected result: {:?}", other), + } +}