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'/>");
}
#[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::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,

View file

@ -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),

View file

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

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
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,