Skip to content

Commit

Permalink
Prefix suffix (#1)
Browse files Browse the repository at this point in the history
Add support for custom string representation.
  • Loading branch information
Yag000 authored Oct 4, 2023
2 parents bb20cac + 3e9376d commit dab92f3
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 9 deletions.
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"] }

20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
100 changes: 100 additions & 0 deletions src/attributes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use syn::{DeriveInput, Meta};

pub(crate) enum Case {
Camel,
Snake,
None,
}

pub(crate) struct Attributes {
pub(crate) case: Option<Case>,
pub(crate) prefix: Option<String>,
pub(crate) suffix: Option<String>,
}

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::<Vec<_>>();

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<syn::Ident> {
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
}
}
56 changes: 47 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
//! Derive [`std::fmt::Display`], [`std::str::FromStr`], [`TryFrom<&str>`] and
//! [`TryFrom<String>`] 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<String>`] for an enum.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -80,36 +107,43 @@ 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::<Vec<_>>();
let identifiers = variants.iter().map(|v| &v.ident).collect::<Vec<_>>();
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));

gen
}

/// 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<syn::Ident>,
) -> 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))),*
}
}
}
Expand All @@ -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<syn::Ident>,
) -> TokenStream {
let gen = quote! {
impl TryFrom<&str> for #name {
type Error = ();

fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
#(stringify!(#names) => Ok(Self::#names),)*
#(stringify!(#names) => Ok(Self::#identifiers),)*
_ => Err(()),
}
}
Expand Down
90 changes: 90 additions & 0 deletions tests/attributes.rs
Original file line number Diff line number Diff line change
@@ -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));
}
Loading

0 comments on commit dab92f3

Please sign in to comment.