xso: implement support for enums
This commit is contained in:
parent
3a56b2bb10
commit
4845715add
9 changed files with 594 additions and 8 deletions
|
|
@ -617,3 +617,120 @@ fn renamed_types_get_renamed() {
|
||||||
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||||
#[xml(namespace = NS1, name = "elem")]
|
#[xml(namespace = NS1, name = "elem")]
|
||||||
struct LintTest_;
|
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.
|
/// `rxml::QName` is in scope.
|
||||||
pub(crate) fn make_as_item_iter_statemachine(
|
pub(crate) fn make_as_item_iter_statemachine(
|
||||||
&self,
|
&self,
|
||||||
input_name: &Path,
|
input_name: &ParentRef,
|
||||||
state_prefix: &str,
|
state_prefix: &str,
|
||||||
lifetime: &Lifetime,
|
lifetime: &Lifetime,
|
||||||
) -> Result<AsItemsSubmachine> {
|
) -> Result<AsItemsSubmachine> {
|
||||||
|
|
@ -430,11 +430,13 @@ impl Compound {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let ParentRef::Named(input_path) = input_name;
|
||||||
|
|
||||||
Ok(AsItemsSubmachine {
|
Ok(AsItemsSubmachine {
|
||||||
defs: TokenStream::default(),
|
defs: TokenStream::default(),
|
||||||
states,
|
states,
|
||||||
destructure: quote! {
|
destructure: quote! {
|
||||||
#input_name { #destructure }
|
#input_path { #destructure }
|
||||||
},
|
},
|
||||||
init: quote! {
|
init: quote! {
|
||||||
Self::#element_head_start_state_ident { #dummy_ident: ::std::marker::PhantomData, #name_ident: name.1, #ns_ident: name.0, #start_init }
|
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
319
xso-proc/src/enums.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ use syn::*;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
mod compound;
|
mod compound;
|
||||||
|
mod enums;
|
||||||
mod error_message;
|
mod error_message;
|
||||||
mod field;
|
mod field;
|
||||||
mod meta;
|
mod meta;
|
||||||
|
|
@ -41,12 +42,17 @@ use common::{AsXmlParts, FromXmlParts, ItemDef};
|
||||||
///
|
///
|
||||||
/// If the item is of an unsupported variant, an appropriate error is
|
/// If the item is of an unsupported variant, an appropriate error is
|
||||||
/// returned.
|
/// returned.
|
||||||
fn parse_struct(item: Item) -> Result<(Visibility, Ident, structs::StructDef)> {
|
fn parse_struct(item: Item) -> Result<(Visibility, Ident, Box<dyn ItemDef>)> {
|
||||||
match item {
|
match item {
|
||||||
Item::Struct(item) => {
|
Item::Struct(item) => {
|
||||||
let meta = meta::XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
|
let meta = meta::XmlCompoundMeta::parse_from_attributes(&item.attrs)?;
|
||||||
let def = structs::StructDef::new(&item.ident, meta, &item.fields)?;
|
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")),
|
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"
|
//! This module is concerned with parsing attributes from the Rust "meta"
|
||||||
//! annotations on structs, enums, enum variants and fields.
|
//! annotations on structs, enums, enum variants and fields.
|
||||||
|
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use proc_macro2::{Span, TokenStream};
|
use proc_macro2::{Span, TokenStream};
|
||||||
use quote::{quote, quote_spanned};
|
use quote::{quote, quote_spanned};
|
||||||
use syn::{meta::ParseNestedMeta, spanned::Spanned, *};
|
use syn::{meta::ParseNestedMeta, spanned::Spanned, *};
|
||||||
|
|
@ -56,7 +58,7 @@ impl quote::ToTokens for NamespaceRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Value for the `#[xml(name = .. )]` attribute.
|
/// Value for the `#[xml(name = .. )]` attribute.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) enum NameRef {
|
pub(crate) enum NameRef {
|
||||||
/// The XML name is specified as a string literal.
|
/// The XML name is specified as a string literal.
|
||||||
Literal {
|
Literal {
|
||||||
|
|
@ -71,6 +73,38 @@ pub(crate) enum NameRef {
|
||||||
Path(Path),
|
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 {
|
impl syn::parse::Parse for NameRef {
|
||||||
fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
|
fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
|
||||||
if input.peek(syn::LitStr) {
|
if input.peek(syn::LitStr) {
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ impl FromEventsSubmachine {
|
||||||
state_defs,
|
state_defs,
|
||||||
advance_match_arms,
|
advance_match_arms,
|
||||||
variants: vec![FromEventsEntryPoint { init: self.init }],
|
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.
|
/// Extra items which are needed for the state machine implementation.
|
||||||
defs: TokenStream,
|
defs: TokenStream,
|
||||||
|
|
||||||
|
/// Extra code run during pre-init phase.
|
||||||
|
pre_init: TokenStream,
|
||||||
|
|
||||||
/// A sequence of enum variant declarations, separated and terminated by
|
/// A sequence of enum variant declarations, separated and terminated by
|
||||||
/// commas.
|
/// commas.
|
||||||
state_defs: TokenStream,
|
state_defs: TokenStream,
|
||||||
|
|
@ -390,6 +394,36 @@ pub(crate) struct FromEventsStateMachine {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// Render the state machine as a token stream.
|
||||||
///
|
///
|
||||||
/// The token stream contains the following pieces:
|
/// The token stream contains the following pieces:
|
||||||
|
|
@ -411,9 +445,10 @@ impl FromEventsStateMachine {
|
||||||
state_defs,
|
state_defs,
|
||||||
advance_match_arms,
|
advance_match_arms,
|
||||||
variants,
|
variants,
|
||||||
|
pre_init,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let mut init_body = TokenStream::default();
|
let mut init_body = pre_init;
|
||||||
for variant in variants {
|
for variant in variants {
|
||||||
let FromEventsEntryPoint { init } = variant;
|
let FromEventsEntryPoint { init } = variant;
|
||||||
init_body.extend(quote! {
|
init_body.extend(quote! {
|
||||||
|
|
@ -550,6 +585,24 @@ pub(crate) struct AsItemsStateMachine {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// Render the state machine as a token stream.
|
||||||
///
|
///
|
||||||
/// The token stream contains the following pieces:
|
/// The token stream contains the following pieces:
|
||||||
|
|
@ -588,7 +641,7 @@ impl AsItemsStateMachine {
|
||||||
let mut match_arms = TokenStream::default();
|
let mut match_arms = TokenStream::default();
|
||||||
for AsItemsEntryPoint { destructure, init } in variants {
|
for AsItemsEntryPoint { destructure, init } in variants {
|
||||||
match_arms.extend(quote! {
|
match_arms.extend(quote! {
|
||||||
#destructure => #init,
|
#destructure => { #init }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ impl ItemDef for StructDef {
|
||||||
let defs = self
|
let defs = self
|
||||||
.inner
|
.inner
|
||||||
.make_as_item_iter_statemachine(
|
.make_as_item_iter_statemachine(
|
||||||
&target_ty_ident.clone().into(),
|
&Path::from(target_ty_ident.clone()).into(),
|
||||||
"Struct",
|
"Struct",
|
||||||
&item_iter_ty_lifetime,
|
&item_iter_ty_lifetime,
|
||||||
)?
|
)?
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ Version NEXT:
|
||||||
be wrapped in Option or Box.
|
be wrapped in Option or Box.
|
||||||
- Support for overriding the names of the types generated by the derive
|
- Support for overriding the names of the types generated by the derive
|
||||||
macros.
|
macros.
|
||||||
|
- Support for deriving FromXml and AsXml on enums.
|
||||||
|
|
||||||
Version 0.1.2:
|
Version 0.1.2:
|
||||||
2024-07-26 Jonas Schäfer <jonas@zombofant.net>
|
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
|
`FromXmlBuilder` and the iterator type uses the type's name suffixed with
|
||||||
`AsXmlIterator`.
|
`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
|
### Field meta
|
||||||
|
|
||||||
For fields, the *meta* consists of a nested meta inside the `#[xml(..)]` meta,
|
For fields, the *meta* consists of a nested meta inside the `#[xml(..)]` meta,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue