diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 7cd690fcfefc936dcd47531df6c170188bcb3b6b..b9dc4c2480649b699b198a7b1cb4c818de877d40 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -1600,3 +1600,36 @@ fn transparent_struct_named_roundtrip() { }; roundtrip_full::(""); } + +#[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::(""); +} + +#[test] +fn dynamic_enum_roundtrip_b() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::("hello world"); +} diff --git a/xso-proc/src/enums.rs b/xso-proc/src/enums.rs index e4320f6277af1b25145b6e40d4dca251e7f6ff7a..ddd9af16e92dc59c3263171d536e0f41d127ee32 100644 --- a/xso-proc/src/enums.rs +++ 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>( - meta: XmlCompoundMeta, + namespace: NamespaceRef, + exhaustive: Flag, variant_iter: I, ) -> Result { - // 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 { + 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, +} + +impl DynamicEnum { + fn new<'x, I: IntoIterator>(variant_iter: I) -> Result { + 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 { + 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 { + 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>( + meta: XmlCompoundMeta, + variant_iter: I, + ) -> Result { + // 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 { + 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 { + 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, diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 69b4abc2a1e3b2cbb3e5e6131ecb300b1efe7e59..13e2d26ac54ad32d6e6a4ed1744096bc224882c9 100644 --- a/xso-proc/src/meta.rs +++ b/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), diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index 42fcc8b2b97c078ef4f04ccd8a53542e059b80f7..ef49a7f8786e3ddf37537f7d48e9ea663d59ad77 100644 --- a/xso-proc/src/structs.rs +++ b/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 { + pub(crate) fn new(meta: XmlCompoundMeta, fields: &Fields) -> Result { // 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, diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index 0e3090d87ebf820f241dd0961b375c433a27f263..17287984cd2a2ff19ad3cf23299ed332eb2dd5ec 100644 --- a/xso/src/from_xml_doc.md +++ b/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"").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"").unwrap(); +assert_eq!(foo, Foo::Variant1 { foo: "hello".to_string() }); + +let foo: Foo = xso::from_bytes(b"").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,