diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 104e9e11..86d0e5f2 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -734,3 +734,64 @@ fn renamed_enum_types_get_renamed() { assert!(std::mem::size_of::() >= 0); assert!(std::mem::size_of::() >= 0); } + +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = NS1, exhaustive)] +enum ExhaustiveNameSwitchedEnum { + #[xml(name = "a")] + Variant1 { + #[xml(attribute)] + foo: String, + }, + #[xml(name = "b")] + Variant2 { + #[xml(text)] + foo: String, + }, +} + +#[test] +fn exhaustive_name_switched_enum_negative_name_mismatch() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + match parse_str::("hello") { + Err(xso::error::FromElementError::Invalid { .. }) => (), + other => panic!("unexpected result: {:?}", other), + } +} + +#[test] +fn exhaustive_name_switched_enum_negative_namespace_mismatch() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + match parse_str::("hello") { + Err(xso::error::FromElementError::Mismatch { .. }) => (), + other => panic!("unexpected result: {:?}", other), + } +} + +#[test] +fn exhaustive_name_switched_enum_roundtrip_variant_1() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::("") +} + +#[test] +fn exhaustive_name_switched_enum_roundtrip_variant_2() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::("hello") +} diff --git a/xso-proc/src/enums.rs b/xso-proc/src/enums.rs index dcad8451..20ac67ae 100644 --- a/xso-proc/src/enums.rs +++ b/xso-proc/src/enums.rs @@ -40,12 +40,14 @@ impl NameVariant { span: meta_span, namespace, name, + exhaustive, debug, builder, iterator, } = 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!(builder not on "enum variants" only on "enums and structs"); reject_key!(iterator not on "enum variants" only on "enums and structs"); @@ -142,6 +144,9 @@ pub(crate) struct EnumDef { /// The variants of the enum. variants: Vec, + /// Flag indicating whether the enum is exhaustive. + exhaustive: bool, + /// Name of the target type. target_ty_ident: Ident, @@ -169,6 +174,7 @@ impl EnumDef { span: meta_span, namespace, name, + exhaustive, debug, builder, iterator, @@ -210,6 +216,7 @@ impl EnumDef { Ok(Self { namespace, variants, + exhaustive: exhaustive.is_set(), target_ty_ident: ident.clone(), builder_ty_ident, item_iter_ty_ident, @@ -245,6 +252,15 @@ impl ItemDef for EnumDef { } }); + if self.exhaustive { + let mismatch_err = format!("This is not a {} element.", target_ty_ident); + statemachine.set_fallback(quote! { + ::core::result::Result::Err(::xso::error::FromEventsError::Invalid( + ::xso::error::Error::Other(#mismatch_err), + )) + }) + } + let defs = statemachine.render( vis, builder_ty_ident, diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index d2715f21..81654cdc 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -235,6 +235,9 @@ pub(crate) struct XmlCompoundMeta { /// The value assigned to `iterator` inside `#[xml(..)]`, if any. pub(crate) iterator: Option, + + /// The exhaustive flag. + pub(crate) exhaustive: Flag, } impl XmlCompoundMeta { @@ -248,6 +251,7 @@ impl XmlCompoundMeta { let mut builder = None; let mut iterator = None; let mut debug = Flag::Absent; + let mut exhaustive = Flag::Absent; attr.parse_nested_meta(|meta| { if meta.path.is_ident("name") { @@ -280,6 +284,12 @@ impl XmlCompoundMeta { } iterator = 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")); + } + exhaustive = (&meta.path).into(); + Ok(()) } else { Err(Error::new_spanned(meta.path, "unsupported key")) } @@ -292,6 +302,7 @@ impl XmlCompoundMeta { debug, builder, iterator, + exhaustive, }) } diff --git a/xso-proc/src/state.rs b/xso-proc/src/state.rs index fb817a8f..4280eb60 100644 --- a/xso-proc/src/state.rs +++ b/xso-proc/src/state.rs @@ -177,6 +177,7 @@ impl FromEventsSubmachine { advance_match_arms, variants: vec![FromEventsEntryPoint { init: self.init }], pre_init: TokenStream::default(), + fallback: None, } } @@ -373,6 +374,12 @@ pub(crate) struct FromEventsStateMachine { /// Extra code run during pre-init phase. pre_init: TokenStream, + /// Code to run as fallback if none of the branches matched the start + /// event. + /// + /// If absent, a `FromEventsError::Mismatch` is generated. + fallback: Option, + /// A sequence of enum variant declarations, separated and terminated by /// commas. state_defs: TokenStream, @@ -402,6 +409,7 @@ impl FromEventsStateMachine { advance_match_arms: TokenStream::default(), pre_init: TokenStream::default(), variants: Vec::new(), + fallback: None, } } @@ -409,6 +417,7 @@ impl FromEventsStateMachine { /// /// This *discards* the other state machine's pre-init code. pub(crate) fn merge(&mut self, other: FromEventsStateMachine) { + assert!(other.fallback.is_none()); self.defs.extend(other.defs); self.state_defs.extend(other.state_defs); self.advance_match_arms.extend(other.advance_match_arms); @@ -424,6 +433,14 @@ impl FromEventsStateMachine { self.pre_init = code; } + /// Set the fallback code to use if none of the branches matches the start + /// event. + /// + /// By default, a `FromEventsError::Mismatch` is generated. + pub(crate) fn set_fallback(&mut self, code: TokenStream) { + self.fallback = Some(code); + } + /// Render the state machine as a token stream. /// /// The token stream contains the following pieces: @@ -446,6 +463,7 @@ impl FromEventsStateMachine { advance_match_arms, variants, pre_init, + fallback, } = self; let mut init_body = pre_init; @@ -460,6 +478,12 @@ impl FromEventsStateMachine { }) } + let fallback = fallback.unwrap_or_else(|| { + quote! { + ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs }) + } + }); + let output_ty_ref = make_ty_ref(output_ty); let docstr = format!("Build a {0} from XML events.\n\nThis type is generated using the [`macro@xso::FromXml`] derive macro and implements [`xso::FromEventsBuilder`] for {0}.", output_ty_ref); @@ -520,7 +544,7 @@ impl FromEventsStateMachine { ) -> ::core::result::Result { #init_body { let _ = &mut attrs; } - ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs }) + #fallback } } }) diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index 74f95564..7d6c28c9 100644 --- a/xso-proc/src/structs.rs +++ b/xso-proc/src/structs.rs @@ -12,7 +12,7 @@ use syn::*; use crate::common::{AsXmlParts, FromXmlParts, ItemDef}; use crate::compound::Compound; -use crate::meta::{NameRef, NamespaceRef, XmlCompoundMeta}; +use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, XmlCompoundMeta}; /// Definition of a struct and how to parse it. pub(crate) struct StructDef { @@ -48,11 +48,14 @@ impl StructDef { span: meta_span, namespace, name, + exhaustive, debug, builder, iterator, } = meta; + reject_key!(exhaustive flag not on "structs" only on "enums"); + let Some(namespace) = namespace else { return Err(Error::new(meta_span, "`namespace` is required on structs")); }; diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index b221c6e1..84dce1e8 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -81,12 +81,22 @@ The following keys are defined on enums: | `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`. | | `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* | If present, the enum considers itself authoritative for its namespace; unknown elements within the namespace are rejected instead of treated as mismatch. | All variants of an enum live within the same namespace and are distinguished exclusively by their XML name within that namespace. The contents of the XML element (including attributes) is not inspected before selecting the variant when parsing XML. +If *exhaustive* is set and an element is encountered which matches the +namespace of the enum, but matches none of its variants, parsing will fail +with an error. If *exhaustive* is *not* set, in such a situation, parsing +would attempt to continue with other siblings of the enum, attempting to find +a handler for that element. + +Note that the *exhaustive* flag is orthogonal to the Rust attribute +`#[non_exhaustive]`. + For details on `builder` and `iterator`, see the [Struct meta](#struct-meta) documentation above.