xso: implement support for enums

This commit is contained in:
Jonas Schäfer 2024-06-30 09:06:10 +02:00
parent 3a56b2bb10
commit 4845715add
9 changed files with 594 additions and 8 deletions

View file

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

View file

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

319
xso-proc/src/enums.rs Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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