From c0262fbafba582c582c3d91739152fd3112e9448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sun, 27 Apr 2025 11:12:21 +0200 Subject: [PATCH] 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. --- parsers/src/util/macro_tests.rs | 107 +++++++++ xso-proc/src/enums.rs | 375 +++++++++++++++++++++++++++++++- xso-proc/src/meta.rs | 63 +++++- xso-proc/src/structs.rs | 4 + xso/src/from_xml_doc.md | 70 +++++- 5 files changed, 608 insertions(+), 11 deletions(-) diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 58fbce88827ccf69641098274325de3a4038b377..4be80c6a0e43c9ec73205a08ea7f207a62403c08 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -2522,3 +2522,110 @@ fn language_roundtrip_nested_parse() { 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::(""); +} + +#[test] +fn attribute_switched_enum_roundtrip_b() { + #[allow(unused_imports)] + use core::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::("abc"); +} + +#[test] +fn attribute_switched_enum_negative_namespace_mismatch() { + #[allow(unused_imports)] + use core::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + match parse_str::("abc") { + 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::("abc") { + 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::("") { + 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::("") { + 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::("abc") { + Ok(AttributeSwitchedEnum::B { content }) => { + assert_eq!(content, "abc"); + } + other => panic!("unexpected result: {:?}", other), + } +} diff --git a/xso-proc/src/enums.rs b/xso-proc/src/enums.rs index d960bb2bc355bf2483bc8e1c81fed553416892e0..7ca4b0f01fbc2e6e35d245e6c2976697ef9af49f 100644 --- a/xso-proc/src/enums.rs +++ b/xso-proc/src/enums.rs @@ -8,15 +8,15 @@ use std::collections::HashMap; -use proc_macro2::Span; -use quote::quote; +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens}; use syn::*; use crate::common::{AsXmlParts, FromXmlParts, ItemDef}; use crate::compound::Compound; use crate::error_message::ParentRef; 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::types::{ref_ty, ty_from_ident}; @@ -51,6 +51,8 @@ impl NameVariant { 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"); @@ -60,9 +62,14 @@ impl NameVariant { 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"); + reject_key!(value not on "name-switched enum variants" only on "attribute-switched enum variants"); 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 { @@ -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 { + // 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 { + 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 { + 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, + + /// The XML name of the attribute. + attr_name: NameRef, + + /// Enum variant definitions + variants: Vec, +} + +impl AttributeSwitchedEnum { + fn new<'x, I: IntoIterator>( + elem_namespace: NamespaceRef, + elem_name: NameRef, + attr_namespace: Option, + attr_name: NameRef, + variant_iter: I, + ) -> Result { + 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 { + 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 { + 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`]. struct DynamicVariant { /// The identifier of the enum variant. @@ -281,6 +579,8 @@ impl DynamicVariant { transparent: _, // used by StructInner discard: _, // used by StructInner ref deserialize_callback, + ref attribute, + ref value, } = meta; 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!(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!(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)?; Ok(Self { ident, inner }) @@ -376,6 +678,10 @@ enum EnumInner { /// namespace fixed. 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. Dynamic(DynamicEnum), } @@ -389,7 +695,7 @@ impl EnumInner { // added and can handle them, either by processing them or raising // an error if they are present. let XmlCompoundMeta { - span: _, + span, qname: QNameRef { namespace, name }, exhaustive, debug, @@ -400,6 +706,8 @@ impl EnumInner { transparent, discard, deserialize_callback, + attribute, + value, } = meta; // These must've been cleared by the caller. Because these being set @@ -410,19 +718,64 @@ impl EnumInner { assert!(!debug.is_set()); 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!(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!(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(attribute) = attribute { + let Some(attr_name) = attribute.qname.name else { + return Err(Error::new( + attribute.span, + "missing `name` for `attribute` key", + )); + }; - if let Some(namespace) = namespace { + 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( namespace, exhaustive, variant_iter, )?)) } 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"); Ok(Self::Dynamic(DynamicEnum::new(variant_iter)?)) } @@ -438,6 +791,9 @@ impl EnumInner { Self::NameSwitched(ref inner) => { 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) => { inner.make_from_events_statemachine(target_ty_ident, state_ty_ident) } @@ -457,6 +813,11 @@ impl EnumInner { state_ty_ident, 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( target_ty_ident, state_ty_ident, diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 7c9cd2078a7b3398de2f697e9f72ebe5104d7c69..94506d088de665447e00ba241ff784bac104fcfa 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -28,8 +28,10 @@ pub const XMLNS_XMLNS: &str = "http://www.w3.org/2000/xmlns/"; macro_rules! reject_key { ($key:ident not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => { if let Some(ref $key) = $key { - return Err(Error::new_spanned( - $key, + #[allow(unused_imports)] + use syn::spanned::Spanned as _; + return Err(Error::new( + $key.span(), concat!( "`", stringify!($key), @@ -437,6 +439,22 @@ impl TryFrom 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. #[derive(Debug)] pub(crate) struct XmlCompoundMeta { @@ -477,6 +495,12 @@ pub(crate) struct XmlCompoundMeta { /// The value assigned to `deserialize_callback` inside `#[xml(..)]`, if any. pub(crate) deserialize_callback: Option, + + /// The value assigned to `attribute` inside `#[xml(..)]`, if any. + pub(crate) attribute: Option, + + /// The value assigned to `value` inside `#[xml(..)]`, if any. + pub(crate) value: Option, } impl XmlCompoundMeta { @@ -495,6 +519,8 @@ impl XmlCompoundMeta { let mut transparent = Flag::Absent; let mut discard = Vec::new(); let mut deserialize_callback = None; + let mut attribute = None; + let mut value = None; attr.parse_nested_meta(|meta| { if meta.path.is_ident("debug") { @@ -560,6 +586,37 @@ impl XmlCompoundMeta { } deserialize_callback = Some(meta.value()?.parse()?); 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 { match qname.parse_incremental_from_meta(meta)? { None => Ok(()), @@ -580,6 +637,8 @@ impl XmlCompoundMeta { transparent, discard, deserialize_callback, + attribute, + value, }) } diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index 588b7d7fcc302b7905d7b91cc6f6f17354d8e168..61e8ac2da1cc6db53d267698ab828eedf71e5440 100644 --- a/xso-proc/src/structs.rs +++ b/xso-proc/src/structs.rs @@ -74,6 +74,8 @@ impl StructInner { transparent, discard, deserialize_callback, + attribute, + value, } = meta; // These must've been cleared by the caller. Because these being set @@ -85,6 +87,8 @@ impl StructInner { assert!(deserialize_callback.is_none()); 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 { reject_key!(namespace not on "transparent structs"); diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index b90f0a72090b26b6fedd64f6c5a78d922cb735c8..cf5a50447a045a395e834a357268ba2ae8474474 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -21,7 +21,8 @@ assert_eq!(foo, Foo); 2. [Struct meta](#struct-meta) 3. [Enums](#enums) 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) 1. [`attribute` meta](#attribute-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 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 present on the `enum`: The different variants have different sets of mandatory @@ -201,6 +206,67 @@ let foo: Foo = xso::from_bytes(b"").unwrap() 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"").unwrap(); +assert_eq!(foo, Foo::Variant1 { foo: "hello".to_string() }); + +let foo: Foo = xso::from_bytes(b"").unwrap(); +assert_eq!(foo, Foo::Variant2 { bar: "hello".to_string() }); +``` + ### Dynamic enum meta Dynamic enums select their variants by attempting to match them in declaration