xso: add support for ignoring unknown attributes
This commit is contained in:
parent
74c81b6a37
commit
290460ba9d
9 changed files with 185 additions and 23 deletions
|
|
@ -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>",
|
"<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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,19 +15,55 @@ use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit,
|
||||||
use crate::meta::NamespaceRef;
|
use crate::meta::NamespaceRef;
|
||||||
use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
|
use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
|
||||||
use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State};
|
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.
|
/// A struct or enum variant's contents.
|
||||||
pub(crate) struct Compound {
|
pub(crate) struct Compound {
|
||||||
/// The fields of this compound.
|
/// The fields of this compound.
|
||||||
fields: Vec<FieldDef>,
|
fields: Vec<FieldDef>,
|
||||||
|
|
||||||
|
/// Policy defining how to handle unknown attributes.
|
||||||
|
unknown_attribute_policy: Expr,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Compound {
|
impl Compound {
|
||||||
/// Construct a compound from processed field definitions.
|
/// Construct a compound from processed field definitions.
|
||||||
pub(crate) fn from_field_defs<I: IntoIterator<Item = Result<FieldDef>>>(
|
pub(crate) fn from_field_defs<I: IntoIterator<Item = Result<FieldDef>>>(
|
||||||
compound_fields: I,
|
compound_fields: I,
|
||||||
|
unknown_attribute_policy: Option<Ident>,
|
||||||
) -> Result<Self> {
|
) -> 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 compound_fields = compound_fields.into_iter();
|
||||||
let size_hint = compound_fields.size_hint();
|
let size_hint = compound_fields.size_hint();
|
||||||
let mut fields = Vec::with_capacity(size_hint.1.unwrap_or(size_hint.0));
|
let mut fields = Vec::with_capacity(size_hint.1.unwrap_or(size_hint.0));
|
||||||
|
|
@ -52,28 +88,35 @@ impl Compound {
|
||||||
|
|
||||||
fields.push(field);
|
fields.push(field);
|
||||||
}
|
}
|
||||||
Ok(Self { fields })
|
Ok(Self {
|
||||||
|
fields,
|
||||||
|
unknown_attribute_policy,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a compound from fields.
|
/// Construct a compound from fields.
|
||||||
pub(crate) fn from_fields(
|
pub(crate) fn from_fields(
|
||||||
compound_fields: &Fields,
|
compound_fields: &Fields,
|
||||||
container_namespace: &NamespaceRef,
|
container_namespace: &NamespaceRef,
|
||||||
|
unknown_attribute_policy: Option<Ident>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
Self::from_field_defs(compound_fields.iter().enumerate().map(|(i, field)| {
|
Self::from_field_defs(
|
||||||
let index = match i.try_into() {
|
compound_fields.iter().enumerate().map(|(i, field)| {
|
||||||
Ok(v) => v,
|
let index = match i.try_into() {
|
||||||
// we are converting to u32, are you crazy?!
|
Ok(v) => v,
|
||||||
// (u32, because syn::Member::Index needs that.)
|
// we are converting to u32, are you crazy?!
|
||||||
Err(_) => {
|
// (u32, because syn::Member::Index needs that.)
|
||||||
return Err(Error::new_spanned(
|
Err(_) => {
|
||||||
field,
|
return Err(Error::new_spanned(
|
||||||
"okay, mate, that are way too many fields. get your life together.",
|
field,
|
||||||
))
|
"okay, mate, that are way too many fields. get your life together.",
|
||||||
}
|
))
|
||||||
};
|
}
|
||||||
FieldDef::from_field(field, index, container_namespace)
|
};
|
||||||
}))
|
FieldDef::from_field(field, index, container_namespace)
|
||||||
|
}),
|
||||||
|
unknown_attribute_policy,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make and return a set of states which is used to construct the target
|
/// 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 {
|
Ok(FromEventsSubmachine {
|
||||||
defs: quote! {
|
defs: quote! {
|
||||||
#extra_defs
|
#extra_defs
|
||||||
|
|
@ -356,9 +401,7 @@ impl Compound {
|
||||||
#builder_data_init
|
#builder_data_init
|
||||||
};
|
};
|
||||||
if #attrs.len() > 0 {
|
if #attrs.len() > 0 {
|
||||||
return ::core::result::Result::Err(::xso::error::Error::Other(
|
let _: () = #unknown_attribute_policy.apply_policy(#unknown_attr_err)?;
|
||||||
#unknown_attr_err,
|
|
||||||
).into());
|
|
||||||
}
|
}
|
||||||
::core::result::Result::Ok(#state_ty_ident::#default_state_ident { #builder_data_ident })
|
::core::result::Result::Ok(#state_ty_ident::#default_state_ident { #builder_data_ident })
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ impl NameVariant {
|
||||||
debug,
|
debug,
|
||||||
builder,
|
builder,
|
||||||
iterator,
|
iterator,
|
||||||
|
on_unknown_attribute,
|
||||||
transparent,
|
transparent,
|
||||||
} = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;
|
} = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;
|
||||||
|
|
||||||
|
|
@ -63,7 +64,7 @@ impl NameVariant {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
name,
|
name,
|
||||||
ident: decl.ident.clone(),
|
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 debug,
|
||||||
ref builder,
|
ref builder,
|
||||||
ref iterator,
|
ref iterator,
|
||||||
transparent: _, // used by StructInner
|
on_unknown_attribute: _, // used by StructInner
|
||||||
|
transparent: _, // used by StructInner
|
||||||
} = meta;
|
} = meta;
|
||||||
|
|
||||||
reject_key!(debug flag not on "enum variants" only on "enums and structs");
|
reject_key!(debug flag not on "enum variants" only on "enums and structs");
|
||||||
|
|
@ -379,6 +381,7 @@ impl EnumInner {
|
||||||
debug,
|
debug,
|
||||||
builder,
|
builder,
|
||||||
iterator,
|
iterator,
|
||||||
|
on_unknown_attribute,
|
||||||
transparent,
|
transparent,
|
||||||
} = meta;
|
} = meta;
|
||||||
|
|
||||||
|
|
@ -391,6 +394,7 @@ impl EnumInner {
|
||||||
|
|
||||||
reject_key!(name not on "enums" only on "their variants");
|
reject_key!(name not on "enums" only on "their variants");
|
||||||
reject_key!(transparent flag not on "enums" only on "structs");
|
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 {
|
if let Some(namespace) = namespace {
|
||||||
Ok(Self::NameSwitched(NameSwitchedEnum::new(
|
Ok(Self::NameSwitched(NameSwitchedEnum::new(
|
||||||
|
|
|
||||||
|
|
@ -366,7 +366,7 @@ fn new_field(
|
||||||
&xml_namespace,
|
&xml_namespace,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let parts = Compound::from_field_defs(field_defs)?;
|
let parts = Compound::from_field_defs(field_defs, None)?;
|
||||||
|
|
||||||
Ok(Box::new(ChildField {
|
Ok(Box::new(ChildField {
|
||||||
default_,
|
default_,
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,10 @@ pub(crate) struct XmlCompoundMeta {
|
||||||
/// The value assigned to `iterator` inside `#[xml(..)]`, if any.
|
/// The value assigned to `iterator` inside `#[xml(..)]`, if any.
|
||||||
pub(crate) iterator: Option<Ident>,
|
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.
|
/// The exhaustive flag.
|
||||||
pub(crate) exhaustive: Flag,
|
pub(crate) exhaustive: Flag,
|
||||||
|
|
||||||
|
|
@ -365,6 +369,7 @@ impl XmlCompoundMeta {
|
||||||
let mut qname = QNameRef::default();
|
let mut qname = QNameRef::default();
|
||||||
let mut builder = None;
|
let mut builder = None;
|
||||||
let mut iterator = None;
|
let mut iterator = None;
|
||||||
|
let mut on_unknown_attribute = None;
|
||||||
let mut debug = Flag::Absent;
|
let mut debug = Flag::Absent;
|
||||||
let mut exhaustive = Flag::Absent;
|
let mut exhaustive = Flag::Absent;
|
||||||
let mut transparent = Flag::Absent;
|
let mut transparent = Flag::Absent;
|
||||||
|
|
@ -388,6 +393,15 @@ impl XmlCompoundMeta {
|
||||||
}
|
}
|
||||||
iterator = Some(meta.value()?.parse()?);
|
iterator = Some(meta.value()?.parse()?);
|
||||||
Ok(())
|
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") {
|
} else if meta.path.is_ident("exhaustive") {
|
||||||
if exhaustive.is_set() {
|
if exhaustive.is_set() {
|
||||||
return Err(Error::new_spanned(meta.path, "duplicate `exhaustive` key"));
|
return Err(Error::new_spanned(meta.path, "duplicate `exhaustive` key"));
|
||||||
|
|
@ -414,6 +428,7 @@ impl XmlCompoundMeta {
|
||||||
debug,
|
debug,
|
||||||
builder,
|
builder,
|
||||||
iterator,
|
iterator,
|
||||||
|
on_unknown_attribute,
|
||||||
exhaustive,
|
exhaustive,
|
||||||
transparent,
|
transparent,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ impl StructInner {
|
||||||
debug,
|
debug,
|
||||||
builder,
|
builder,
|
||||||
iterator,
|
iterator,
|
||||||
|
on_unknown_attribute,
|
||||||
transparent,
|
transparent,
|
||||||
} = meta;
|
} = meta;
|
||||||
|
|
||||||
|
|
@ -84,6 +85,7 @@ impl StructInner {
|
||||||
if let Flag::Present(_) = transparent {
|
if let Flag::Present(_) = transparent {
|
||||||
reject_key!(namespace not on "transparent structs");
|
reject_key!(namespace not on "transparent structs");
|
||||||
reject_key!(name 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_span = fields.span();
|
||||||
let fields = match fields {
|
let fields = match fields {
|
||||||
|
|
@ -143,7 +145,7 @@ impl StructInner {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self::Compound {
|
Ok(Self::Compound {
|
||||||
inner: Compound::from_fields(fields, &xml_namespace)?,
|
inner: Compound::from_fields(fields, &xml_namespace, on_unknown_attribute)?,
|
||||||
xml_namespace,
|
xml_namespace,
|
||||||
xml_name,
|
xml_name,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ such:
|
||||||
|
|
||||||
- *path*: A Rust path, like `some_crate::foo::Bar`. Note that `foo` on its own
|
- *path*: A Rust path, like `some_crate::foo::Bar`. Note that `foo` on its own
|
||||||
is also a path.
|
is also a path.
|
||||||
|
- *identifier*: A single Rust identifier.
|
||||||
- *string literal*: A string literal, like `"hello world!"`.
|
- *string literal*: A string literal, like `"hello world!"`.
|
||||||
- *type*: A Rust type.
|
- *type*: A Rust type.
|
||||||
- *expression*: A Rust expression.
|
- *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) |
|
| `transparent` | *flag* | If present, declares the struct as *transparent* struct (see below) |
|
||||||
| `builder` | optional *ident* | The name to use for the generated builder type. |
|
| `builder` | optional *ident* | The name to use for the generated builder type. |
|
||||||
| `iterator` | optional *ident* | The name to use for the generated iterator 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.
|
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
|
The namespace prefix, if any, is assigned automatically at serialisation time
|
||||||
|
|
@ -146,6 +148,7 @@ documentation above.
|
||||||
| Key | Value type | Description |
|
| 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`. |
|
| `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.
|
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
|
The namespace prefix, if any, is assigned automatically at serialisation time
|
||||||
|
|
|
||||||
|
|
@ -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
|
/// Attempt to transform a type implementing [`AsXml`] into another
|
||||||
/// type which implements [`FromXml`].
|
/// type which implements [`FromXml`].
|
||||||
pub fn transform<T: FromXml, F: AsXml>(from: F) -> Result<T, self::error::Error> {
|
pub fn transform<T: FromXml, F: AsXml>(from: F) -> Result<T, self::error::Error> {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue