xso_proc: add options to override behavior for unknown things

This commit is contained in:
Jonas Schäfer 2024-04-01 13:11:20 +02:00
parent 970a48e07b
commit 457ae7ea69
7 changed files with 253 additions and 12 deletions

View File

@ -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::<IgnoreUnknownChildren>(
"<ignore-unknown-children xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'><foo/></ignore-unknown-children>",
) {
Ok(_) => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn ignore_unknown_children_rejects_attributes() {
match crate::util::test::parse_str::<IgnoreUnknownChildren>(
"<ignore-unknown-children xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' foo='bar'/>",
) {
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::<IgnoreUnknownAttributes>(
"<ignore-unknown-attributes xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' foo='bar'/>",
) {
Ok(_) => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn ignore_unknown_attributes_rejects_children() {
match crate::util::test::parse_str::<IgnoreUnknownAttributes>(
"<ignore-unknown-attributes xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'><foo/></ignore-unknown-attributes>",
) {
Err(Error::ParseError(msg)) if msg.find("Unknown child").is_some() => (),
other => panic!("unexpected result: {:?}", other),
}
}

View File

@ -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<Ident>) -> 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("xso", Span::call_site())),
PathSegment::from(Ident::new("UnknownChildPolicy", Span::call_site())),
PathSegment::from(v),
]
.into_iter()
.collect(),
},
}),
None => syn::parse2(quote! { ::xso::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<Ident>) -> 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("xso", Span::call_site())),
PathSegment::from(Ident::new("UnknownAttributePolicy", Span::call_site())),
PathSegment::from(v),
]
.into_iter()
.collect(),
},
}),
None => syn::parse2(quote! { ::xso::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<T: Iterator<Item = Result<FieldDef>>>(input: T) -> Result<Self> {
pub(crate) fn new<T: Iterator<Item = Result<FieldDef>>>(
on_unknown_child: Option<Ident>,
on_unknown_attribute: Option<Ident>,
input: T,
) -> Result<Self> {
let mut fields = Vec::with_capacity(input.size_hint().1.unwrap_or(0));
let mut text_field: Option<Span> = 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<Self> {
pub(crate) fn from_fields(
on_unknown_child: Option<Ident>,
on_unknown_attribute: Option<Ident>,
fields: &Fields,
) -> Result<Self> {
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<TokenStream> {
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(::xso::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(::xso::error::Error::ParseError(concat!("Unknown attribute in ", #readable_name, ".")));
#on_unknown_attribute.trigger(concat!("Unknown attribute in ", #readable_name, "."))?;
}
#tempinit

View File

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

View File

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

View File

@ -502,6 +502,14 @@ pub(crate) struct XmlCompoundMeta {
/// The options set inside `element`, if any.
pub(crate) element: Option<NodeFilterMeta>,
/// Member of the `UnknownChildPolicy` enum to use when handling unknown
/// children.
pub(crate) on_unknown_child: Option<Ident>,
/// Member of the `UnknownAttributePolicy` enum to use when handling
/// unknown attributes.
pub(crate) on_unknown_attribute: Option<Ident>,
}
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<Path> = None;
let mut normalize_with: Option<Path> = None;
let mut element: Option<NodeFilterMeta> = None;
let mut on_unknown_attribute: Option<Ident> = None;
let mut on_unknown_child: Option<Ident> = 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,
})
}

View File

@ -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<Self> {
let inner = Compound::from_fields(fields)?;
fn new_compound(
namespace: NamespaceRef,
name: NameRef,
on_unknown_child: Option<Ident>,
on_unknown_attribute: Option<Ident>,
fields: &Fields,
) -> Result<Self> {
let inner = Compound::from_fields(on_unknown_child, on_unknown_attribute, fields)?;
let namespace_field = inner.namespace_field();
let namespace = match namespace {

View File

@ -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<T: Into<Self::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(()),
}
}
}