xso: add support for dynamic enums
This commit is contained in:
parent
b36d1d0d4c
commit
fb80bb7532
5 changed files with 329 additions and 46 deletions
|
|
@ -1600,3 +1600,36 @@ fn transparent_struct_named_roundtrip() {
|
|||
};
|
||||
roundtrip_full::<TransparentStructNamed>("<attr xmlns='urn:example:ns1' foo='bar'/>");
|
||||
}
|
||||
|
||||
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||
#[xml()]
|
||||
enum DynamicEnum {
|
||||
#[xml(transparent)]
|
||||
A(RequiredAttribute),
|
||||
|
||||
#[xml(namespace = NS2, name = "b")]
|
||||
B {
|
||||
#[xml(text)]
|
||||
contents: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_enum_roundtrip_a() {
|
||||
#[allow(unused_imports)]
|
||||
use std::{
|
||||
option::Option::{None, Some},
|
||||
result::Result::{Err, Ok},
|
||||
};
|
||||
roundtrip_full::<DynamicEnum>("<attr xmlns='urn:example:ns1' foo='bar'/>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_enum_roundtrip_b() {
|
||||
#[allow(unused_imports)]
|
||||
use std::{
|
||||
option::Option::{None, Some},
|
||||
result::Result::{Err, Ok},
|
||||
};
|
||||
roundtrip_full::<DynamicEnum>("<b xmlns='urn:example:ns2'>hello world</b>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ 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::structs::StructInner;
|
||||
use crate::types::{ref_ty, ty_from_ident};
|
||||
|
||||
/// The definition of an enum variant, switched on the XML element's name.
|
||||
/// The definition of an enum variant, switched on the XML element's name,
|
||||
/// inside a [`NameSwitchedEnum`].
|
||||
struct NameVariant {
|
||||
/// The XML name of the element to map the enum variant to.
|
||||
name: NameRef,
|
||||
|
|
@ -140,6 +142,8 @@ impl NameVariant {
|
|||
}
|
||||
}
|
||||
|
||||
/// The definition of a enum which switches based on the XML element name,
|
||||
/// with the XML namespace fixed.
|
||||
struct NameSwitchedEnum {
|
||||
/// The XML namespace of the element to map the enum to.
|
||||
namespace: NamespaceRef,
|
||||
|
|
@ -153,36 +157,10 @@ struct NameSwitchedEnum {
|
|||
|
||||
impl NameSwitchedEnum {
|
||||
fn new<'x, I: IntoIterator<Item = &'x Variant>>(
|
||||
meta: XmlCompoundMeta,
|
||||
namespace: NamespaceRef,
|
||||
exhaustive: Flag,
|
||||
variant_iter: I,
|
||||
) -> 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,
|
||||
transparent,
|
||||
} = meta;
|
||||
|
||||
// These must've been cleared by the caller. Because these being set
|
||||
// is a programming error (in xso-proc) and not a usage error, we
|
||||
// assert here instead of using reject_key!.
|
||||
assert!(builder.is_none());
|
||||
assert!(iterator.is_none());
|
||||
assert!(!debug.is_set());
|
||||
|
||||
reject_key!(name not on "enums" only on "their variants");
|
||||
reject_key!(transparent flag not on "enums" only on "structs");
|
||||
|
||||
let Some(namespace) = namespace else {
|
||||
return Err(Error::new(meta_span, "`namespace` is required on enums"));
|
||||
};
|
||||
|
||||
let mut variants = Vec::new();
|
||||
let mut seen_names = HashMap::new();
|
||||
for variant in variant_iter {
|
||||
|
|
@ -207,6 +185,7 @@ impl NameSwitchedEnum {
|
|||
})
|
||||
}
|
||||
|
||||
/// Build the deserialisation statemachine for the name-switched enum.
|
||||
fn make_from_events_statemachine(
|
||||
&self,
|
||||
target_ty_ident: &Ident,
|
||||
|
|
@ -241,6 +220,7 @@ impl NameSwitchedEnum {
|
|||
Ok(statemachine)
|
||||
}
|
||||
|
||||
/// Build the serialisation statemachine for the name-switched enum.
|
||||
fn make_as_item_iter_statemachine(
|
||||
&self,
|
||||
target_ty_ident: &Ident,
|
||||
|
|
@ -261,10 +241,211 @@ impl NameSwitchedEnum {
|
|||
}
|
||||
}
|
||||
|
||||
/// The definition of an enum variant in a [`DynamicEnum`].
|
||||
struct DynamicVariant {
|
||||
/// The identifier of the enum variant.
|
||||
ident: Ident,
|
||||
|
||||
/// The definition of the struct-like which resembles the enum variant.
|
||||
inner: StructInner,
|
||||
}
|
||||
|
||||
impl DynamicVariant {
|
||||
fn new(variant: &Variant) -> Result<Self> {
|
||||
let ident = variant.ident.clone();
|
||||
let meta = XmlCompoundMeta::parse_from_attributes(&variant.attrs)?;
|
||||
|
||||
// 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: _,
|
||||
qname: _, // used by StructInner
|
||||
ref exhaustive,
|
||||
ref debug,
|
||||
ref builder,
|
||||
ref iterator,
|
||||
transparent: _, // used by StructInner
|
||||
} = meta;
|
||||
|
||||
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!(builder not on "enum variants" only on "enums and structs");
|
||||
reject_key!(iterator not on "enum variants" only on "enums and structs");
|
||||
|
||||
let inner = StructInner::new(meta, &variant.fields)?;
|
||||
Ok(Self { ident, inner })
|
||||
}
|
||||
}
|
||||
|
||||
/// The definition of an enum where each variant is a completely unrelated
|
||||
/// possible XML subtree.
|
||||
struct DynamicEnum {
|
||||
/// The enum variants.
|
||||
variants: Vec<DynamicVariant>,
|
||||
}
|
||||
|
||||
impl DynamicEnum {
|
||||
fn new<'x, I: IntoIterator<Item = &'x Variant>>(variant_iter: I) -> Result<Self> {
|
||||
let mut variants = Vec::new();
|
||||
for variant in variant_iter {
|
||||
variants.push(DynamicVariant::new(variant)?);
|
||||
}
|
||||
|
||||
Ok(Self { variants })
|
||||
}
|
||||
|
||||
/// Build the deserialisation statemachine for the dynamic enum.
|
||||
fn make_from_events_statemachine(
|
||||
&self,
|
||||
target_ty_ident: &Ident,
|
||||
state_ty_ident: &Ident,
|
||||
) -> Result<FromEventsStateMachine> {
|
||||
let mut statemachine = FromEventsStateMachine::new();
|
||||
for variant in self.variants.iter() {
|
||||
let submachine = variant.inner.make_from_events_statemachine(
|
||||
state_ty_ident,
|
||||
&ParentRef::Named(Path {
|
||||
leading_colon: None,
|
||||
segments: [
|
||||
PathSegment::from(target_ty_ident.clone()),
|
||||
variant.ident.clone().into(),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}),
|
||||
&variant.ident.to_string(),
|
||||
)?;
|
||||
|
||||
statemachine.merge(submachine.compile());
|
||||
}
|
||||
|
||||
Ok(statemachine)
|
||||
}
|
||||
|
||||
/// Build the serialisation statemachine for the dynamic enum.
|
||||
fn make_as_item_iter_statemachine(
|
||||
&self,
|
||||
target_ty_ident: &Ident,
|
||||
state_ty_ident: &Ident,
|
||||
item_iter_ty_lifetime: &Lifetime,
|
||||
) -> Result<AsItemsStateMachine> {
|
||||
let mut statemachine = AsItemsStateMachine::new();
|
||||
for variant in self.variants.iter() {
|
||||
let submachine = variant.inner.make_as_item_iter_statemachine(
|
||||
&ParentRef::Named(Path {
|
||||
leading_colon: None,
|
||||
segments: [
|
||||
PathSegment::from(target_ty_ident.clone()),
|
||||
variant.ident.clone().into(),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}),
|
||||
state_ty_ident,
|
||||
&variant.ident.to_string(),
|
||||
item_iter_ty_lifetime,
|
||||
)?;
|
||||
|
||||
statemachine.merge(submachine.compile());
|
||||
}
|
||||
|
||||
Ok(statemachine)
|
||||
}
|
||||
}
|
||||
|
||||
/// The definition of an enum.
|
||||
enum EnumInner {
|
||||
/// The enum switches based on the XML name of the element, with the XML
|
||||
/// namespace fixed.
|
||||
NameSwitched(NameSwitchedEnum),
|
||||
|
||||
/// The enum consists of variants with entirely unrelated XML structures.
|
||||
Dynamic(DynamicEnum),
|
||||
}
|
||||
|
||||
impl EnumInner {
|
||||
fn new<'x, I: IntoIterator<Item = &'x Variant>>(
|
||||
meta: XmlCompoundMeta,
|
||||
variant_iter: I,
|
||||
) -> 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: _,
|
||||
qname: QNameRef { namespace, name },
|
||||
exhaustive,
|
||||
debug,
|
||||
builder,
|
||||
iterator,
|
||||
transparent,
|
||||
} = meta;
|
||||
|
||||
// These must've been cleared by the caller. Because these being set
|
||||
// is a programming error (in xso-proc) and not a usage error, we
|
||||
// assert here instead of using reject_key!.
|
||||
assert!(builder.is_none());
|
||||
assert!(iterator.is_none());
|
||||
assert!(!debug.is_set());
|
||||
|
||||
reject_key!(name not on "enums" only on "their variants");
|
||||
reject_key!(transparent flag not on "enums" only on "structs");
|
||||
|
||||
if let Some(namespace) = namespace {
|
||||
Ok(Self::NameSwitched(NameSwitchedEnum::new(
|
||||
namespace,
|
||||
exhaustive,
|
||||
variant_iter,
|
||||
)?))
|
||||
} else {
|
||||
reject_key!(exhaustive flag not on "dynamic enums" only on "name-switched enums");
|
||||
Ok(Self::Dynamic(DynamicEnum::new(variant_iter)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the deserialisation statemachine for the enum.
|
||||
fn make_from_events_statemachine(
|
||||
&self,
|
||||
target_ty_ident: &Ident,
|
||||
state_ty_ident: &Ident,
|
||||
) -> Result<FromEventsStateMachine> {
|
||||
match self {
|
||||
Self::NameSwitched(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the serialisation statemachine for the enum.
|
||||
fn make_as_item_iter_statemachine(
|
||||
&self,
|
||||
target_ty_ident: &Ident,
|
||||
state_ty_ident: &Ident,
|
||||
item_iter_ty_lifetime: &Lifetime,
|
||||
) -> Result<AsItemsStateMachine> {
|
||||
match self {
|
||||
Self::NameSwitched(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,
|
||||
item_iter_ty_lifetime,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Definition of an enum and how to parse it.
|
||||
pub(crate) struct EnumDef {
|
||||
/// Implementation of the enum itself
|
||||
inner: NameSwitchedEnum,
|
||||
inner: EnumInner,
|
||||
|
||||
/// Name of the target type.
|
||||
target_ty_ident: Ident,
|
||||
|
|
@ -299,7 +480,7 @@ impl EnumDef {
|
|||
let debug = meta.debug.take().is_set();
|
||||
|
||||
Ok(Self {
|
||||
inner: NameSwitchedEnum::new(meta, variant_iter)?,
|
||||
inner: EnumInner::new(meta, variant_iter)?,
|
||||
target_ty_ident: ident.clone(),
|
||||
builder_ty_ident,
|
||||
item_iter_ty_ident,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ 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($key) = $key {
|
||||
if let Some(ref $key) = $key {
|
||||
return Err(Error::new_spanned(
|
||||
$key,
|
||||
concat!(
|
||||
|
|
@ -43,9 +43,9 @@ macro_rules! reject_key {
|
|||
};
|
||||
|
||||
($key:ident flag not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => {
|
||||
if let Flag::Present($key) = $key {
|
||||
if let Flag::Present(ref $key) = $key {
|
||||
return Err(Error::new(
|
||||
$key,
|
||||
*$key,
|
||||
concat!(
|
||||
"`",
|
||||
stringify!($key),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use crate::types::{
|
|||
/// The inner parts of the struct.
|
||||
///
|
||||
/// This contains all data necessary for the matching logic.
|
||||
enum StructInner {
|
||||
pub(crate) enum StructInner {
|
||||
/// Single-field struct declared with `#[xml(transparent)]`.
|
||||
///
|
||||
/// Transparent struct delegate all parsing and serialising to their
|
||||
|
|
@ -58,7 +58,7 @@ enum StructInner {
|
|||
}
|
||||
|
||||
impl StructInner {
|
||||
fn new(meta: XmlCompoundMeta, fields: &Fields) -> Result<Self> {
|
||||
pub(crate) fn new(meta: XmlCompoundMeta, fields: &Fields) -> 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.
|
||||
|
|
@ -150,7 +150,7 @@ impl StructInner {
|
|||
}
|
||||
}
|
||||
|
||||
fn make_from_events_statemachine(
|
||||
pub(crate) fn make_from_events_statemachine(
|
||||
&self,
|
||||
state_ty_ident: &Ident,
|
||||
output_name: &ParentRef,
|
||||
|
|
@ -225,7 +225,7 @@ impl StructInner {
|
|||
}
|
||||
}
|
||||
|
||||
fn make_as_item_iter_statemachine(
|
||||
pub(crate) fn make_as_item_iter_statemachine(
|
||||
&self,
|
||||
input_name: &ParentRef,
|
||||
state_ty_ident: &Ident,
|
||||
|
|
|
|||
|
|
@ -81,9 +81,27 @@ implement [`FromXml`] in order to derive `FromXml` and [`AsXml`] in order to
|
|||
derive `AsXml`. The struct will be (de-)serialised exactly like the type of
|
||||
that single field. This allows a newtype-like pattern for XSO structs.
|
||||
|
||||
### Enum meta
|
||||
### Enums
|
||||
|
||||
The following keys are defined on enums:
|
||||
Two different `enum` flavors are supported:
|
||||
|
||||
1. [**Name-switched enums**](#name-switched-enum-meta) have a fixed XML
|
||||
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.
|
||||
|
||||
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
|
||||
keys and can thus be uniquely identified.
|
||||
|
||||
#### Name-switched enum meta
|
||||
|
||||
Name-switched enums match a fixed XML namespace and then select the enum
|
||||
variant based on the XML element's name. Name-switched enums are declared by
|
||||
setting the `namespace` key on a `enum` item.
|
||||
|
||||
The following keys are defined on name-switched enums:
|
||||
|
||||
| Key | Value type | Description |
|
||||
| --- | --- | --- |
|
||||
|
|
@ -92,10 +110,10 @@ The following keys are defined on enums:
|
|||
| `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.
|
||||
All variants of a name-switched 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
|
||||
|
|
@ -109,7 +127,7 @@ Note that the *exhaustive* flag is orthogonal to the Rust attribute
|
|||
For details on `builder` and `iterator`, see the [Struct meta](#struct-meta)
|
||||
documentation above.
|
||||
|
||||
#### Enum variant meta
|
||||
##### Name-switched enum variant meta
|
||||
|
||||
| Key | Value type | Description |
|
||||
| --- | --- | --- |
|
||||
|
|
@ -119,7 +137,7 @@ Note that the `name` value must be a valid XML element name, without colons.
|
|||
The namespace prefix, if any, is assigned automatically at serialisation time
|
||||
and cannot be overridden.
|
||||
|
||||
#### Example
|
||||
##### Example
|
||||
|
||||
```rust
|
||||
# use xso::FromXml;
|
||||
|
|
@ -145,6 +163,57 @@ let foo: Foo = xso::from_bytes(b"<b xmlns='urn:example' bar='hello'/>").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
|
||||
order. Dynamic enums are declared by not setting the `namespace` key on an
|
||||
`enum` item.
|
||||
|
||||
The following keys are defined on dynamic enums:
|
||||
|
||||
| Key | Value type | Description |
|
||||
| --- | --- | --- |
|
||||
| `builder` | optional *ident* | The name to use for the generated builder type. |
|
||||
| `iterator` | optional *ident* | The name to use for the generated iterator type. |
|
||||
|
||||
For details on `builder` and `iterator`, see the [Struct meta](#struct-meta)
|
||||
documentation above.
|
||||
|
||||
##### Dynamic enum variant meta
|
||||
|
||||
Dynamic enum variants are completely independent of one another and thus use
|
||||
the same meta structure as structs. See [Struct meta](#struct-meta) for
|
||||
details.
|
||||
|
||||
The `builder`, `iterator` and `debug` keys cannot be used on dynmaic enum
|
||||
variants.
|
||||
|
||||
##### Example
|
||||
|
||||
```rust
|
||||
# use xso::FromXml;
|
||||
#[derive(FromXml, Debug, PartialEq)]
|
||||
#[xml()]
|
||||
enum Foo {
|
||||
#[xml(namespace = "urn:example:ns1", name = "a")]
|
||||
Variant1 {
|
||||
#[xml(attribute)]
|
||||
foo: String,
|
||||
},
|
||||
#[xml(namespace = "urn:example:ns2", name = "b")]
|
||||
Variant2 {
|
||||
#[xml(attribute)]
|
||||
bar: String,
|
||||
},
|
||||
}
|
||||
|
||||
let foo: Foo = xso::from_bytes(b"<a xmlns='urn:example:ns1' foo='hello'/>").unwrap();
|
||||
assert_eq!(foo, Foo::Variant1 { foo: "hello".to_string() });
|
||||
|
||||
let foo: Foo = xso::from_bytes(b"<b xmlns='urn:example:ns2' bar='hello'/>").unwrap();
|
||||
assert_eq!(foo, Foo::Variant2 { bar: "hello".to_string() });
|
||||
```
|
||||
|
||||
### Field meta
|
||||
|
||||
For fields, the *meta* consists of a nested meta inside the `#[xml(..)]` meta,
|
||||
|
|
|
|||
Loading…
Reference in a new issue