xso: add support for attribute-switched enums
skip-changelog, because enums have been introduced in the same release, so this is merely an addition to the already changelogged enum feature.
This commit is contained in:
parent
f4b981dc78
commit
c0262fbafb
5 changed files with 608 additions and 11 deletions
|
|
@ -2522,3 +2522,110 @@ fn language_roundtrip_nested_parse() {
|
||||||
other => panic!("unexpected parse result: {:?}", other),
|
other => panic!("unexpected parse result: {:?}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
|
||||||
|
#[xml(namespace = NS1, name = "foo", attribute = "bar", exhaustive)]
|
||||||
|
enum AttributeSwitchedEnum {
|
||||||
|
#[xml(value = "a")]
|
||||||
|
A {
|
||||||
|
#[xml(attribute = "baz")]
|
||||||
|
baz: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[xml(value = "b")]
|
||||||
|
B {
|
||||||
|
#[xml(text)]
|
||||||
|
content: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_switched_enum_roundtrip_a() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
roundtrip_full::<AttributeSwitchedEnum>("<foo xmlns='urn:example:ns1' bar='a' baz='abc'/>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_switched_enum_roundtrip_b() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
roundtrip_full::<AttributeSwitchedEnum>("<foo xmlns='urn:example:ns1' bar='b'>abc</foo>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_switched_enum_negative_namespace_mismatch() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<AttributeSwitchedEnum>("<foo xmlns='urn:example:ns2' bar='b'>abc</foo>") {
|
||||||
|
Err(xso::error::Error::TypeMismatch) => (),
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_switched_enum_negative_name_mismatch() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<AttributeSwitchedEnum>("<quux xmlns='urn:example:ns1' bar='b'>abc</quux>") {
|
||||||
|
Err(xso::error::Error::TypeMismatch) => (),
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_switched_enum_negative_attribute_missing() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<AttributeSwitchedEnum>("<foo xmlns='urn:example:ns1'/>") {
|
||||||
|
Err(xso::error::Error::Other(e)) => {
|
||||||
|
assert_eq!(e, "Missing discriminator attribute.");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_switched_enum_negative_attribute_mismatch() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<AttributeSwitchedEnum>("<foo xmlns='urn:example:ns1' bar='quux'/>") {
|
||||||
|
Err(xso::error::Error::Other(e)) => {
|
||||||
|
assert_eq!(e, "Unknown value for discriminator attribute.");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attribute_switched_enum_positive_attribute_mismatch() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<AttributeSwitchedEnum>("<foo xmlns='urn:example:ns1' bar='b'>abc</foo>") {
|
||||||
|
Ok(AttributeSwitchedEnum::B { content }) => {
|
||||||
|
assert_eq!(content, "abc");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use proc_macro2::Span;
|
use proc_macro2::{Span, TokenStream};
|
||||||
use quote::quote;
|
use quote::{quote, ToTokens};
|
||||||
use syn::*;
|
use syn::*;
|
||||||
|
|
||||||
use crate::common::{AsXmlParts, FromXmlParts, ItemDef};
|
use crate::common::{AsXmlParts, FromXmlParts, ItemDef};
|
||||||
use crate::compound::Compound;
|
use crate::compound::Compound;
|
||||||
use crate::error_message::ParentRef;
|
use crate::error_message::ParentRef;
|
||||||
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
|
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
|
||||||
use crate::state::{AsItemsStateMachine, FromEventsStateMachine};
|
use crate::state::{AsItemsStateMachine, FromEventsMatchMode, FromEventsStateMachine};
|
||||||
use crate::structs::StructInner;
|
use crate::structs::StructInner;
|
||||||
use crate::types::{ref_ty, ty_from_ident};
|
use crate::types::{ref_ty, ty_from_ident};
|
||||||
|
|
||||||
|
|
@ -51,6 +51,8 @@ impl NameVariant {
|
||||||
transparent,
|
transparent,
|
||||||
discard,
|
discard,
|
||||||
deserialize_callback,
|
deserialize_callback,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
} = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;
|
} = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;
|
||||||
|
|
||||||
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");
|
||||||
|
|
@ -60,9 +62,14 @@ impl NameVariant {
|
||||||
reject_key!(iterator not on "enum variants" only on "enums and structs");
|
reject_key!(iterator not on "enum variants" only on "enums and structs");
|
||||||
reject_key!(transparent flag not on "named enum variants" only on "structs");
|
reject_key!(transparent flag not on "named enum variants" only on "structs");
|
||||||
reject_key!(deserialize_callback not on "enum variants" only on "enums and structs");
|
reject_key!(deserialize_callback not on "enum variants" only on "enums and structs");
|
||||||
|
reject_key!(attribute not on "enum variants" only on "attribute-switched enums");
|
||||||
|
reject_key!(value not on "name-switched enum variants" only on "attribute-switched enum variants");
|
||||||
|
|
||||||
let Some(name) = name else {
|
let Some(name) = name else {
|
||||||
return Err(Error::new(meta_span, "`name` is required on enum variants"));
|
return Err(Error::new(
|
||||||
|
meta_span,
|
||||||
|
"`name` is required on name-switched enum variants",
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
@ -252,6 +259,297 @@ impl NameSwitchedEnum {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The definition of an enum variant, switched on the XML element's
|
||||||
|
/// attribute's value, inside a [`AttributeSwitchedEnum`].
|
||||||
|
struct ValueVariant {
|
||||||
|
/// The verbatim value to match.
|
||||||
|
value: String,
|
||||||
|
|
||||||
|
/// The identifier of the variant
|
||||||
|
ident: Ident,
|
||||||
|
|
||||||
|
/// The field(s) of the variant.
|
||||||
|
inner: Compound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueVariant {
|
||||||
|
/// Construct a new value-selected variant from its declaration.
|
||||||
|
fn new(decl: &Variant, enum_namespace: &NamespaceRef) -> Result<Self> {
|
||||||
|
// We destructure here so that we get informed when new fields are
|
||||||
|
// added and can handle them, either by processing them or raising
|
||||||
|
// an error if they are present.
|
||||||
|
let XmlCompoundMeta {
|
||||||
|
span: meta_span,
|
||||||
|
qname: QNameRef { namespace, name },
|
||||||
|
exhaustive,
|
||||||
|
debug,
|
||||||
|
builder,
|
||||||
|
iterator,
|
||||||
|
on_unknown_attribute,
|
||||||
|
on_unknown_child,
|
||||||
|
transparent,
|
||||||
|
discard,
|
||||||
|
deserialize_callback,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
|
} = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;
|
||||||
|
|
||||||
|
reject_key!(debug flag not on "enum variants" only on "enums and structs");
|
||||||
|
reject_key!(exhaustive flag not on "enum variants" only on "enums");
|
||||||
|
reject_key!(namespace not on "enum variants" only on "enums and structs");
|
||||||
|
reject_key!(name not on "attribute-switched enum variants" only on "enums, structs and name-switched enum variants");
|
||||||
|
reject_key!(builder not on "enum variants" only on "enums and structs");
|
||||||
|
reject_key!(iterator not on "enum variants" only on "enums and structs");
|
||||||
|
reject_key!(transparent flag not on "named enum variants" only on "structs");
|
||||||
|
reject_key!(deserialize_callback not on "enum variants" only on "enums and structs");
|
||||||
|
reject_key!(attribute not on "enum variants" only on "attribute-switched enums");
|
||||||
|
|
||||||
|
let Some(value) = value else {
|
||||||
|
return Err(Error::new(
|
||||||
|
meta_span,
|
||||||
|
"`value` is required on attribute-switched enum variants",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
value: value.value(),
|
||||||
|
ident: decl.ident.clone(),
|
||||||
|
inner: Compound::from_fields(
|
||||||
|
&decl.fields,
|
||||||
|
enum_namespace,
|
||||||
|
on_unknown_attribute,
|
||||||
|
on_unknown_child,
|
||||||
|
discard,
|
||||||
|
)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_from_events_statemachine(
|
||||||
|
&self,
|
||||||
|
enum_ident: &Ident,
|
||||||
|
state_ty_ident: &Ident,
|
||||||
|
) -> Result<FromEventsStateMachine> {
|
||||||
|
let value = &self.value;
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.make_from_events_statemachine(
|
||||||
|
state_ty_ident,
|
||||||
|
&ParentRef::Named(Path {
|
||||||
|
leading_colon: None,
|
||||||
|
segments: [
|
||||||
|
PathSegment::from(enum_ident.clone()),
|
||||||
|
self.ident.clone().into(),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
}),
|
||||||
|
&self.ident.to_string(),
|
||||||
|
)?
|
||||||
|
.with_augmented_init(|init| {
|
||||||
|
quote! {
|
||||||
|
#value => { #init },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compile())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_as_item_iter_statemachine(
|
||||||
|
&self,
|
||||||
|
elem_namespace: &NamespaceRef,
|
||||||
|
elem_name: &NameRef,
|
||||||
|
attr_namespace: &TokenStream,
|
||||||
|
attr_name: &NameRef,
|
||||||
|
enum_ident: &Ident,
|
||||||
|
state_ty_ident: &Ident,
|
||||||
|
item_iter_ty_lifetime: &Lifetime,
|
||||||
|
) -> Result<AsItemsStateMachine> {
|
||||||
|
let attr_value = &self.value;
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.make_as_item_iter_statemachine(
|
||||||
|
&ParentRef::Named(Path {
|
||||||
|
leading_colon: None,
|
||||||
|
segments: [
|
||||||
|
PathSegment::from(enum_ident.clone()),
|
||||||
|
self.ident.clone().into(),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
}),
|
||||||
|
state_ty_ident,
|
||||||
|
&self.ident.to_string(),
|
||||||
|
item_iter_ty_lifetime,
|
||||||
|
)?
|
||||||
|
.with_augmented_init(|init| {
|
||||||
|
quote! {
|
||||||
|
let name = (
|
||||||
|
::xso::exports::rxml::Namespace::from(#elem_namespace),
|
||||||
|
::xso::exports::alloc::borrow::Cow::Borrowed(#elem_name),
|
||||||
|
);
|
||||||
|
#init
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_extra_header_state(
|
||||||
|
// Note: we convert the identifier to a string here to prevent
|
||||||
|
// its Span from leaking into the new identifier.
|
||||||
|
quote::format_ident!("{}Discriminator", &self.ident.to_string()),
|
||||||
|
quote! {
|
||||||
|
::xso::Item::Attribute(
|
||||||
|
#attr_namespace,
|
||||||
|
::xso::exports::alloc::borrow::Cow::Borrowed(#attr_name),
|
||||||
|
::xso::exports::alloc::borrow::Cow::Borrowed(#attr_value),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.compile())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The definition of an enum where each variant represents a different value
|
||||||
|
/// of a fixed attribute.
|
||||||
|
struct AttributeSwitchedEnum {
|
||||||
|
/// The XML namespace of the element.
|
||||||
|
elem_namespace: NamespaceRef,
|
||||||
|
|
||||||
|
/// The XML name of the element.
|
||||||
|
elem_name: NameRef,
|
||||||
|
|
||||||
|
/// The XML namespace of the attribute, or None if the attribute isn't
|
||||||
|
/// namespaced.'
|
||||||
|
attr_namespace: Option<NamespaceRef>,
|
||||||
|
|
||||||
|
/// The XML name of the attribute.
|
||||||
|
attr_name: NameRef,
|
||||||
|
|
||||||
|
/// Enum variant definitions
|
||||||
|
variants: Vec<ValueVariant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeSwitchedEnum {
|
||||||
|
fn new<'x, I: IntoIterator<Item = &'x Variant>>(
|
||||||
|
elem_namespace: NamespaceRef,
|
||||||
|
elem_name: NameRef,
|
||||||
|
attr_namespace: Option<NamespaceRef>,
|
||||||
|
attr_name: NameRef,
|
||||||
|
variant_iter: I,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let mut variants = Vec::new();
|
||||||
|
let mut seen_values = HashMap::new();
|
||||||
|
for variant in variant_iter {
|
||||||
|
let variant = ValueVariant::new(variant, &elem_namespace)?;
|
||||||
|
if let Some(other) = seen_values.get(&variant.value) {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
variant.value,
|
||||||
|
format!(
|
||||||
|
"duplicate `value` in enum: variants {} and {} have the same attribute value",
|
||||||
|
other, variant.ident
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
seen_values.insert(variant.value.clone(), variant.ident.clone());
|
||||||
|
variants.push(variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
elem_namespace,
|
||||||
|
elem_name,
|
||||||
|
attr_namespace,
|
||||||
|
attr_name,
|
||||||
|
variants,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the deserialisation statemachine for the attribute-switched enum.
|
||||||
|
fn make_from_events_statemachine(
|
||||||
|
&self,
|
||||||
|
target_ty_ident: &Ident,
|
||||||
|
state_ty_ident: &Ident,
|
||||||
|
) -> Result<FromEventsStateMachine> {
|
||||||
|
let elem_namespace = &self.elem_namespace;
|
||||||
|
let elem_name = &self.elem_name;
|
||||||
|
let attr_namespace = match self.attr_namespace.as_ref() {
|
||||||
|
Some(v) => v.to_token_stream(),
|
||||||
|
None => quote! {
|
||||||
|
::xso::exports::rxml::Namespace::none()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let attr_name = &self.attr_name;
|
||||||
|
|
||||||
|
let mut statemachine = FromEventsStateMachine::new();
|
||||||
|
for variant in self.variants.iter() {
|
||||||
|
statemachine
|
||||||
|
.merge(variant.make_from_events_statemachine(target_ty_ident, state_ty_ident)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
statemachine.set_pre_init(quote! {
|
||||||
|
let attr = {
|
||||||
|
if name.0 != #elem_namespace || name.1 != #elem_name {
|
||||||
|
return ::core::result::Result::Err(
|
||||||
|
::xso::error::FromEventsError::Mismatch {
|
||||||
|
name,
|
||||||
|
attrs,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ::core::option::Option::Some(attr) = attrs.remove(#attr_namespace, #attr_name) else {
|
||||||
|
return ::core::result::Result::Err(
|
||||||
|
::xso::error::FromEventsError::Invalid(
|
||||||
|
::xso::error::Error::Other("Missing discriminator attribute.")
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
attr
|
||||||
|
};
|
||||||
|
});
|
||||||
|
statemachine.set_fallback(quote! {
|
||||||
|
::core::result::Result::Err(
|
||||||
|
::xso::error::FromEventsError::Invalid(
|
||||||
|
::xso::error::Error::Other("Unknown value for discriminator attribute.")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
statemachine.set_match_mode(FromEventsMatchMode::Matched {
|
||||||
|
expr: quote! { attr.as_str() },
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(statemachine)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the serialisation statemachine for the attribute-switched enum.
|
||||||
|
fn make_as_item_iter_statemachine(
|
||||||
|
&self,
|
||||||
|
target_ty_ident: &Ident,
|
||||||
|
state_ty_ident: &Ident,
|
||||||
|
item_iter_ty_lifetime: &Lifetime,
|
||||||
|
) -> Result<AsItemsStateMachine> {
|
||||||
|
let attr_namespace = match self.attr_namespace.as_ref() {
|
||||||
|
Some(v) => v.to_token_stream(),
|
||||||
|
None => quote! {
|
||||||
|
::xso::exports::rxml::Namespace::NONE
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut statemachine = AsItemsStateMachine::new();
|
||||||
|
for variant in self.variants.iter() {
|
||||||
|
statemachine.merge(variant.make_as_item_iter_statemachine(
|
||||||
|
&self.elem_namespace,
|
||||||
|
&self.elem_name,
|
||||||
|
&attr_namespace,
|
||||||
|
&self.attr_name,
|
||||||
|
target_ty_ident,
|
||||||
|
state_ty_ident,
|
||||||
|
item_iter_ty_lifetime,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(statemachine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The definition of an enum variant in a [`DynamicEnum`].
|
/// The definition of an enum variant in a [`DynamicEnum`].
|
||||||
struct DynamicVariant {
|
struct DynamicVariant {
|
||||||
/// The identifier of the enum variant.
|
/// The identifier of the enum variant.
|
||||||
|
|
@ -281,6 +579,8 @@ impl DynamicVariant {
|
||||||
transparent: _, // used by StructInner
|
transparent: _, // used by StructInner
|
||||||
discard: _, // used by StructInner
|
discard: _, // used by StructInner
|
||||||
ref deserialize_callback,
|
ref deserialize_callback,
|
||||||
|
ref attribute,
|
||||||
|
ref value,
|
||||||
} = 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");
|
||||||
|
|
@ -288,6 +588,8 @@ impl DynamicVariant {
|
||||||
reject_key!(builder not on "enum variants" only on "enums and structs");
|
reject_key!(builder not on "enum variants" only on "enums and structs");
|
||||||
reject_key!(iterator not on "enum variants" only on "enums and structs");
|
reject_key!(iterator not on "enum variants" only on "enums and structs");
|
||||||
reject_key!(deserialize_callback not on "enum variants" only on "enums and structs");
|
reject_key!(deserialize_callback not on "enum variants" only on "enums and structs");
|
||||||
|
reject_key!(attribute not on "enum variants" only on "attribute-switched enums");
|
||||||
|
reject_key!(value not on "dynamic enum variants" only on "attribute-switched enum variants");
|
||||||
|
|
||||||
let inner = StructInner::new(meta, &variant.fields)?;
|
let inner = StructInner::new(meta, &variant.fields)?;
|
||||||
Ok(Self { ident, inner })
|
Ok(Self { ident, inner })
|
||||||
|
|
@ -376,6 +678,10 @@ enum EnumInner {
|
||||||
/// namespace fixed.
|
/// namespace fixed.
|
||||||
NameSwitched(NameSwitchedEnum),
|
NameSwitched(NameSwitchedEnum),
|
||||||
|
|
||||||
|
/// The enum switches based on the value of an attribute of the XML
|
||||||
|
/// element.
|
||||||
|
AttributeSwitched(AttributeSwitchedEnum),
|
||||||
|
|
||||||
/// The enum consists of variants with entirely unrelated XML structures.
|
/// The enum consists of variants with entirely unrelated XML structures.
|
||||||
Dynamic(DynamicEnum),
|
Dynamic(DynamicEnum),
|
||||||
}
|
}
|
||||||
|
|
@ -389,7 +695,7 @@ impl EnumInner {
|
||||||
// added and can handle them, either by processing them or raising
|
// added and can handle them, either by processing them or raising
|
||||||
// an error if they are present.
|
// an error if they are present.
|
||||||
let XmlCompoundMeta {
|
let XmlCompoundMeta {
|
||||||
span: _,
|
span,
|
||||||
qname: QNameRef { namespace, name },
|
qname: QNameRef { namespace, name },
|
||||||
exhaustive,
|
exhaustive,
|
||||||
debug,
|
debug,
|
||||||
|
|
@ -400,6 +706,8 @@ impl EnumInner {
|
||||||
transparent,
|
transparent,
|
||||||
discard,
|
discard,
|
||||||
deserialize_callback,
|
deserialize_callback,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
} = meta;
|
} = meta;
|
||||||
|
|
||||||
// These must've been cleared by the caller. Because these being set
|
// These must've been cleared by the caller. Because these being set
|
||||||
|
|
@ -410,19 +718,64 @@ impl EnumInner {
|
||||||
assert!(!debug.is_set());
|
assert!(!debug.is_set());
|
||||||
assert!(deserialize_callback.is_none());
|
assert!(deserialize_callback.is_none());
|
||||||
|
|
||||||
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");
|
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");
|
reject_key!(on_unknown_child not on "enums" only on "enum variants and structs");
|
||||||
reject_key!(discard vec not on "enums" only on "enum variants and structs");
|
reject_key!(discard vec not on "enums" only on "enum variants and structs");
|
||||||
|
reject_key!(value not on "enums" only on "attribute-switched enum variants");
|
||||||
|
|
||||||
if let Some(namespace) = namespace {
|
if let Some(attribute) = attribute {
|
||||||
|
let Some(attr_name) = attribute.qname.name else {
|
||||||
|
return Err(Error::new(
|
||||||
|
attribute.span,
|
||||||
|
"missing `name` for `attribute` key",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let attr_namespace = attribute.qname.namespace;
|
||||||
|
|
||||||
|
let Some(elem_namespace) = namespace else {
|
||||||
|
let mut error =
|
||||||
|
Error::new(span, "missing `namespace` key on attribute-switched enum");
|
||||||
|
error.combine(Error::new(
|
||||||
|
attribute.span,
|
||||||
|
"enum is attribute-switched because of the `attribute` key here",
|
||||||
|
));
|
||||||
|
return Err(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(elem_name) = name else {
|
||||||
|
let mut error = Error::new(span, "missing `name` key on attribute-switched enum");
|
||||||
|
error.combine(Error::new(
|
||||||
|
attribute.span,
|
||||||
|
"enum is attribute-switched because of the `attribute` key here",
|
||||||
|
));
|
||||||
|
return Err(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !exhaustive.is_set() {
|
||||||
|
return Err(Error::new(
|
||||||
|
span,
|
||||||
|
"attribute-switched enums must be marked as `exhaustive`. non-exhaustive attribute-switched enums are not supported."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self::AttributeSwitched(AttributeSwitchedEnum::new(
|
||||||
|
elem_namespace,
|
||||||
|
elem_name,
|
||||||
|
attr_namespace,
|
||||||
|
attr_name,
|
||||||
|
variant_iter,
|
||||||
|
)?))
|
||||||
|
} else if let Some(namespace) = namespace {
|
||||||
|
reject_key!(name not on "name-switched enums" only on "their variants");
|
||||||
Ok(Self::NameSwitched(NameSwitchedEnum::new(
|
Ok(Self::NameSwitched(NameSwitchedEnum::new(
|
||||||
namespace,
|
namespace,
|
||||||
exhaustive,
|
exhaustive,
|
||||||
variant_iter,
|
variant_iter,
|
||||||
)?))
|
)?))
|
||||||
} else {
|
} else {
|
||||||
|
reject_key!(name not on "dynamic enums" only on "their variants");
|
||||||
reject_key!(exhaustive flag not on "dynamic enums" only on "name-switched enums");
|
reject_key!(exhaustive flag not on "dynamic enums" only on "name-switched enums");
|
||||||
Ok(Self::Dynamic(DynamicEnum::new(variant_iter)?))
|
Ok(Self::Dynamic(DynamicEnum::new(variant_iter)?))
|
||||||
}
|
}
|
||||||
|
|
@ -438,6 +791,9 @@ impl EnumInner {
|
||||||
Self::NameSwitched(ref inner) => {
|
Self::NameSwitched(ref inner) => {
|
||||||
inner.make_from_events_statemachine(target_ty_ident, state_ty_ident)
|
inner.make_from_events_statemachine(target_ty_ident, state_ty_ident)
|
||||||
}
|
}
|
||||||
|
Self::AttributeSwitched(ref inner) => {
|
||||||
|
inner.make_from_events_statemachine(target_ty_ident, state_ty_ident)
|
||||||
|
}
|
||||||
Self::Dynamic(ref inner) => {
|
Self::Dynamic(ref inner) => {
|
||||||
inner.make_from_events_statemachine(target_ty_ident, state_ty_ident)
|
inner.make_from_events_statemachine(target_ty_ident, state_ty_ident)
|
||||||
}
|
}
|
||||||
|
|
@ -457,6 +813,11 @@ impl EnumInner {
|
||||||
state_ty_ident,
|
state_ty_ident,
|
||||||
item_iter_ty_lifetime,
|
item_iter_ty_lifetime,
|
||||||
),
|
),
|
||||||
|
Self::AttributeSwitched(ref inner) => inner.make_as_item_iter_statemachine(
|
||||||
|
target_ty_ident,
|
||||||
|
state_ty_ident,
|
||||||
|
item_iter_ty_lifetime,
|
||||||
|
),
|
||||||
Self::Dynamic(ref inner) => inner.make_as_item_iter_statemachine(
|
Self::Dynamic(ref inner) => inner.make_as_item_iter_statemachine(
|
||||||
target_ty_ident,
|
target_ty_ident,
|
||||||
state_ty_ident,
|
state_ty_ident,
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,10 @@ pub const XMLNS_XMLNS: &str = "http://www.w3.org/2000/xmlns/";
|
||||||
macro_rules! reject_key {
|
macro_rules! reject_key {
|
||||||
($key:ident not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => {
|
($key:ident not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => {
|
||||||
if let Some(ref $key) = $key {
|
if let Some(ref $key) = $key {
|
||||||
return Err(Error::new_spanned(
|
#[allow(unused_imports)]
|
||||||
$key,
|
use syn::spanned::Spanned as _;
|
||||||
|
return Err(Error::new(
|
||||||
|
$key.span(),
|
||||||
concat!(
|
concat!(
|
||||||
"`",
|
"`",
|
||||||
stringify!($key),
|
stringify!($key),
|
||||||
|
|
@ -437,6 +439,22 @@ impl TryFrom<XmlFieldMeta> for DiscardSpec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper around `QNameRef` which saves additional span information.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct SpannedQNameRef {
|
||||||
|
/// The span which created the (potentially empty) ref.
|
||||||
|
pub span: Span,
|
||||||
|
|
||||||
|
/// The ref itself.
|
||||||
|
pub qname: QNameRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpannedQNameRef {
|
||||||
|
pub(crate) fn span(&self) -> Span {
|
||||||
|
self.span
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
|
/// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct XmlCompoundMeta {
|
pub(crate) struct XmlCompoundMeta {
|
||||||
|
|
@ -477,6 +495,12 @@ pub(crate) struct XmlCompoundMeta {
|
||||||
|
|
||||||
/// The value assigned to `deserialize_callback` inside `#[xml(..)]`, if any.
|
/// The value assigned to `deserialize_callback` inside `#[xml(..)]`, if any.
|
||||||
pub(crate) deserialize_callback: Option<Path>,
|
pub(crate) deserialize_callback: Option<Path>,
|
||||||
|
|
||||||
|
/// The value assigned to `attribute` inside `#[xml(..)]`, if any.
|
||||||
|
pub(crate) attribute: Option<SpannedQNameRef>,
|
||||||
|
|
||||||
|
/// The value assigned to `value` inside `#[xml(..)]`, if any.
|
||||||
|
pub(crate) value: Option<LitStr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XmlCompoundMeta {
|
impl XmlCompoundMeta {
|
||||||
|
|
@ -495,6 +519,8 @@ impl XmlCompoundMeta {
|
||||||
let mut transparent = Flag::Absent;
|
let mut transparent = Flag::Absent;
|
||||||
let mut discard = Vec::new();
|
let mut discard = Vec::new();
|
||||||
let mut deserialize_callback = None;
|
let mut deserialize_callback = None;
|
||||||
|
let mut attribute = None;
|
||||||
|
let mut value = None;
|
||||||
|
|
||||||
attr.parse_nested_meta(|meta| {
|
attr.parse_nested_meta(|meta| {
|
||||||
if meta.path.is_ident("debug") {
|
if meta.path.is_ident("debug") {
|
||||||
|
|
@ -560,6 +586,37 @@ impl XmlCompoundMeta {
|
||||||
}
|
}
|
||||||
deserialize_callback = Some(meta.value()?.parse()?);
|
deserialize_callback = Some(meta.value()?.parse()?);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
} else if meta.path.is_ident("attribute") {
|
||||||
|
if attribute.is_some() {
|
||||||
|
return Err(Error::new_spanned(meta.path, "duplicate `attribute` key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let span = meta.path.span();
|
||||||
|
let qname = if meta.input.peek(Token![=]) {
|
||||||
|
let (namespace, name) = parse_prefixed_name(meta.value()?)?;
|
||||||
|
QNameRef {
|
||||||
|
name: Some(name),
|
||||||
|
namespace,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut qname = QNameRef::default();
|
||||||
|
meta.parse_nested_meta(|meta| {
|
||||||
|
match qname.parse_incremental_from_meta(meta)? {
|
||||||
|
None => Ok(()),
|
||||||
|
Some(meta) => Err(Error::new_spanned(meta.path, "unsupported key")),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
qname
|
||||||
|
};
|
||||||
|
|
||||||
|
attribute = Some(SpannedQNameRef { qname, span });
|
||||||
|
Ok(())
|
||||||
|
} else if meta.path.is_ident("value") {
|
||||||
|
if value.is_some() {
|
||||||
|
return Err(Error::new_spanned(meta.path, "duplicate `value` key"));
|
||||||
|
}
|
||||||
|
value = Some(meta.value()?.parse()?);
|
||||||
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
match qname.parse_incremental_from_meta(meta)? {
|
match qname.parse_incremental_from_meta(meta)? {
|
||||||
None => Ok(()),
|
None => Ok(()),
|
||||||
|
|
@ -580,6 +637,8 @@ impl XmlCompoundMeta {
|
||||||
transparent,
|
transparent,
|
||||||
discard,
|
discard,
|
||||||
deserialize_callback,
|
deserialize_callback,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ impl StructInner {
|
||||||
transparent,
|
transparent,
|
||||||
discard,
|
discard,
|
||||||
deserialize_callback,
|
deserialize_callback,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
} = meta;
|
} = meta;
|
||||||
|
|
||||||
// These must've been cleared by the caller. Because these being set
|
// These must've been cleared by the caller. Because these being set
|
||||||
|
|
@ -85,6 +87,8 @@ impl StructInner {
|
||||||
assert!(deserialize_callback.is_none());
|
assert!(deserialize_callback.is_none());
|
||||||
|
|
||||||
reject_key!(exhaustive flag not on "structs" only on "enums");
|
reject_key!(exhaustive flag not on "structs" only on "enums");
|
||||||
|
reject_key!(attribute not on "structs" only on "enums");
|
||||||
|
reject_key!(value not on "structs" only on "attribute-switched enum variants");
|
||||||
|
|
||||||
if let Flag::Present(_) = transparent {
|
if let Flag::Present(_) = transparent {
|
||||||
reject_key!(namespace not on "transparent structs");
|
reject_key!(namespace not on "transparent structs");
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ assert_eq!(foo, Foo);
|
||||||
2. [Struct meta](#struct-meta)
|
2. [Struct meta](#struct-meta)
|
||||||
3. [Enums](#enums)
|
3. [Enums](#enums)
|
||||||
1. [Name-switched enum meta](#name-switched-enum-meta)
|
1. [Name-switched enum meta](#name-switched-enum-meta)
|
||||||
2. [Dynamic enum meta](#dynamic-enum-meta)
|
2. [Attribute-switched enum meta](#attribute-switched-enum-meta)
|
||||||
|
3. [Dynamic enum meta](#dynamic-enum-meta)
|
||||||
4. [Field meta](#field-meta)
|
4. [Field meta](#field-meta)
|
||||||
1. [`attribute` meta](#attribute-meta)
|
1. [`attribute` meta](#attribute-meta)
|
||||||
2. [`child` meta](#child-meta)
|
2. [`child` meta](#child-meta)
|
||||||
|
|
@ -123,7 +124,11 @@ Two different `enum` flavors are supported:
|
||||||
namespace they match on and each variant corresponds to a different XML
|
namespace they match on and each variant corresponds to a different XML
|
||||||
element name within that namespace.
|
element name within that namespace.
|
||||||
|
|
||||||
2. [**Dynamic enums**](#dynamic-enum-meta) have entirely unrelated variants.
|
2. [**Attribute-switched enums**](#attribute-switched-enum-meta) have a fixed
|
||||||
|
XML element they match which must have a specific attribute. The variants
|
||||||
|
correspond to a value of that XML attribute.
|
||||||
|
|
||||||
|
3. [**Dynamic enums**](#dynamic-enum-meta) have entirely unrelated variants.
|
||||||
|
|
||||||
At the source-code level, they are distinguished by the meta keys which are
|
At the source-code level, they are distinguished by the meta keys which are
|
||||||
present on the `enum`: The different variants have different sets of mandatory
|
present on the `enum`: The different variants have different sets of mandatory
|
||||||
|
|
@ -201,6 +206,67 @@ let foo: Foo = xso::from_bytes(b"<b xmlns='urn:example' bar='hello'/>").unwrap()
|
||||||
assert_eq!(foo, Foo::Variant2 { bar: "hello".to_string() });
|
assert_eq!(foo, Foo::Variant2 { bar: "hello".to_string() });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Attribute-switched enum meta
|
||||||
|
|
||||||
|
Attribute-switched enums match a fixed XML element and then select the enum
|
||||||
|
variant based on a specific attribute on that XML element. Attribute-switched
|
||||||
|
enums are declared by setting the `namespace`, `name` and `attribute` keys on
|
||||||
|
a `enum` item.
|
||||||
|
|
||||||
|
The following keys are defined on name-switched enums:
|
||||||
|
|
||||||
|
| Key | Value type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `namespace` | *string literal* or *path* | The XML element namespace to match for this enum. If it is a *path*, it must point at a `&'static str`. |
|
||||||
|
| `name` | *string literal* or *path* | The XML element name to match. If it is a *path*, it must point at a `&'static NcNameStr`. |
|
||||||
|
| `attribute` | *string literal*, *path* or *nested* | The attribute to match. If it is a *path*, it must point at a `&'static NcNameStr`. |
|
||||||
|
| `builder` | optional *ident* | The name to use for the generated builder type. |
|
||||||
|
| `iterator` | optional *ident* | The name to use for the generated iterator type. |
|
||||||
|
| `exhaustive` | *flag* | Must be set to allow future extensions. |
|
||||||
|
| `discard` | optional *nested* | Contains field specifications of content to ignore. See the struct meta docs for details. |
|
||||||
|
| `deserialize_callback` | optional *path* | Path to a `fn(&mut T) -> Result<(), Error>` which is called on the deserialized enum after deserialization. |
|
||||||
|
|
||||||
|
`attribute` follows the same syntax and semantic as the
|
||||||
|
[`attribute` meta](#attribute-meta), but only allows the `namespace` and
|
||||||
|
`name` keys.
|
||||||
|
|
||||||
|
For details on `builder` and `iterator`, see the [Struct meta](#struct-meta)
|
||||||
|
documentation above.
|
||||||
|
|
||||||
|
#### Attribute-switched enum variant meta
|
||||||
|
|
||||||
|
| Key | Value type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `value` | *string literal* | The text content to match for this variant. |
|
||||||
|
| `on_unknown_attribute` | optional *ident* | Name of an [`UnknownAttributePolicy`] member, controlling how unknown attributes are handled. |
|
||||||
|
| `on_unknown_child` | optional *ident* | Name of an [`UnknownChildPolicy`] member, controlling how unknown children are handled. |
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# use xso::FromXml;
|
||||||
|
#[derive(FromXml, Debug, PartialEq)]
|
||||||
|
#[xml(namespace = "urn:example", name = "foo", attribute = "version", exhaustive)]
|
||||||
|
enum Foo {
|
||||||
|
#[xml(value = "a")]
|
||||||
|
Variant1 {
|
||||||
|
#[xml(attribute)]
|
||||||
|
foo: String,
|
||||||
|
},
|
||||||
|
#[xml(value = "b")]
|
||||||
|
Variant2 {
|
||||||
|
#[xml(attribute)]
|
||||||
|
bar: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example' version='a' foo='hello'/>").unwrap();
|
||||||
|
assert_eq!(foo, Foo::Variant1 { foo: "hello".to_string() });
|
||||||
|
|
||||||
|
let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example' version='b' bar='hello'/>").unwrap();
|
||||||
|
assert_eq!(foo, Foo::Variant2 { bar: "hello".to_string() });
|
||||||
|
```
|
||||||
|
|
||||||
### Dynamic enum meta
|
### Dynamic enum meta
|
||||||
|
|
||||||
Dynamic enums select their variants by attempting to match them in declaration
|
Dynamic enums select their variants by attempting to match them in declaration
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue