diff --git a/instant-xml-macros/src/case.rs b/instant-xml-macros/src/case.rs index 4a9af55..320c527 100644 --- a/instant-xml-macros/src/case.rs +++ b/instant-xml-macros/src/case.rs @@ -11,7 +11,7 @@ use std::fmt::{self, Debug, Display}; use self::RenameRule::*; /// The different possible ways to change case of fields in a struct, or variants in an enum. -#[derive(Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum RenameRule { /// Don't apply a default rename rule. None, @@ -36,6 +36,12 @@ pub enum RenameRule { ScreamingKebabCase, } +impl Default for RenameRule { + fn default() -> Self { + None + } +} + static RENAME_RULES: &[(&str, RenameRule)] = &[ ("lowercase", LowerCase), ("UPPERCASE", UpperCase), diff --git a/instant-xml-macros/src/de.rs b/instant-xml-macros/src/de.rs index 1f875da..4ab418d 100644 --- a/instant-xml-macros/src/de.rs +++ b/instant-xml-macros/src/de.rs @@ -17,19 +17,19 @@ pub(crate) fn from_xml(input: &syn::DeriveInput) -> TokenStream { syn::Error::new(input.span(), "non-scalar enums are currently unsupported!") .to_compile_error() } - syn::Data::Enum(ref data) => deserialize_enum(input, data), + syn::Data::Enum(ref data) => deserialize_enum(input, data, meta), _ => todo!(), } } #[rustfmt::skip] -fn deserialize_enum(input: &syn::DeriveInput, data: &syn::DataEnum) -> TokenStream { +fn deserialize_enum(input: &syn::DeriveInput, data: &syn::DataEnum, meta: ContainerMeta) -> 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) { + let meta = match VariantMeta::from_variant(variant, &meta) { Ok(meta) => meta, Err(err) => return err.to_compile_error() }; @@ -90,7 +90,14 @@ fn deserialize_struct( match data.fields { syn::Fields::Named(ref fields) => { fields.named.iter().enumerate().for_each(|(index, field)| { - let field_meta = FieldMeta::from_field(field); + let field_meta = match FieldMeta::from_field(field) { + Ok(meta) => meta, + Err(err) => { + return_val.extend(err.into_compile_error()); + return; + } + }; + let tokens = match field_meta.attribute { true => &mut attributes_tokens, false => &mut elements_tokens, @@ -212,9 +219,13 @@ fn process_field( container_meta: &ContainerMeta, ) { let field_name = field.ident.as_ref().unwrap(); - let field_tag = match field_meta.rename { - Some(name) => quote!(#name), - None => field_name.to_string().into_token_stream(), + + let field_tag = match &field_meta.rename { + Some(rename) => quote!(#rename), + None => container_meta + .rename_all + .apply_to_field(&field_name.to_string()) + .into_token_stream(), }; let const_field_var_str = Ident::new(&field_name.to_string().to_uppercase(), Span::call_site()); diff --git a/instant-xml-macros/src/lib.rs b/instant-xml-macros/src/lib.rs index c40c2ca..646e871 100644 --- a/instant-xml-macros/src/lib.rs +++ b/instant-xml-macros/src/lib.rs @@ -1,5 +1,6 @@ extern crate proc_macro; +mod case; mod de; mod ser; @@ -13,6 +14,8 @@ use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::token::Colon2; +use case::RenameRule; + #[proc_macro_derive(ToXml, attributes(xml))] pub fn to_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast = parse_macro_input!(input as syn::DeriveInput); @@ -29,6 +32,7 @@ pub fn from_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream { struct ContainerMeta { ns: NamespaceMeta, rename: Option, + rename_all: RenameRule, scalar: bool, } @@ -40,6 +44,12 @@ 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::RenameAll(lit) => { + meta.rename_all = match RenameRule::from_str(&lit.to_string()) { + Ok(rule) => rule, + Err(err) => panic!("{err}"), + }; + } MetaItem::Scalar => meta.scalar = true, } } @@ -55,17 +65,29 @@ struct FieldMeta { } impl FieldMeta { - fn from_field(input: &syn::Field) -> FieldMeta { + fn from_field(input: &syn::Field) -> Result { let mut meta = FieldMeta::default(); for item in meta_items(&input.attrs) { match item { 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"), + MetaItem::RenameAll(_) => { + return Err(syn::Error::new( + input.span(), + "attribute 'rename_all' invalid in field xml attribute", + )) + } + MetaItem::Scalar => { + return Err(syn::Error::new( + input.span(), + "attribute 'scalar' is invalid for struct fields", + )) + } } } - meta + + Ok(meta) } } @@ -75,7 +97,10 @@ struct VariantMeta { } impl VariantMeta { - fn from_variant(input: &syn::Variant) -> Result { + fn from_variant( + input: &syn::Variant, + container: &ContainerMeta, + ) -> Result { if !input.fields.is_empty() { return Err(syn::Error::new( input.fields.span(), @@ -86,6 +111,8 @@ impl VariantMeta { let mut rename = None; for item in meta_items(&input.attrs) { match item { + MetaItem::Rename(lit) => rename = Some(lit.to_token_stream()), + MetaItem::Attribute => { return Err(syn::Error::new( input.span(), @@ -98,7 +125,12 @@ impl VariantMeta { "attribute 'ns' is invalid for enum variants", )) } - MetaItem::Rename(lit) => rename = Some(lit.to_token_stream()), + MetaItem::RenameAll(_) => { + return Err(syn::Error::new( + input.span(), + "attribute 'rename_all' invalid in field xml attribute", + )) + } MetaItem::Scalar => { return Err(syn::Error::new( input.span(), @@ -141,7 +173,10 @@ impl VariantMeta { let serialize_as = match rename.or(discriminant) { Some(lit) => lit.into_token_stream(), - None => input.ident.to_string().to_token_stream(), + None => container + .rename_all + .apply_to_variant(&input.ident.to_string()) + .to_token_stream(), }; Ok(VariantMeta { serialize_as }) @@ -435,6 +470,8 @@ fn meta_items(attrs: &[syn::Attribute]) -> Vec { MetaState::Ns } else if id == "rename" { MetaState::Rename + } else if id == "rename_all" { + MetaState::RenameAll } else if id == "scalar" { items.push(MetaItem::Scalar); MetaState::Comma @@ -454,10 +491,17 @@ fn meta_items(attrs: &[syn::Attribute]) -> Vec { (MetaState::Rename, TokenTree::Punct(punct)) if punct.as_char() == '=' => { MetaState::RenameValue } + (MetaState::RenameAll, TokenTree::Punct(punct)) if punct.as_char() == '=' => { + MetaState::RenameAllValue + } (MetaState::RenameValue, TokenTree::Literal(lit)) => { items.push(MetaItem::Rename(lit)); MetaState::Comma } + (MetaState::RenameAllValue, TokenTree::Ident(ident)) => { + items.push(MetaItem::RenameAll(ident)); + MetaState::Comma + } (state, tree) => { panic!( "invalid state transition while parsing xml attribute ({}, {tree})", @@ -477,6 +521,8 @@ enum MetaState { Ns, Rename, RenameValue, + RenameAll, + RenameAllValue, } impl MetaState { @@ -487,6 +533,8 @@ impl MetaState { MetaState::Ns => "Ns", MetaState::Rename => "Rename", MetaState::RenameValue => "RenameValue", + MetaState::RenameAll => "RenameAll", + MetaState::RenameAllValue => "RenameAllValue", } } } @@ -547,6 +595,7 @@ enum MetaItem { Ns(NamespaceMeta), Rename(Literal), Scalar, + RenameAll(Ident), } enum Namespace { @@ -650,20 +699,6 @@ mod tests { ) } - #[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() { @@ -725,4 +760,64 @@ mod tests { } }).to_string().find("compile_error ! { \"invalid field discriminant value!\" }").unwrap(); } + + #[test] + #[rustfmt::skip] + fn struct_rename_all_permitted() { + assert_eq!(super::ser::to_xml(&parse_quote! { + #[xml(rename_all = UPPERCASE)] + pub struct TestStruct { + field_1: String, + field_2: u8, + } + }).to_string(), "impl ToXml for TestStruct { fn serialize < W : :: core :: fmt :: Write + ? :: core :: marker :: Sized > (& self , serializer : & mut instant_xml :: Serializer < W > ,) -> Result < () , instant_xml :: Error > { let prefix = serializer . write_start (\"TestStruct\" , \"\" , false) ? ; debug_assert_eq ! (prefix , None) ; let mut new = :: instant_xml :: ser :: Context :: < 0usize > :: default () ; new . default_ns = \"\" ; let old = serializer . push (new) ? ; serializer . end_start () ? ; match < String as ToXml > :: KIND { :: instant_xml :: Kind :: Element (_) => { self . field_1 . serialize (serializer) ? ; } :: instant_xml :: Kind :: Scalar => { let prefix = serializer . write_start (\"FIELD_1\" , \"\" , true) ? ; serializer . end_start () ? ; self . field_1 . serialize (serializer) ? ; serializer . write_close (prefix , \"FIELD_1\") ? ; } } match < u8 as ToXml > :: KIND { :: instant_xml :: Kind :: Element (_) => { self . field_2 . serialize (serializer) ? ; } :: instant_xml :: Kind :: Scalar => { let prefix = serializer . write_start (\"FIELD_2\" , \"\" , true) ? ; serializer . end_start () ? ; self . field_2 . serialize (serializer) ? ; serializer . write_close (prefix , \"FIELD_2\") ? ; } } serializer . write_close (prefix , \"TestStruct\") ? ; serializer . pop (old) ; Ok (()) } const KIND : :: instant_xml :: Kind = :: instant_xml :: Kind :: Element (:: instant_xml :: Id { ns : \"\" , name : \"TestStruct\" , }) ; } ;"); + } + + #[test] + #[rustfmt::skip] + fn scalar_enum_rename_all_permitted() { + assert_eq!(super::ser::to_xml(&parse_quote! { + #[xml(scalar, rename_all = UPPERCASE)] + pub enum TestEnum { + Foo = 1, + Bar, + Baz + } + }).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 => \"1\" , TestEnum :: Bar => \"BAR\" , TestEnum :: Baz => \"BAZ\" , }) } }"); + } + + #[test] + #[rustfmt::skip] + fn rename_all_attribute_not_permitted() { + super::ser::to_xml(&parse_quote! { + pub struct TestStruct { + #[xml(rename_all = UPPERCASE)] + field_1: String, + field_2: u8, + } + }).to_string().find("compile_error ! { \"attribute 'rename_all' invalid in field xml attribute\" }").unwrap(); + + super::ser::to_xml(&parse_quote! { + #[xml(scalar)] + pub enum TestEnum { + Foo = 1, + Bar, + #[xml(rename_all = UPPERCASE)] + Baz + } + }).to_string().find("compile_error ! { \"attribute 'rename_all' invalid in field xml attribute\" }").unwrap(); + } + + #[test] + #[rustfmt::skip] + #[should_panic(expected = "unknown rename rule `rename_all = \"forgetaboutit\"`, expected one of \"lowercase\", \"UPPERCASE\", \"PascalCase\", \"camelCase\", \"snake_case\", \"SCREAMING_SNAKE_CASE\", \"kebab-case\", \"SCREAMING-KEBAB-CASE\"")] + fn bogus_rename_all_not_permitted() { + super::ser::to_xml(&parse_quote! { + #[xml(rename_all = forgetaboutit)] + pub struct TestStruct { + field_1: String, + field_2: u8, + } + }).to_string().find("compile_error ! { \"attribute 'rename_all' invalid in field xml attribute\" }").unwrap(); + } } diff --git a/instant-xml-macros/src/ser.rs b/instant-xml-macros/src/ser.rs index bf3a46e..2b9d03c 100644 --- a/instant-xml-macros/src/ser.rs +++ b/instant-xml-macros/src/ser.rs @@ -17,7 +17,7 @@ pub fn to_xml(input: &syn::DeriveInput) -> proc_macro2::TokenStream { syn::Error::new(input.span(), "non-scalar enums are currently unsupported!") .to_compile_error() } - syn::Data::Enum(ref data) => serialize_enum(input, data), + syn::Data::Enum(ref data) => serialize_enum(input, data, meta), _ => todo!(), } } @@ -26,13 +26,14 @@ pub fn to_xml(input: &syn::DeriveInput) -> proc_macro2::TokenStream { fn serialize_enum( input: &syn::DeriveInput, data: &syn::DataEnum, + meta: ContainerMeta ) -> 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) { + let meta = match VariantMeta::from_variant(variant, &meta) { Ok(meta) => meta, Err(err) => return err.to_compile_error() }; @@ -138,10 +139,20 @@ fn process_named_field( meta: &ContainerMeta, ) { let field_name = field.ident.as_ref().unwrap(); - let field_meta = FieldMeta::from_field(field); + let field_meta = match FieldMeta::from_field(field) { + Ok(meta) => meta, + Err(err) => { + body.extend(err.into_compile_error()); + return; + } + }; + let tag = match &field_meta.rename { Some(rename) => quote!(#rename), - None => field_name.to_string().into_token_stream(), + None => meta + .rename_all + .apply_to_field(&field_name.to_string()) + .into_token_stream(), }; let default_ns = &meta.ns.uri;