xso: implement exhaustive enums

These more closely mirror how enums work currently with the macros.
Non-exhaustive enums may be useful though and kind of were the natural
thing to implement.
This commit is contained in:
Jonas Schäfer 2024-07-10 16:35:29 +02:00
parent a20caf839f
commit c028c3b91a
6 changed files with 127 additions and 2 deletions

View file

@ -734,3 +734,64 @@ fn renamed_enum_types_get_renamed() {
assert!(std::mem::size_of::<RenamedEnumBuilder>() >= 0);
assert!(std::mem::size_of::<RenamedEnumIter>() >= 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::<ExhaustiveNameSwitchedEnum>("<x xmlns='urn:example:ns1'>hello</x>") {
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::<ExhaustiveNameSwitchedEnum>("<b xmlns='urn:example:ns2'>hello</b>") {
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::<ExhaustiveNameSwitchedEnum>("<a xmlns='urn:example:ns1' foo='hello'/>")
}
#[test]
fn exhaustive_name_switched_enum_roundtrip_variant_2() {
#[allow(unused_imports)]
use std::{
option::Option::{None, Some},
result::Result::{Err, Ok},
};
roundtrip_full::<ExhaustiveNameSwitchedEnum>("<b xmlns='urn:example:ns1'>hello</b>")
}

View file

@ -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<NameVariant>,
/// 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,

View file

@ -235,6 +235,9 @@ pub(crate) struct XmlCompoundMeta {
/// The value assigned to `iterator` inside `#[xml(..)]`, if any.
pub(crate) iterator: Option<Ident>,
/// 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,
})
}

View file

@ -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<TokenStream>,
/// 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<Self, ::xso::error::FromEventsError> {
#init_body
{ let _ = &mut attrs; }
::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs })
#fallback
}
}
})

View file

@ -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"));
};

View file

@ -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.