diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 104e9e1197a1dca6c60f35d8165e9875623a337a..86d0e5f2d770b62df4f8a4993608c926f9461bff 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 dcad84515c0ff5a22afc986ea93a15ee5d3199f6..20ac67ae6f71ca7cac0870d7792fd66c8746cea2 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 d2715f213427392f8b1c93a664103c0393c7b272..81654cdc1b769ff9b5e1c41432d9e87d71ab384a 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 fb817a8f5cf29eaf225c179c724670a788867064..4280eb60411ef5b4ed438bd0458d506214d9ab6b 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 74f95564d4a23b1c8f522c6484abbab18db033c9..7d6c28c9daa162c57f1ceb852a35bbd3df53328a 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 b221c6e144ab193fab07138e98422809f25f9a52..84dce1e8cec27921dc22ccce96b66af9f6b43c26 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.