diff --git a/Cargo.lock b/Cargo.lock index ab726e3..4ccae07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,6 +7,7 @@ name = "enum_stringify" version = "0.1.0" dependencies = [ "quote", + "serde", "syn", ] @@ -28,6 +29,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "2.0.32" diff --git a/Cargo.toml b/Cargo.toml index 4745e98..6348df2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" homepage = "https://github.com/Yag000/enum_stringify" repository = "https://github.com/Yag000/enum_stringify" documentation = "https://docs.rs/enum_stringify" +keywords = ["enum", "string", "derive", "macro"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -19,3 +20,7 @@ proc-macro = true [dependencies] quote = "1.0.33" syn = "2.0.32" + +[dev-dependencies] +serde = { version = "1.0.130", features = ["derive"] } + diff --git a/README.md b/README.md index 8eb89d4..816078d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,26 @@ fn main() { } ``` +### Custom string representation + +You can customize the string representation of the enum by adding rpefixes or/and suffixes to the +variants. + +```rust +use enum_stringify::EnumStringify; + +#[derive(EnumStringify)] +#[enum_stringify(prefix = "MyPrefix", suffix = "MySuffix")] +enum MyEnum { + Variant1, + Variant2, + Variant3, +} +``` + +In this case the string representation of `MyEnum::Variant1` will be `MyPrefixVariant1MySuffix`(and +so on for the other variants). + ## Documentation and installation See [docs.rs](https://docs.rs/enum-stringify) for documentation. diff --git a/src/attributes.rs b/src/attributes.rs new file mode 100644 index 0000000..b53e04d --- /dev/null +++ b/src/attributes.rs @@ -0,0 +1,100 @@ +use syn::{DeriveInput, Meta}; + +pub(crate) enum Case { + Camel, + Snake, + None, +} + +pub(crate) struct Attributes { + pub(crate) case: Option, + pub(crate) prefix: Option, + pub(crate) suffix: Option, +} + +impl Attributes { + pub(crate) fn new(ast: &DeriveInput) -> Self { + let mut new = Self { + case: None, + prefix: None, + suffix: None, + }; + + ast.attrs.iter().for_each(|attr| match &attr.meta { + Meta::Path(_) => { + panic!("Unexpected argument"); + } + Meta::List(list) => { + let path = list + .path + .segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>(); + + if path == vec!["enum_stringify"] { + let mut tokens = list.tokens.clone().into_iter(); + + while let Some(attribute_type) = tokens.next() { + let attribute_type = attribute_type.to_string(); + + if tokens.next().expect("type must be specified").to_string() != "=" { + panic!("too many arguments"); + } + let value = tokens.next().expect("value must be specified").to_string(); + + match attribute_type.as_str() { + "case" => { + let case = match value.as_str() { + "camel" => Case::Camel, + "snake" => Case::Snake, + _ => Case::None, + }; + new.case = Some(case); + } + "prefix" => { + new.prefix = Some(value); + } + "suffix" => { + new.suffix = Some(value); + } + _ => { + panic!("Attribute not supported"); + } + } + + if let Some(comma_separator) = tokens.next() { + if comma_separator.to_string() != "," { + panic!("Expected a commaseparated attribute list"); + } + } + } + } + } + Meta::NameValue(_) => { + panic!("Unexpected argument"); + } + }); + + new + } + + pub(crate) fn apply(&self, names: &Vec<&syn::Ident>) -> Vec { + let mut new_names = Vec::new(); + for name in names { + let mut new_name = String::new(); + if let Some(prefix) = &self.prefix { + new_name.push_str(prefix); + } + + // Add here case logic + new_name.push_str(&name.to_string()); + + if let Some(suffix) = &self.suffix { + new_name.push_str(suffix); + } + new_names.push(syn::Ident::new(&new_name, name.span())); + } + new_names + } +} diff --git a/src/lib.rs b/src/lib.rs index 1257214..ba89739 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,12 @@ //! Derive [`std::fmt::Display`], [`std::str::FromStr`], [`TryFrom<&str>`] and //! [`TryFrom`] with a simple derive macro: [`EnumStringify`]. +use attributes::Attributes; use proc_macro::TokenStream; use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +mod attributes; /// Derive [`std::fmt::Display`], [`std::str::FromStr`], [`TryFrom<&str>`] and /// [`TryFrom`] for an enum. @@ -33,6 +37,29 @@ use quote::quote; /// assert!(Numbers::try_from("Three").is_err()); /// ``` /// +/// # Prefix and suffix +/// +/// You can add a prefix and/or a suffix to the string representation of the +/// enum variants. +/// +/// ``` +/// use enum_stringify::EnumStringify; +/// use std::str::FromStr; +/// +/// #[derive(EnumStringify, Debug, PartialEq)] +/// #[enum_stringify(prefix = MyPrefix, suffix = MySuffix)] +/// enum Numbers { +/// One, +/// Two, +/// } +/// +/// assert_eq!(Numbers::One.to_string(), "MyPrefixOneMySuffix"); +/// assert_eq!(Numbers::Two.to_string(), "MyPrefixTwoMySuffix"); +/// +/// assert_eq!(Numbers::try_from("MyPrefixOneMySuffix").unwrap(), Numbers::One); +/// assert_eq!(Numbers::try_from("MyPrefixTwoMySuffix").unwrap(), Numbers::Two); +/// ``` +/// /// # Details /// /// The implementations of the above traits corresponds to this: @@ -80,23 +107,26 @@ use quote::quote; /// } /// } /// ``` -#[proc_macro_derive(EnumStringify)] +#[proc_macro_derive(EnumStringify, attributes(enum_stringify))] pub fn enum_stringify(input: TokenStream) -> TokenStream { - let ast = syn::parse(input).unwrap(); + let ast = parse_macro_input!(input as DeriveInput); + impl_enum_to_string(&ast) } fn impl_enum_to_string(ast: &syn::DeriveInput) -> TokenStream { + let attributes = Attributes::new(ast); let name = &ast.ident; let variants = match ast.data { syn::Data::Enum(ref e) => &e.variants, _ => panic!("EnumToString only works with Enums"), }; - let names = variants.iter().map(|v| &v.ident).collect::>(); + let identifiers = variants.iter().map(|v| &v.ident).collect::>(); + let names = attributes.apply(&identifiers); - let mut gen = impl_display(name, &names); - gen.extend(impl_from_str(name, &names)); + let mut gen = impl_display(name, &identifiers, &names); + gen.extend(impl_from_str(name, &identifiers, &names)); gen.extend(impl_from_string(name)); gen.extend(impl_from_str_trait(name)); @@ -104,12 +134,16 @@ fn impl_enum_to_string(ast: &syn::DeriveInput) -> TokenStream { } /// Implementation of [`std::fmt::Display`]. -fn impl_display(name: &syn::Ident, names: &Vec<&syn::Ident>) -> TokenStream { +fn impl_display( + name: &syn::Ident, + identifiers: &Vec<&syn::Ident>, + names: &Vec, +) -> TokenStream { let gen = quote! { impl std::fmt::Display for #name { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - #(Self::#names => write!(f, stringify!(#names))),* + #(Self::#identifiers=> write!(f, stringify!(#names))),* } } } @@ -119,14 +153,18 @@ fn impl_display(name: &syn::Ident, names: &Vec<&syn::Ident>) -> TokenStream { } /// Implementation of [`TryFrom<&str>`]. -fn impl_from_str(name: &syn::Ident, names: &Vec<&syn::Ident>) -> TokenStream { +fn impl_from_str( + name: &syn::Ident, + identifiers: &Vec<&syn::Ident>, + names: &Vec, +) -> TokenStream { let gen = quote! { impl TryFrom<&str> for #name { type Error = (); fn try_from(s: &str) -> Result { match s { - #(stringify!(#names) => Ok(Self::#names),)* + #(stringify!(#names) => Ok(Self::#identifiers),)* _ => Err(()), } } diff --git a/tests/attributes.rs b/tests/attributes.rs new file mode 100644 index 0000000..4535ec2 --- /dev/null +++ b/tests/attributes.rs @@ -0,0 +1,90 @@ +use std::str::FromStr; + +#[derive(Debug, PartialEq, enum_stringify::EnumStringify)] +#[enum_stringify(suffix = Suff)] +enum Number1 { + Zero, + One, + Two, +} + +#[test] +fn test_suffix_to_string() { + assert_eq!(Number1::Zero.to_string(), "ZeroSuff"); + assert_eq!(Number1::One.to_string(), "OneSuff"); + assert_eq!(Number1::Two.to_string(), "TwoSuff"); +} +#[test] +fn test_suffix_from_str() { + assert_eq!(Number1::from_str("ZeroSuff"), Ok(Number1::Zero)); + assert_eq!(Number1::from_str("OneSuff"), Ok(Number1::One)); + assert_eq!(Number1::from_str("TwoSuff"), Ok(Number1::Two)); +} + +#[derive(Debug, PartialEq, enum_stringify::EnumStringify)] +#[enum_stringify(prefix = Pref)] +enum Number2 { + Zero, + One, + Two, +} + +#[test] +fn test_prefix_to_string() { + assert_eq!(Number2::Zero.to_string(), "PrefZero"); + assert_eq!(Number2::One.to_string(), "PrefOne"); + assert_eq!(Number2::Two.to_string(), "PrefTwo"); +} + +#[test] +fn test_prefix_from_str() { + assert_eq!(Number2::from_str("PrefZero"), Ok(Number2::Zero)); + assert_eq!(Number2::from_str("PrefOne"), Ok(Number2::One)); + assert_eq!(Number2::from_str("PrefTwo"), Ok(Number2::Two)); +} + +#[derive(Debug, PartialEq, enum_stringify::EnumStringify)] +#[enum_stringify(prefix = Pref, suffix = Suff)] +enum Number3 { + Zero, + One, + Two, +} + +#[test] +fn test_prefix_suffix_to_string() { + assert_eq!(Number3::Zero.to_string(), "PrefZeroSuff"); + assert_eq!(Number3::One.to_string(), "PrefOneSuff"); + assert_eq!(Number3::Two.to_string(), "PrefTwoSuff"); +} + +#[test] +fn test_prefix_suffix_from_str() { + assert_eq!(Number3::from_str("PrefZeroSuff"), Ok(Number3::Zero)); + assert_eq!(Number3::from_str("PrefOneSuff"), Ok(Number3::One)); + assert_eq!(Number3::from_str("PrefTwoSuff"), Ok(Number3::Two)); +} + +// Testing commutativity of prefix and suffix + +#[derive(Debug, PartialEq, enum_stringify::EnumStringify)] +#[enum_stringify(suffix = Suff, prefix = Pref)] +enum Number4 { + Zero, + One, + Two, +} + +#[test] +fn test_suffix_prefix_to_string() { + assert_eq!(Number4::Zero.to_string(), "PrefZeroSuff"); + assert_eq!(Number4::One.to_string(), "PrefOneSuff"); + assert_eq!(Number4::Two.to_string(), "PrefTwoSuff"); +} + +#[test] +fn test_suffix_prefix_from_str() { + assert_eq!(Number4::from_str("PrefZeroSuff"), Ok(Number4::Zero)); + assert_eq!(Number4::from_str("PrefOneSuff"), Ok(Number4::One)); + assert_eq!(Number4::from_str("PrefTwoSuff"), Ok(Number4::Two)); +} diff --git a/tests/compatibility.rs b/tests/compatibility.rs new file mode 100644 index 0000000..195ce7d --- /dev/null +++ b/tests/compatibility.rs @@ -0,0 +1,51 @@ +use std::str::FromStr; + +// Testing compatibility with other attribute macros + +#[derive(Debug, PartialEq, Eq, enum_stringify::EnumStringify, serde::Serialize)] +#[serde(rename_all = "snake_case")] +#[enum_stringify(prefix = MyPrefix, suffix = MySuffix)] +enum MyEnum { + A, + B, + C, +} + +#[test] +fn test_prefix_suffix_to_string() { + assert_eq!(MyEnum::A.to_string(), "MyPrefixAMySuffix"); + assert_eq!(MyEnum::B.to_string(), "MyPrefixBMySuffix"); + assert_eq!(MyEnum::C.to_string(), "MyPrefixCMySuffix"); +} + +#[test] +fn test_prefix_suffix_from_str() { + assert_eq!(MyEnum::from_str("MyPrefixAMySuffix"), Ok(MyEnum::A)); + assert_eq!(MyEnum::from_str("MyPrefixBMySuffix"), Ok(MyEnum::B)); + assert_eq!(MyEnum::from_str("MyPrefixCMySuffix"), Ok(MyEnum::C)); +} + +// Testing commutativity with other attribute macros + +#[derive(Debug, PartialEq, Eq, enum_stringify::EnumStringify, serde::Serialize)] +#[enum_stringify(suffix = MySuffix, prefix = MyPrefix)] +#[serde(rename_all = "snake_case")] +enum MyEnum2 { + A, + B, + C, +} + +#[test] +fn test_suffix_prefix_to_string() { + assert_eq!(MyEnum2::A.to_string(), "MyPrefixAMySuffix"); + assert_eq!(MyEnum2::B.to_string(), "MyPrefixBMySuffix"); + assert_eq!(MyEnum2::C.to_string(), "MyPrefixCMySuffix"); +} + +#[test] +fn test_suffix_prefix_from_str() { + assert_eq!(MyEnum2::from_str("MyPrefixAMySuffix"), Ok(MyEnum2::A)); + assert_eq!(MyEnum2::from_str("MyPrefixBMySuffix"), Ok(MyEnum2::B)); + assert_eq!(MyEnum2::from_str("MyPrefixCMySuffix"), Ok(MyEnum2::C)); +}