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,