xso: add support for dynamic enums

This commit is contained in:
Jonas Schäfer 2024-08-05 16:35:10 +02:00 committed by Link Mauve
commit fb80bb7532
5 changed files with 329 additions and 46 deletions

View file

@ -1600,3 +1600,36 @@ fn transparent_struct_named_roundtrip() {
}; };
roundtrip_full::<TransparentStructNamed>("<attr xmlns='urn:example:ns1' foo='bar'/>"); 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>");
}

View file

@ -17,9 +17,11 @@ use crate::compound::Compound;
use crate::error_message::ParentRef; use crate::error_message::ParentRef;
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta}; use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
use crate::state::{AsItemsStateMachine, FromEventsStateMachine}; use crate::state::{AsItemsStateMachine, FromEventsStateMachine};
use crate::structs::StructInner;
use crate::types::{ref_ty, ty_from_ident}; 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 { struct NameVariant {
/// The XML name of the element to map the enum variant to. /// The XML name of the element to map the enum variant to.
name: NameRef, 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 { struct NameSwitchedEnum {
/// The XML namespace of the element to map the enum to. /// The XML namespace of the element to map the enum to.
namespace: NamespaceRef, namespace: NamespaceRef,
@ -153,36 +157,10 @@ struct NameSwitchedEnum {
impl NameSwitchedEnum { impl NameSwitchedEnum {
fn new<'x, I: IntoIterator<Item = &'x Variant>>( fn new<'x, I: IntoIterator<Item = &'x Variant>>(
meta: XmlCompoundMeta, namespace: NamespaceRef,
exhaustive: Flag,
variant_iter: I, variant_iter: I,
) -> Result<Self> { ) -> 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 variants = Vec::new();
let mut seen_names = HashMap::new(); let mut seen_names = HashMap::new();
for variant in variant_iter { 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( fn make_from_events_statemachine(
&self, &self,
target_ty_ident: &Ident, target_ty_ident: &Ident,
@ -241,6 +220,7 @@ impl NameSwitchedEnum {
Ok(statemachine) Ok(statemachine)
} }
/// Build the serialisation statemachine for the name-switched enum.
fn make_as_item_iter_statemachine( fn make_as_item_iter_statemachine(
&self, &self,
target_ty_ident: &Ident, 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. /// Definition of an enum and how to parse it.
pub(crate) struct EnumDef { pub(crate) struct EnumDef {
/// Implementation of the enum itself /// Implementation of the enum itself
inner: NameSwitchedEnum, inner: EnumInner,
/// Name of the target type. /// Name of the target type.
target_ty_ident: Ident, target_ty_ident: Ident,
@ -299,7 +480,7 @@ impl EnumDef {
let debug = meta.debug.take().is_set(); let debug = meta.debug.take().is_set();
Ok(Self { Ok(Self {
inner: NameSwitchedEnum::new(meta, variant_iter)?, inner: EnumInner::new(meta, variant_iter)?,
target_ty_ident: ident.clone(), target_ty_ident: ident.clone(),
builder_ty_ident, builder_ty_ident,
item_iter_ty_ident, item_iter_ty_ident,

View file

@ -24,7 +24,7 @@ pub const XMLNS_XMLNS: &str = "http://www.w3.org/2000/xmlns/";
macro_rules! reject_key { macro_rules! reject_key {
($key:ident not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => { ($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( return Err(Error::new_spanned(
$key, $key,
concat!( concat!(
@ -43,9 +43,9 @@ macro_rules! reject_key {
}; };
($key:ident flag not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => { ($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( return Err(Error::new(
$key, *$key,
concat!( concat!(
"`", "`",
stringify!($key), stringify!($key),

View file

@ -23,7 +23,7 @@ use crate::types::{
/// The inner parts of the struct. /// The inner parts of the struct.
/// ///
/// This contains all data necessary for the matching logic. /// This contains all data necessary for the matching logic.
enum StructInner { pub(crate) enum StructInner {
/// Single-field struct declared with `#[xml(transparent)]`. /// Single-field struct declared with `#[xml(transparent)]`.
/// ///
/// Transparent struct delegate all parsing and serialising to their /// Transparent struct delegate all parsing and serialising to their
@ -58,7 +58,7 @@ enum StructInner {
} }
impl 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 // We destructure here so that we get informed when new fields are
// added and can handle them, either by processing them or raising // added and can handle them, either by processing them or raising
// an error if they are present. // 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, &self,
state_ty_ident: &Ident, state_ty_ident: &Ident,
output_name: &ParentRef, output_name: &ParentRef,
@ -225,7 +225,7 @@ impl StructInner {
} }
} }
fn make_as_item_iter_statemachine( pub(crate) fn make_as_item_iter_statemachine(
&self, &self,
input_name: &ParentRef, input_name: &ParentRef,
state_ty_ident: &Ident, state_ty_ident: &Ident,

View file

@ -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 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. 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 | | 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. | | `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. | | `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 All variants of a name-switched enum live within the same namespace and are
exclusively by their XML name within that namespace. The contents of the XML distinguished exclusively by their XML name within that namespace. The
element (including attributes) is not inspected before selecting the variant contents of the XML element (including attributes) is not inspected before
when parsing XML. selecting the variant when parsing XML.
If *exhaustive* is set and an element is encountered which matches the 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 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) For details on `builder` and `iterator`, see the [Struct meta](#struct-meta)
documentation above. documentation above.
#### Enum variant meta ##### Name-switched enum variant meta
| Key | Value type | Description | | 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 The namespace prefix, if any, is assigned automatically at serialisation time
and cannot be overridden. and cannot be overridden.
#### Example ##### Example
```rust ```rust
# use xso::FromXml; # 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() }); 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 ### Field meta
For fields, the *meta* consists of a nested meta inside the `#[xml(..)]` meta, For fields, the *meta* consists of a nested meta inside the `#[xml(..)]` meta,