xso: add support for dynamic enums

Jonas SchΓ€fer created

Change summary

parsers/src/util/macro_tests.rs |  33 ++++
xso-proc/src/enums.rs           | 243 ++++++++++++++++++++++++++++++----
xso-proc/src/meta.rs            |   6 
xso-proc/src/structs.rs         |   8 
xso/src/from_xml_doc.md         |  85 +++++++++++-
5 files changed, 329 insertions(+), 46 deletions(-)

Detailed changes

parsers/src/util/macro_tests.rs πŸ”—

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

xso-proc/src/enums.rs πŸ”—

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

xso-proc/src/meta.rs πŸ”—

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

xso-proc/src/structs.rs πŸ”—

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

xso/src/from_xml_doc.md πŸ”—

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