From 1a6be5e46f932ea0ddfc1c7f70c8bb19c0dad874 Mon Sep 17 00:00:00 2001
From: rsdy
Date: Thu, 22 Sep 2022 10:57:04 +0100
Subject: [PATCH] Add scalar enum management
---
instant-xml-macros/src/de.rs | 41 ++++++-
instant-xml-macros/src/lib.rs | 202 ++++++++++++++++++++++++++++++++++
instant-xml-macros/src/ser.rs | 43 +++++++-
3 files changed, 284 insertions(+), 2 deletions(-)
diff --git a/instant-xml-macros/src/de.rs b/instant-xml-macros/src/de.rs
index 924f4a0..1f875da 100644
--- a/instant-xml-macros/src/de.rs
+++ b/instant-xml-macros/src/de.rs
@@ -1,18 +1,57 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
+use syn::spanned::Spanned;
-use super::{discard_lifetimes, ContainerMeta, FieldMeta, Namespace};
+use super::{discard_lifetimes, ContainerMeta, FieldMeta, Namespace, VariantMeta};
pub(crate) fn from_xml(input: &syn::DeriveInput) -> TokenStream {
let ident = &input.ident;
let meta = ContainerMeta::from_derive(input);
match &input.data {
+ syn::Data::Struct(_) if meta.scalar => {
+ syn::Error::new(input.span(), "scalar structs are unsupported!").to_compile_error()
+ }
syn::Data::Struct(ref data) => deserialize_struct(input, data, meta, ident),
+ syn::Data::Enum(_) if !meta.scalar => {
+ syn::Error::new(input.span(), "non-scalar enums are currently unsupported!")
+ .to_compile_error()
+ }
+ syn::Data::Enum(ref data) => deserialize_enum(input, data),
_ => todo!(),
}
}
+#[rustfmt::skip]
+fn deserialize_enum(input: &syn::DeriveInput, data: &syn::DataEnum) -> TokenStream {
+ let ident = &input.ident;
+ let mut variants = TokenStream::new();
+
+ for variant in data.variants.iter() {
+ let v_ident = &variant.ident;
+ let meta = match VariantMeta::from_variant(variant) {
+ Ok(meta) => meta,
+ Err(err) => return err.to_compile_error()
+ };
+
+ let serialize_as = meta.serialize_as;
+ variants.extend(quote!(Ok(#serialize_as) => #ident::#v_ident,));
+ }
+
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ quote!(
+ impl #impl_generics FromXml<'xml> for #ident #ty_generics #where_clause {
+ fn deserialize<'cx>(deserializer: &'cx mut ::instant_xml::Deserializer<'cx, 'xml>) -> Result {
+ match deserializer.take_str() {
+ #variants
+ _ => Err(::instant_xml::Error::UnexpectedValue)
+ }
+ }
+ }
+ )
+}
+
fn deserialize_struct(
input: &syn::DeriveInput,
data: &syn::DataStruct,
diff --git a/instant-xml-macros/src/lib.rs b/instant-xml-macros/src/lib.rs
index 3e4969d..c40c2ca 100644
--- a/instant-xml-macros/src/lib.rs
+++ b/instant-xml-macros/src/lib.rs
@@ -10,6 +10,7 @@ use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Span, TokenStream, To
use quote::ToTokens;
use syn::parse_macro_input;
use syn::punctuated::Punctuated;
+use syn::spanned::Spanned;
use syn::token::Colon2;
#[proc_macro_derive(ToXml, attributes(xml))]
@@ -28,6 +29,7 @@ pub fn from_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
struct ContainerMeta {
ns: NamespaceMeta,
rename: Option,
+ scalar: bool,
}
impl ContainerMeta {
@@ -38,6 +40,7 @@ impl ContainerMeta {
MetaItem::Attribute => panic!("attribute key invalid in container xml attribute"),
MetaItem::Ns(ns) => meta.ns = ns,
MetaItem::Rename(lit) => meta.rename = Some(lit),
+ MetaItem::Scalar => meta.scalar = true,
}
}
meta
@@ -59,12 +62,92 @@ impl FieldMeta {
MetaItem::Attribute => meta.attribute = true,
MetaItem::Ns(ns) => meta.ns = ns,
MetaItem::Rename(lit) => meta.rename = Some(lit),
+ MetaItem::Scalar => panic!("attribute 'scalar' invalid in field xml attribute"),
}
}
meta
}
}
+#[derive(Debug, Default)]
+struct VariantMeta {
+ serialize_as: TokenStream,
+}
+
+impl VariantMeta {
+ fn from_variant(input: &syn::Variant) -> Result {
+ if !input.fields.is_empty() {
+ return Err(syn::Error::new(
+ input.fields.span(),
+ "only unit enum variants are permitted!",
+ ));
+ }
+
+ let mut rename = None;
+ for item in meta_items(&input.attrs) {
+ match item {
+ MetaItem::Attribute => {
+ return Err(syn::Error::new(
+ input.span(),
+ "attribute 'attribute' is invalid for enum variants",
+ ))
+ }
+ MetaItem::Ns(_ns) => {
+ return Err(syn::Error::new(
+ input.span(),
+ "attribute 'ns' is invalid for enum variants",
+ ))
+ }
+ MetaItem::Rename(lit) => rename = Some(lit.to_token_stream()),
+ MetaItem::Scalar => {
+ return Err(syn::Error::new(
+ input.span(),
+ "attribute 'scalar' is invalid for enum variants",
+ ))
+ }
+ }
+ }
+
+ let discriminant = match input.discriminant {
+ Some((
+ _,
+ syn::Expr::Lit(syn::ExprLit {
+ lit: syn::Lit::Str(ref lit),
+ ..
+ }),
+ )) => Some(lit.to_token_stream()),
+ Some((
+ _,
+ syn::Expr::Lit(syn::ExprLit {
+ lit: syn::Lit::Int(ref lit),
+ ..
+ }),
+ )) => Some(lit.base10_digits().to_token_stream()),
+ Some((_, ref value)) => {
+ return Err(syn::Error::new(
+ value.span(),
+ "invalid field discriminant value!",
+ ))
+ }
+ None => None,
+ };
+
+ if discriminant.is_some() && rename.is_some() {
+ return Err(syn::Error::new(
+ input.span(),
+ "conflicting `rename` attribute and variant discriminant!",
+ ));
+ }
+
+ let serialize_as = match rename.or(discriminant) {
+ Some(lit) => lit.into_token_stream(),
+ None => input.ident.to_string().to_token_stream(),
+ };
+
+ Ok(VariantMeta { serialize_as })
+ }
+}
+
#[derive(Debug, Default)]
struct NamespaceMeta {
uri: Option,
@@ -352,6 +435,9 @@ fn meta_items(attrs: &[syn::Attribute]) -> Vec {
MetaState::Ns
} else if id == "rename" {
MetaState::Rename
+ } else if id == "scalar" {
+ items.push(MetaItem::Scalar);
+ MetaState::Comma
} else {
panic!("unexpected key in xml attribute");
}
@@ -460,6 +546,7 @@ enum MetaItem {
Attribute,
Ns(NamespaceMeta),
Rename(Literal),
+ Scalar,
}
enum Namespace {
@@ -524,3 +611,118 @@ fn discard_path_lifetimes(path: &mut syn::TypePath) {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use syn::parse_quote;
+
+ #[test]
+ #[rustfmt::skip]
+ fn unit_enum_scalar_ser() {
+ let input = parse_quote! {
+ #[xml(scalar)]
+ pub enum TestEnum {
+ Foo,
+ Bar,
+ Baz = 1,
+ }
+ };
+
+ assert_eq!(super::ser::to_xml(&input).to_string(),
+"impl ToXml for TestEnum { fn serialize < W : :: core :: fmt :: Write + ? :: core :: marker :: Sized > (& self , serializer : & mut instant_xml :: Serializer < W > ,) -> Result < () , instant_xml :: Error > { serializer . write_str (match self { TestEnum :: Foo => \"Foo\" , TestEnum :: Bar => \"Bar\" , TestEnum :: Baz => \"1\" , }) } }"
+ )
+ }
+
+ #[test]
+ #[rustfmt::skip]
+ fn unit_enum_scalar_de() {
+ let input = parse_quote! {
+ #[xml(scalar)]
+ pub enum TestEnum {
+ Foo,
+ Bar,
+ Baz = 1,
+ }
+ };
+
+ assert_eq!(super::de::from_xml(&input).to_string(),
+"impl FromXml < 'xml > for TestEnum { fn deserialize < 'cx > (deserializer : & 'cx mut :: instant_xml :: Deserializer < 'cx , 'xml >) -> Result < Self , :: instant_xml :: Error > { match deserializer . take_str () { Ok (\"Foo\") => TestEnum :: Foo , Ok (\"Bar\") => TestEnum :: Bar , Ok (\"1\") => TestEnum :: Baz , _ => Err (:: instant_xml :: Error :: UnexpectedValue) } } }"
+ )
+ }
+
+ #[test]
+ #[rustfmt::skip]
+ fn enum_variant_rename_and_discriminant_conflict() {
+ super::ser::to_xml(&parse_quote! {
+ #[xml(scalar)]
+ pub enum TestEnum {
+ Foo,
+ Bar,
+ #[xml(rename = 2)]
+ Baz = 1,
+ }
+ }).to_string().find("compile_error ! { \"conflicting `rename` attribute and variant discriminant!\" }").unwrap();
+ }
+
+ #[test]
+ #[rustfmt::skip]
+ fn non_unit_enum_variant_unsupported() {
+ super::ser::to_xml(&parse_quote! {
+ #[xml(scalar)]
+ pub enum TestEnum {
+ Foo(String),
+ Bar,
+ Baz
+ }
+ }).to_string().find("compile_error ! { \"only unit enum variants are permitted!\" }").unwrap();
+ }
+
+ #[test]
+ #[rustfmt::skip]
+ fn non_scalar_enums_unsupported() {
+ super::ser::to_xml(&parse_quote! {
+ #[xml()]
+ pub enum TestEnum {
+ Foo,
+ Bar,
+ Baz
+ }
+ }).to_string().find("compile_error ! { \"non-scalar enums are currently unsupported!\" }").unwrap();
+ }
+
+ #[test]
+ #[rustfmt::skip]
+ fn scalar_variant_attribute_not_permitted() {
+ super::ser::to_xml(&parse_quote! {
+ #[xml(scalar)]
+ pub enum TestEnum {
+ Foo,
+ Bar,
+ #[xml(scalar)]
+ Baz
+ }
+ }).to_string().find("compile_error ! { \"attribute 'scalar' is invalid for enum variants\" }").unwrap();
+ }
+
+ #[test]
+ #[rustfmt::skip]
+ fn scalar_discrimintant_must_be_literal() {
+ assert_eq!(None, super::ser::to_xml(&parse_quote! {
+ #[xml(scalar)]
+ pub enum TestEnum {
+ Foo = 1,
+ Bar,
+ Baz
+ }
+ }).to_string().find("compile_error ! { \"invalid field discriminant value!\" }"));
+
+ super::ser::to_xml(&parse_quote! {
+ #[xml(scalar)]
+ pub enum TestEnum {
+ Foo = 1+1,
+ Bar,
+ Baz
+ }
+ }).to_string().find("compile_error ! { \"invalid field discriminant value!\" }").unwrap();
+ }
+}
diff --git a/instant-xml-macros/src/ser.rs b/instant-xml-macros/src/ser.rs
index c20c29a..bf3a46e 100644
--- a/instant-xml-macros/src/ser.rs
+++ b/instant-xml-macros/src/ser.rs
@@ -4,16 +4,57 @@ use syn::spanned::Spanned;
use crate::Namespace;
-use super::{discard_lifetimes, ContainerMeta, FieldMeta};
+use super::{discard_lifetimes, ContainerMeta, FieldMeta, VariantMeta};
pub fn to_xml(input: &syn::DeriveInput) -> proc_macro2::TokenStream {
let meta = ContainerMeta::from_derive(input);
match &input.data {
+ syn::Data::Struct(_) if meta.scalar => {
+ syn::Error::new(input.span(), "scalar structs are unsupported!").to_compile_error()
+ }
syn::Data::Struct(ref data) => serialize_struct(input, data, meta),
+ syn::Data::Enum(_) if !meta.scalar => {
+ syn::Error::new(input.span(), "non-scalar enums are currently unsupported!")
+ .to_compile_error()
+ }
+ syn::Data::Enum(ref data) => serialize_enum(input, data),
_ => todo!(),
}
}
+#[rustfmt::skip]
+fn serialize_enum(
+ input: &syn::DeriveInput,
+ data: &syn::DataEnum,
+) -> TokenStream {
+ let ident = &input.ident;
+ let mut variants = TokenStream::new();
+
+ for variant in data.variants.iter() {
+ let v_ident = &variant.ident;
+ let meta = match VariantMeta::from_variant(variant) {
+ Ok(meta) => meta,
+ Err(err) => return err.to_compile_error()
+ };
+
+ let serialize_as = meta.serialize_as;
+ variants.extend(quote!(#ident::#v_ident => #serialize_as,));
+ }
+
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ quote!(
+ impl #impl_generics ToXml for #ident #ty_generics #where_clause {
+ fn serialize(
+ &self,
+ serializer: &mut instant_xml::Serializer,
+ ) -> Result<(), instant_xml::Error> {
+ serializer.write_str(match self { #variants })
+ }
+ }
+ )
+}
+
fn serialize_struct(
input: &syn::DeriveInput,
data: &syn::DataStruct,