Detailed changes
@@ -617,3 +617,120 @@ fn renamed_types_get_renamed() {
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
#[xml(namespace = NS1, name = "elem")]
struct LintTest_;
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1)]
+enum NameSwitchedEnum {
+ #[xml(name = "a")]
+ Variant1 {
+ #[xml(attribute)]
+ foo: String,
+ },
+ #[xml(name = "b")]
+ Variant2 {
+ #[xml(text)]
+ foo: String,
+ },
+}
+
+#[test]
+fn name_switched_enum_positive_variant_1() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<NameSwitchedEnum>("<a xmlns='urn:example:ns1' foo='hello'/>") {
+ Ok(NameSwitchedEnum::Variant1 { foo }) => {
+ assert_eq!(foo, "hello");
+ }
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn name_switched_enum_positive_variant_2() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<NameSwitchedEnum>("<b xmlns='urn:example:ns1'>hello</b>") {
+ Ok(NameSwitchedEnum::Variant2 { foo }) => {
+ assert_eq!(foo, "hello");
+ }
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn name_switched_enum_negative_name_mismatch() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<NameSwitchedEnum>("<x xmlns='urn:example:ns1'>hello</x>") {
+ Err(xso::error::FromElementError::Mismatch { .. }) => (),
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn name_switched_enum_negative_namespace_mismatch() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<NameSwitchedEnum>("<b xmlns='urn:example:ns2'>hello</b>") {
+ Err(xso::error::FromElementError::Mismatch { .. }) => (),
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn name_switched_enum_roundtrip_variant_1() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::<NameSwitchedEnum>("<a xmlns='urn:example:ns1' foo='hello'/>")
+}
+
+#[test]
+fn name_switched_enum_roundtrip_variant_2() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::<NameSwitchedEnum>("<b xmlns='urn:example:ns1'>hello</b>")
+}
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, builder = RenamedEnumBuilder, iterator = RenamedEnumIter)]
+enum RenamedEnumTypes {
+ #[xml(name = "elem")]
+ A,
+}
+
+#[test]
+fn renamed_enum_types_roundtrip() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::<RenamedEnumTypes>("<elem xmlns='urn:example:ns1'/>")
+}
+
+#[test]
+#[allow(unused_comparisons)]
+fn renamed_enum_types_get_renamed() {
+ // these merely serve as a test that the types are declared with the names
+ // given in the attributes.
+ assert!(std::mem::size_of::<RenamedEnumBuilder>() >= 0);
+ assert!(std::mem::size_of::<RenamedEnumIter>() >= 0);
+}
@@ -297,7 +297,7 @@ impl Compound {
/// `rxml::QName` is in scope.
pub(crate) fn make_as_item_iter_statemachine(
&self,
- input_name: &Path,
+ input_name: &ParentRef,
state_prefix: &str,
lifetime: &Lifetime,
) -> Result<AsItemsSubmachine> {
@@ -430,11 +430,13 @@ impl Compound {
}),
);
+ let ParentRef::Named(input_path) = input_name;
+
Ok(AsItemsSubmachine {
defs: TokenStream::default(),
states,
destructure: quote! {
- #input_name { #destructure }
+ #input_path { #destructure }
},
init: quote! {
Self::#element_head_start_state_ident { #dummy_ident: ::std::marker::PhantomData, #name_ident: name.1, #ns_ident: name.0, #start_init }
@@ -0,0 +1,319 @@
+// Copyright (c) 2024 Jonas Schรคfer <jonas@zombofant.net>
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+//! Handling of enums
+
+use std::collections::HashMap;
+
+use proc_macro2::Span;
+use quote::quote;
+use syn::*;
+
+use crate::common::{AsXmlParts, FromXmlParts, ItemDef};
+use crate::compound::Compound;
+use crate::error_message::ParentRef;
+use crate::meta::{NameRef, NamespaceRef, XmlCompoundMeta};
+use crate::state::{AsItemsStateMachine, FromEventsStateMachine};
+
+/// The definition of an enum variant, switched on the XML element's name.
+struct NameVariant {
+ /// The XML name of the element to map the enum variant to.
+ name: NameRef,
+
+ /// The name of the variant
+ ident: Ident,
+
+ /// The field(s) of this struct.
+ inner: Compound,
+}
+
+impl NameVariant {
+ /// Construct a new name-selected variant from its declaration.
+ fn new(decl: &Variant) -> Result<Self> {
+ let meta = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;
+
+ if let Some(namespace) = meta.namespace {
+ return Err(Error::new_spanned(
+ namespace,
+ "`namespace` is not allowed on enum variants (only on enums and structs)",
+ ));
+ }
+
+ let Some(name) = meta.name else {
+ return Err(Error::new(meta.span, "`name` is required on enum variants"));
+ };
+
+ Ok(Self {
+ name,
+ ident: decl.ident.clone(),
+ inner: Compound::from_fields(&decl.fields)?,
+ })
+ }
+
+ fn make_from_events_statemachine(
+ &self,
+ enum_ident: &Ident,
+ state_ty_ident: &Ident,
+ ) -> Result<FromEventsStateMachine> {
+ let xml_name = &self.name;
+
+ Ok(self
+ .inner
+ .make_from_events_statemachine(
+ state_ty_ident,
+ &ParentRef::Named(Path {
+ leading_colon: None,
+ segments: [
+ PathSegment::from(enum_ident.clone()),
+ self.ident.clone().into(),
+ ]
+ .into_iter()
+ .collect(),
+ }),
+ &self.ident.to_string(),
+ )?
+ .with_augmented_init(|init| {
+ quote! {
+ if name.1 != #xml_name {
+ ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch {
+ name,
+ attrs,
+ })
+ } else {
+ #init
+ }
+ }
+ })
+ .compile())
+ }
+
+ fn make_as_item_iter_statemachine(
+ &self,
+ xml_namespace: &NamespaceRef,
+ enum_ident: &Ident,
+ item_iter_ty_lifetime: &Lifetime,
+ ) -> Result<AsItemsStateMachine> {
+ let xml_name = &self.name;
+
+ Ok(self
+ .inner
+ .make_as_item_iter_statemachine(
+ &ParentRef::Named(Path {
+ leading_colon: None,
+ segments: [
+ PathSegment::from(enum_ident.clone()),
+ self.ident.clone().into(),
+ ]
+ .into_iter()
+ .collect(),
+ }),
+ &self.ident.to_string(),
+ &item_iter_ty_lifetime,
+ )?
+ .with_augmented_init(|init| {
+ quote! {
+ let name = (
+ ::xso::exports::rxml::Namespace::from(#xml_namespace),
+ ::std::borrow::Cow::Borrowed(#xml_name),
+ );
+ #init
+ }
+ })
+ .compile())
+ }
+}
+
+/// Definition of an enum and how to parse it.
+pub(crate) struct EnumDef {
+ /// The XML namespace of the element to map the enum to.
+ namespace: NamespaceRef,
+
+ /// The variants of the enum.
+ variants: Vec<NameVariant>,
+
+ /// Name of the target type.
+ target_ty_ident: Ident,
+
+ /// Name of the builder type.
+ builder_ty_ident: Ident,
+
+ /// Name of the iterator type.
+ item_iter_ty_ident: Ident,
+
+ /// Flag whether debug mode is enabled.
+ debug: bool,
+}
+
+impl EnumDef {
+ /// Create a new enum from its name, meta, and variants.
+ pub(crate) fn new<'x, I: IntoIterator<Item = &'x Variant>>(
+ ident: &Ident,
+ meta: XmlCompoundMeta,
+ variant_iter: I,
+ ) -> Result<Self> {
+ if let Some(name) = meta.name {
+ return Err(Error::new_spanned(
+ name,
+ "`name` is not allowed on enums (only on their variants)",
+ ));
+ }
+
+ let Some(namespace) = meta.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 {
+ let variant = NameVariant::new(variant)?;
+ if let Some(other) = seen_names.get(&variant.name) {
+ return Err(Error::new_spanned(
+ variant.name,
+ format!(
+ "duplicate `name` in enum: variants {} and {} have the same XML name",
+ other, variant.ident
+ ),
+ ));
+ }
+ seen_names.insert(variant.name.clone(), variant.ident.clone());
+ variants.push(variant);
+ }
+
+ let builder_ty_ident = match meta.builder {
+ Some(v) => v,
+ None => quote::format_ident!("{}FromXmlBuilder", ident.to_string()),
+ };
+
+ let item_iter_ty_ident = match meta.iterator {
+ Some(v) => v,
+ None => quote::format_ident!("{}AsXmlIterator", ident.to_string()),
+ };
+
+ Ok(Self {
+ namespace,
+ variants,
+ target_ty_ident: ident.clone(),
+ builder_ty_ident,
+ item_iter_ty_ident,
+ debug: meta.debug.is_set(),
+ })
+ }
+}
+
+impl ItemDef for EnumDef {
+ fn make_from_events_builder(
+ &self,
+ vis: &Visibility,
+ name_ident: &Ident,
+ attrs_ident: &Ident,
+ ) -> Result<FromXmlParts> {
+ let xml_namespace = &self.namespace;
+ let target_ty_ident = &self.target_ty_ident;
+ let builder_ty_ident = &self.builder_ty_ident;
+ let state_ty_ident = quote::format_ident!("{}State", builder_ty_ident);
+
+ let mut statemachine = FromEventsStateMachine::new();
+ for variant in self.variants.iter() {
+ statemachine
+ .merge(variant.make_from_events_statemachine(target_ty_ident, &state_ty_ident)?);
+ }
+
+ statemachine.set_pre_init(quote! {
+ if name.0 != #xml_namespace {
+ return ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch {
+ name,
+ attrs,
+ })
+ }
+ });
+
+ let defs = statemachine.render(
+ vis,
+ builder_ty_ident,
+ &state_ty_ident,
+ &TypePath {
+ qself: None,
+ path: target_ty_ident.clone().into(),
+ }
+ .into(),
+ )?;
+
+ Ok(FromXmlParts {
+ defs,
+ from_events_body: quote! {
+ #builder_ty_ident::new(#name_ident, #attrs_ident)
+ },
+ builder_ty_ident: builder_ty_ident.clone(),
+ })
+ }
+
+ fn make_as_xml_iter(&self, vis: &Visibility) -> Result<AsXmlParts> {
+ let target_ty_ident = &self.target_ty_ident;
+ let item_iter_ty_ident = &self.item_iter_ty_ident;
+ let item_iter_ty_lifetime = Lifetime {
+ apostrophe: Span::call_site(),
+ ident: Ident::new("xso_proc_as_xml_iter_lifetime", Span::call_site()),
+ };
+ let item_iter_ty = Type::Path(TypePath {
+ qself: None,
+ path: Path {
+ leading_colon: None,
+ segments: [PathSegment {
+ ident: item_iter_ty_ident.clone(),
+ arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments {
+ colon2_token: None,
+ lt_token: token::Lt {
+ spans: [Span::call_site()],
+ },
+ args: [GenericArgument::Lifetime(item_iter_ty_lifetime.clone())]
+ .into_iter()
+ .collect(),
+ gt_token: token::Gt {
+ spans: [Span::call_site()],
+ },
+ }),
+ }]
+ .into_iter()
+ .collect(),
+ },
+ });
+ let state_ty_ident = quote::format_ident!("{}State", item_iter_ty_ident);
+
+ let mut statemachine = AsItemsStateMachine::new();
+ for variant in self.variants.iter() {
+ statemachine.merge(variant.make_as_item_iter_statemachine(
+ &self.namespace,
+ target_ty_ident,
+ &item_iter_ty_lifetime,
+ )?);
+ }
+
+ let defs = statemachine.render(
+ vis,
+ &TypePath {
+ qself: None,
+ path: target_ty_ident.clone().into(),
+ }
+ .into(),
+ &state_ty_ident,
+ &item_iter_ty_lifetime,
+ &item_iter_ty,
+ )?;
+
+ Ok(AsXmlParts {
+ defs,
+ as_xml_iter_body: quote! {
+ #item_iter_ty_ident::new(self)
+ },
+ item_iter_ty,
+ item_iter_ty_lifetime,
+ })
+ }
+
+ fn debug(&self) -> bool {
+ self.debug
+ }
+}
@@ -27,6 +27,7 @@ use syn::*;
mod common;
mod compound;
+mod enums;
mod error_message;
mod field;
mod meta;
@@ -41,12 +42,17 @@ use common::{AsXmlParts, FromXmlParts, ItemDef};
///
/// If the item is of an unsupported variant, an appropriate error is
/// returned.
-fn parse_struct(item: Item) -> Result<(Visibility, Ident, structs::StructDef)> {
+fn parse_struct(item: Item) -> Result<(Visibility, Ident, Box<dyn ItemDef>)> {
match item {
Item::Struct(item) => {
let meta = meta::XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
let def = structs::StructDef::new(&item.ident, meta, &item.fields)?;
- Ok((item.vis, item.ident, def))
+ Ok((item.vis, item.ident, Box::new(def)))
+ }
+ Item::Enum(item) => {
+ let meta = meta::XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
+ let def = enums::EnumDef::new(&item.ident, meta, &item.variants)?;
+ Ok((item.vis, item.ident, Box::new(def)))
}
other => Err(Error::new_spanned(other, "cannot derive on this item")),
}
@@ -9,6 +9,8 @@
//! This module is concerned with parsing attributes from the Rust "meta"
//! annotations on structs, enums, enum variants and fields.
+use std::hash::{Hash, Hasher};
+
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{meta::ParseNestedMeta, spanned::Spanned, *};
@@ -56,7 +58,7 @@ impl quote::ToTokens for NamespaceRef {
}
/// Value for the `#[xml(name = .. )]` attribute.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub(crate) enum NameRef {
/// The XML name is specified as a string literal.
Literal {
@@ -71,6 +73,38 @@ pub(crate) enum NameRef {
Path(Path),
}
+impl Hash for NameRef {
+ fn hash<H: Hasher>(&self, h: &mut H) {
+ match self {
+ Self::Literal { ref value, .. } => value.hash(h),
+ Self::Path(ref path) => path.hash(h),
+ }
+ }
+}
+
+impl PartialEq for NameRef {
+ fn eq(&self, other: &NameRef) -> bool {
+ match self {
+ Self::Literal {
+ value: ref my_value,
+ ..
+ } => match other {
+ Self::Literal {
+ value: ref other_value,
+ ..
+ } => my_value == other_value,
+ _ => false,
+ },
+ Self::Path(ref my_path) => match other {
+ Self::Path(ref other_path) => my_path == other_path,
+ _ => false,
+ },
+ }
+ }
+}
+
+impl Eq for NameRef {}
+
impl syn::parse::Parse for NameRef {
fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
if input.peek(syn::LitStr) {
@@ -176,6 +176,7 @@ impl FromEventsSubmachine {
state_defs,
advance_match_arms,
variants: vec![FromEventsEntryPoint { init: self.init }],
+ pre_init: TokenStream::default(),
}
}
@@ -369,6 +370,9 @@ pub(crate) struct FromEventsStateMachine {
/// Extra items which are needed for the state machine implementation.
defs: TokenStream,
+ /// Extra code run during pre-init phase.
+ pre_init: TokenStream,
+
/// A sequence of enum variant declarations, separated and terminated by
/// commas.
state_defs: TokenStream,
@@ -390,6 +394,36 @@ pub(crate) struct FromEventsStateMachine {
}
impl FromEventsStateMachine {
+ /// Create a new, empty state machine.
+ pub(crate) fn new() -> Self {
+ Self {
+ defs: TokenStream::default(),
+ state_defs: TokenStream::default(),
+ advance_match_arms: TokenStream::default(),
+ pre_init: TokenStream::default(),
+ variants: Vec::new(),
+ }
+ }
+
+ /// Merge another state machine into this state machine.
+ ///
+ /// This *discards* the other state machine's pre-init code.
+ pub(crate) fn merge(&mut self, other: FromEventsStateMachine) {
+ self.defs.extend(other.defs);
+ self.state_defs.extend(other.state_defs);
+ self.advance_match_arms.extend(other.advance_match_arms);
+ self.variants.extend(other.variants);
+ }
+
+ /// Set additional code to inject at the head of the `new` method for the
+ /// builder.
+ ///
+ /// This can be used to do preliminary checks and is commonly used with
+ /// specifically-formed init codes on the variants.
+ pub(crate) fn set_pre_init(&mut self, code: TokenStream) {
+ self.pre_init = code;
+ }
+
/// Render the state machine as a token stream.
///
/// The token stream contains the following pieces:
@@ -411,9 +445,10 @@ impl FromEventsStateMachine {
state_defs,
advance_match_arms,
variants,
+ pre_init,
} = self;
- let mut init_body = TokenStream::default();
+ let mut init_body = pre_init;
for variant in variants {
let FromEventsEntryPoint { init } = variant;
init_body.extend(quote! {
@@ -550,6 +585,24 @@ pub(crate) struct AsItemsStateMachine {
}
impl AsItemsStateMachine {
+ /// Create a new, empty state machine.
+ pub(crate) fn new() -> Self {
+ Self {
+ defs: TokenStream::default(),
+ state_defs: TokenStream::default(),
+ advance_match_arms: TokenStream::default(),
+ variants: Vec::new(),
+ }
+ }
+
+ /// Merge another state machine into this state machine.
+ pub(crate) fn merge(&mut self, other: AsItemsStateMachine) {
+ self.defs.extend(other.defs);
+ self.state_defs.extend(other.state_defs);
+ self.advance_match_arms.extend(other.advance_match_arms);
+ self.variants.extend(other.variants);
+ }
+
/// Render the state machine as a token stream.
///
/// The token stream contains the following pieces:
@@ -588,7 +641,7 @@ impl AsItemsStateMachine {
let mut match_arms = TokenStream::default();
for AsItemsEntryPoint { destructure, init } in variants {
match_arms.extend(quote! {
- #destructure => #init,
+ #destructure => { #init }
});
}
@@ -163,7 +163,7 @@ impl ItemDef for StructDef {
let defs = self
.inner
.make_as_item_iter_statemachine(
- &target_ty_ident.clone().into(),
+ &Path::from(target_ty_ident.clone()).into(),
"Struct",
&item_iter_ty_lifetime,
)?
@@ -5,6 +5,7 @@ Version NEXT:
be wrapped in Option or Box.
- Support for overriding the names of the types generated by the derive
macros.
+ - Support for deriving FromXml and AsXml on enums.
Version 0.1.2:
2024-07-26 Jonas Schรคfer <jonas@zombofant.net>
@@ -71,6 +71,60 @@ By default, the builder type uses the type's name suffixed with
`FromXmlBuilder` and the iterator type uses the type's name suffixed with
`AsXmlIterator`.
+### Enum meta
+
+The following keys are defined on enums:
+
+| Key | Value type | Description |
+| --- | --- | --- |
+| `namespace` | *string literal* or *path* | The XML element namespace to match for this enum. If it is a *path*, it must point at a `&'static str`. |
+| `builder` | optional *ident* | The name to use for the generated builder type. |
+| `iterator` | optional *ident* | The name to use for the generated iterator type. |
+
+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.
+
+For details on `builder` and `iterator`, see the [Struct meta](#struct-meta)
+documentation above.
+
+#### Enum variant meta
+
+| Key | Value type | Description |
+| --- | --- | --- |
+| `name` | *string literal* or *path* | The XML element name to match for this variant. If it is a *path*, it must point at a `&'static NcNameStr`. |
+
+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
+
+```rust
+# use xso::FromXml;
+#[derive(FromXml, Debug, PartialEq)]
+#[xml(namespace = "urn:example")]
+enum Foo {
+ #[xml(name = "a")]
+ Variant1 {
+ #[xml(attribute)]
+ foo: String,
+ },
+ #[xml(name = "b")]
+ Variant2 {
+ #[xml(attribute)]
+ bar: String,
+ },
+}
+
+let foo: Foo = xso::from_bytes(b"<a xmlns='urn:example' foo='hello'/>").unwrap();
+assert_eq!(foo, Foo::Variant1 { foo: "hello".to_string() });
+
+let foo: Foo = xso::from_bytes(b"<b xmlns='urn:example' 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,