xso: add support for ignoring unknown children
This commit is contained in:
parent
290460ba9d
commit
66233b0150
10 changed files with 242 additions and 6 deletions
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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_,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue