From 4845715add14579b6068cef46d9dc9a9abbd5c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sun, 30 Jun 2024 09:06:10 +0200 Subject: [PATCH] xso: implement support for enums --- parsers/src/util/macro_tests.rs | 117 ++++++++++++ xso-proc/src/compound.rs | 6 +- xso-proc/src/enums.rs | 319 ++++++++++++++++++++++++++++++++ xso-proc/src/lib.rs | 10 +- xso-proc/src/meta.rs | 36 +++- xso-proc/src/state.rs | 57 +++++- xso-proc/src/structs.rs | 2 +- xso/ChangeLog | 1 + xso/src/from_xml_doc.md | 54 ++++++ 9 files changed, 594 insertions(+), 8 deletions(-) create mode 100644 xso-proc/src/enums.rs diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 191be4c3..104e9e11 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -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::("") { + 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::("hello") { + 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::("hello") { + 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::("hello") { + 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::("") +} + +#[test] +fn name_switched_enum_roundtrip_variant_2() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::("hello") +} + +#[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::("") +} + +#[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::() >= 0); + assert!(std::mem::size_of::() >= 0); +} diff --git a/xso-proc/src/compound.rs b/xso-proc/src/compound.rs index d1193b17..ec18830b 100644 --- a/xso-proc/src/compound.rs +++ b/xso-proc/src/compound.rs @@ -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 { @@ -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 } diff --git a/xso-proc/src/enums.rs b/xso-proc/src/enums.rs new file mode 100644 index 00000000..2ee4fd37 --- /dev/null +++ b/xso-proc/src/enums.rs @@ -0,0 +1,319 @@ +// Copyright (c) 2024 Jonas Schäfer +// +// 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 { + 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 { + 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 { + 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, + + /// 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>( + ident: &Ident, + meta: XmlCompoundMeta, + variant_iter: I, + ) -> Result { + 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 { + 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 { + 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 + } +} diff --git a/xso-proc/src/lib.rs b/xso-proc/src/lib.rs index 95d65794..ce2385b6 100644 --- a/xso-proc/src/lib.rs +++ b/xso-proc/src/lib.rs @@ -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)> { 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")), } diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index e751f21f..e954ae2d 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -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(&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 { if input.peek(syn::LitStr) { diff --git a/xso-proc/src/state.rs b/xso-proc/src/state.rs index 6611b984..fb817a8f 100644 --- a/xso-proc/src/state.rs +++ b/xso-proc/src/state.rs @@ -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 } }); } diff --git a/xso-proc/src/structs.rs b/xso-proc/src/structs.rs index 6c91ddd8..99417d02 100644 --- a/xso-proc/src/structs.rs +++ b/xso-proc/src/structs.rs @@ -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, )? diff --git a/xso/ChangeLog b/xso/ChangeLog index 251469ce..f015e9de 100644 --- a/xso/ChangeLog +++ b/xso/ChangeLog @@ -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 diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index dad9c81d..8b76ecd3 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -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"").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,