diff --git a/src/attributes.rs b/src/attributes.rs index 245b490..b4e4c6d 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -1,91 +1,103 @@ +use std::borrow::Cow; use std::collections::HashMap; use crate::case::Case; use proc_macro2::{Ident, TokenStream}; use syn::{DeriveInput, Meta}; +/// The attribute name used for enum variant renaming. static ATTRIBUTE_NAME: &str = "enum_stringify"; -/// Parses a string literal by removing surrounding quotes if present. -fn parse_string(s: &str) -> Result { - if s.starts_with('"') && s.ends_with('"') { - Ok(s[1..s.len() - 1].to_string()) - } else { - Err(()) +/// Parses a string literal by removing surrounding double quotes if present. +/// +/// # Arguments +/// * `s` - A string slice containing the quoted string. +/// +/// # Returns +/// * `Ok(String)` if the string is correctly formatted. +/// * `Err(&'static str)` if the string is not enclosed in double quotes. +fn parse_string(s: &str) -> Result { + s.strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + .map(std::string::ToString::to_string) + .ok_or("String must be enclosed in double quotes") +} + +/// Parses a list of attribute tokens into a vector of type `T`. +/// +/// # Arguments +/// * `tokens` - A reference to a token stream containing attributes. +/// +/// # Returns +/// * `Ok(Vec)` if parsing succeeds. +/// * `Err(String)` if parsing fails due to incorrect syntax. +fn parse_token_list(tokens: &TokenStream) -> Result, String> +where + T: TryFrom<(String, String)>, +{ + let mut result = Vec::new(); + let mut tokens = tokens.clone().into_iter(); + + while let Some(attribute_type) = tokens.next() { + let attribute_type = attribute_type.to_string(); + + let Some(eq_token) = tokens.next() else { + return Err(format!("Expected '=' after '{attribute_type}'")); + }; + if eq_token.to_string() != "=" { + return Err(format!("Unexpected token '{eq_token}', expected '='")); + } + + let value = tokens.next().ok_or("Value must be specified")?.to_string(); + + match T::try_from((attribute_type.clone(), value)) { + Ok(value) => result.push(value), + Err(_) => return Err(format!("Invalid argument: {attribute_type}")), + } + + if let Some(comma_separator) = tokens.next() { + if comma_separator.to_string() != "," { + return Err("Expected a comma-separated attribute list".to_string()); + } + } } + Ok(result) } +/// Represents a rename attribute for an enum variant. #[derive(Clone)] -/// Represents a rename attribute applied to an enum variant. struct VariantRename(String); impl TryFrom<(String, String)> for VariantRename { - type Error = (); + type Error = &'static str; fn try_from(value: (String, String)) -> Result { if value.0 == "rename" { - Ok(Self(parse_string(value.1.as_str())?)) + Ok(Self(parse_string(&value.1)?)) } else { - Err(()) + Err("Not a rename string") } } } impl VariantRename { - /// Parses an attribute to determine if it is a rename directive. + /// Parses the rename attribute from a given `syn::Attribute`. fn parse_args(attribute: &syn::Attribute) -> Option { if !attribute.path().is_ident(ATTRIBUTE_NAME) { return None; } match &attribute.meta { - Meta::List(list) => { - let path = list - .path - .segments - .iter() - .map(|s| s.ident.to_string()) - .collect::>(); - - if path == vec![ATTRIBUTE_NAME] { - Some( - Attributes::parse_token_list::(&list.tokens) - .ok()? - .first()? - .clone(), - ) - } else { - None - } - } + Meta::List(list) => parse_token_list::(&list.tokens) + .ok()? + .first() + .cloned(), _ => None, } } } -// Represents different renaming attributes that can be applied to enum variants. -enum RenameAttribute { - Case(Case), - Prefix(String), - Suffix(String), -} - -impl TryFrom<(String, String)> for RenameAttribute { - type Error = (); - - fn try_from(value: (String, String)) -> Result { - if value.0 == "prefix" { - Ok(Self::Prefix(parse_string(value.1.as_str())?)) - } else if value.0 == "suffix" { - Ok(Self::Suffix(parse_string(value.1.as_str())?)) - } else if value.0 == "case" { - Ok(Self::Case(Case::try_from(value)?)) - } else { - Err(()) - } - } -} - +/// Represents attribute configurations for renaming enum variants. #[derive(Default)] pub(crate) struct Attributes { case: Option, @@ -94,107 +106,70 @@ pub(crate) struct Attributes { } impl Attributes { - /// Constructs an `Attributes` instance by parsing derive attributes from an AST. + /// Constructs an `Attributes` instance by parsing the attributes of a derive input. pub(crate) fn new(ast: &DeriveInput) -> Self { - let mut new = Self { - case: None, - prefix: None, - suffix: None, - }; - + let mut new = Self::default(); ast.attrs.iter().for_each(|attr| { - let rename_rules = Self::parse_args(attr); - if let Some(rename_rules) = rename_rules { + if let Some(rename_rules) = Self::parse_args(attr) { new.prefix = rename_rules.prefix; new.suffix = rename_rules.suffix; new.case = rename_rules.case; - }; + } }); - new } - /// Parses attributes related to casing, prefixes, and suffixes. fn parse_args(attribute: &syn::Attribute) -> Option { if !attribute.path().is_ident(ATTRIBUTE_NAME) { return None; } let mut new = Self::default(); - match &attribute.meta { Meta::List(list) => { - let path = list - .path - .segments - .iter() - .map(|s| s.ident.to_string()) - .collect::>(); - - if path == vec![ATTRIBUTE_NAME] { - let attributes = - Attributes::parse_token_list::(&list.tokens).ok()?; - for attr in attributes { - new.merge_attribute(attr); - } - Some(new) - } else { - None + let attributes = parse_token_list::<(String, String)>(&list.tokens).ok()?; + for value in attributes { + new.update_attribute(value); } + Some(new) } _ => None, } } - /// Merges parsed attribute into the struct. - fn merge_attribute(&mut self, attr: RenameAttribute) { - match attr { - RenameAttribute::Prefix(s) => self.prefix = Some(s), - RenameAttribute::Suffix(s) => self.suffix = Some(s), - RenameAttribute::Case(s) => self.case = Some(s), + fn update_attribute(&mut self, value: (String, String)) { + match value.0.as_str() { + "prefix" => self.prefix = parse_string(&value.1).ok(), + "suffix" => self.suffix = parse_string(&value.1).ok(), + "case" => self.case = Case::try_from(value).ok(), + _ => {} } } - /// Parses tokens into attributes. - fn parse_token_list(tokens: &TokenStream) -> Result, String> - where - T: TryFrom<(String, String)>, - { - let mut result = Vec::new(); - let mut tokens = tokens.clone().into_iter(); - - while let Some(attribute_type) = tokens.next() { - let attribute_type = attribute_type.to_string(); - - assert!( - tokens.next().expect("type must be specified").to_string() == "=", - "too many arguments" - ); - let value = tokens.next().expect("value must be specified").to_string(); - - match T::try_from((attribute_type.clone(), value)) { - Ok(value) => result.push(value), - Err(_) => return Err(format!("Invalid argument: {attribute_type}")), - } + /// Applies renaming rules (prefix, suffix, case) to a given string. + fn rename<'a>(&self, s: &'a str) -> Cow<'a, str> { + let mut new_name = Cow::Borrowed(s); - if let Some(comma_separator) = tokens.next() { - assert!( - comma_separator.to_string() == ",", - "Expected a comma separated attribute list" - ); - } + if let Some(prefix) = &self.prefix { + new_name = Cow::Owned(format!("{prefix}{new_name}")); + } + if let Some(suffix) = &self.suffix { + new_name = Cow::Owned(format!("{new_name}{suffix}")); } - Ok(result) + if let Some(case) = &self.case { + new_name = Cow::Owned(case.to_case(&new_name)); + } + new_name } } -/// Stores enum variants and their optional renaming attributes. +/// Stores renaming information for enum variants. pub(crate) struct Variants { variant_renames: HashMap>, } impl Variants { - /// Parses an AST to extract enum variants and their attributes. + /// Constructs a `Variants` instance by parsing the derive input. pub(crate) fn new(ast: &DeriveInput) -> Self { let mut new = Self { variant_renames: HashMap::new(), @@ -208,53 +183,31 @@ impl Variants { variants .iter() .for_each(|variant| new.parse_variant_attribute(variant)); - new } - /// Extracts renaming attributes from an enum variant. + /// Parses attributes for a given enum variant. fn parse_variant_attribute(&mut self, variant: &syn::Variant) { - let attribute_renames = variant.attrs.iter().filter_map(VariantRename::parse_args); - - let rename = attribute_renames.last(); - + let rename = variant + .attrs + .iter() + .filter_map(VariantRename::parse_args) + .reduce(|_, new| new); self.variant_renames.insert(variant.ident.clone(), rename); } - /// Applies attributes (prefix, suffix, case) to enum variant names. + /// Applies renaming rules to each enum variant name. pub(crate) fn apply(&self, attributes: &Attributes) -> Vec<(syn::Ident, String)> { - let mut new_names = Vec::new(); - - for (name, rename) in &self.variant_renames { - if let Some(rename) = rename { - new_names.push(rename.0.clone()); - continue; - } - let mut new_name = String::new(); - if let Some(prefix) = &attributes.prefix { - new_name.push_str(prefix); - } - - new_name.push_str(&name.to_string()); - - if let Some(suffix) = &attributes.suffix { - new_name.push_str(suffix); - } - - if let Some(case) = &attributes.case { - new_name = case.to_case(&new_name); - } - - new_names.push(new_name); - } - - let tmp = self - .variant_renames - .keys() - .cloned() - .zip(new_names) - .collect::>(); - - tmp + self.variant_renames + .iter() + .map(|(ident, rename)| { + let new_name = if let Some(rename) = rename { + rename.0.clone() + } else { + attributes.rename(ident.to_string().as_str()).into_owned() + }; + (ident.clone(), new_name) + }) + .collect() } } diff --git a/src/case.rs b/src/case.rs index b2ebd7f..89927e4 100644 --- a/src/case.rs +++ b/src/case.rs @@ -5,20 +5,20 @@ pub(crate) struct Case(convert_case::Case); // This is used to check if the first string is "case" and then attempt conversion of the second string. impl TryFrom<(String, String)> for Case { - type Error = (); + type Error = &'static str; fn try_from(value: (String, String)) -> Result { if value.0 == "case" { value.1.try_into() } else { - Err(()) + Err("The first string is not \"case\"") } } } // Maps specific string values to their corresponding `convert_case::Case` variant. impl TryFrom for Case { - type Error = (); + type Error = &'static str; fn try_from(value: String) -> Result { Ok(Self(match value.as_str() { @@ -39,7 +39,7 @@ impl TryFrom for Case { "\"flat\"" => convert_case::Case::Flat, "\"upper_flat\"" => convert_case::Case::UpperFlat, "\"alternating\"" => convert_case::Case::Alternating, - _ => Err(())?, + _ => Err("Invalid case")?, })) } } diff --git a/src/lib.rs b/src/lib.rs index ecec732..0ab9e03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,208 @@ //! # enum-stringify //! -//! Derive [`std::fmt::Display`], [`std::str::FromStr`], [`TryFrom<&str>`] and -//! [`TryFrom`] with a simple derive macro: [`EnumStringify`]. +//! A procedural macro that derives implementations for: +//! - [`std::fmt::Display`]: Converts enum variants to their string representations. +//! - [`std::str::FromStr`]: Parses a string into an enum variant. +//! - [`TryFrom<&str>`] and [`TryFrom`]: Alternative conversion methods. +//! +//! ## Example +//! +//! ``` +//! use enum_stringify::EnumStringify; +//! use std::str::FromStr; +//! +//! #[derive(EnumStringify, Debug, PartialEq)] +//! enum Numbers { +//! One, +//! Two, +//! } +//! +//! assert_eq!(Numbers::One.to_string(), "One"); +//! assert_eq!(Numbers::Two.to_string(), "Two"); +//! +//! +//! assert_eq!(Numbers::try_from("One").unwrap(), Numbers::One); +//! assert_eq!(Numbers::try_from("Two").unwrap(), Numbers::Two); +//! +//! assert!(Numbers::try_from("Three").is_err()); +//! ``` +//! +//! ## Custom Prefix and Suffix +//! +//! You can add a prefix and/or suffix to the string representation: +//! +//! ``` +//! use enum_stringify::EnumStringify; +//! +//! #[derive(EnumStringify, Debug, PartialEq)] +//! #[enum_stringify(prefix = "Pre", suffix = "Post")] +//! enum Numbers { +//! One, +//! Two, +//! } +//! +//! assert_eq!(Numbers::One.to_string(), "PreOnePost"); +//! assert_eq!(Numbers::try_from("PreOnePost").unwrap(), Numbers::One); +//! ``` +//! +//! ## Case Conversion +//! +//! Convert enum variant names to different cases using the [`convert_case`] crate. +//! +//! ``` +//! use enum_stringify::EnumStringify; +//! +//! #[derive(EnumStringify, Debug, PartialEq)] +//! #[enum_stringify(case = "flat")] +//! enum Numbers { +//! One, +//! Two, +//! } +//! +//! assert_eq!(Numbers::One.to_string(), "one"); +//! assert_eq!(Numbers::try_from("one").unwrap(), Numbers::One); +//! ``` +//! +//! ## Rename Variants +//! +//! Customize the string representation of specific variants: +//! +//! ``` +//! use enum_stringify::EnumStringify; +//! +//! #[derive(EnumStringify, Debug, PartialEq)] +//! enum Istari { +//! #[enum_stringify(rename = "Ólorin")] +//! Gandalf, +//! Saruman, +//! } +//! +//! assert_eq!(Istari::Gandalf.to_string(), "Ólorin"); +//! assert_eq!(Istari::try_from("Ólorin").unwrap(), Istari::Gandalf); +//! ``` +//! +//! This takes precedence over the other attributes : +//! +//! ``` +//! use enum_stringify::EnumStringify; +//! +//! #[derive(EnumStringify, Debug, PartialEq)] +//! #[enum_stringify(prefix = "Pre", suffix = "Post", case = "upper")] +//! enum Istari { +//! #[enum_stringify(rename = "Ólorin")] +//! Gandalf, +//! } +//! +//! assert_eq!(Istari::Gandalf.to_string(), "Ólorin"); +//! assert_eq!(Istari::try_from("Ólorin").unwrap(), Istari::Gandalf); +//! ``` +//! +//! ## Using All Options Together +//! +//! You can combine all options: renaming, prefix, suffix, and case conversion. +//! +//! ``` +//! use enum_stringify::EnumStringify; +//! +//! #[derive(EnumStringify, Debug, PartialEq)] +//! #[enum_stringify(prefix = "Pre", suffix = "Post", case = "upper_flat")] +//! enum Status { +//! #[enum_stringify(rename = "okay")] +//! Okk, +//! Error3, +//! } +//! +//! assert_eq!(Status::Okk.to_string(), "okay"); +//! assert_eq!(Status::Error3.to_string(), "PREERROR3POST"); +//! +//! assert_eq!(Status::try_from("okay").unwrap(), Status::Okk); +//! assert_eq!(Status::try_from("PREERROR3POST").unwrap(), Status::Error3); +//! ``` +//! +//! And using another case : +//! +//! +//! ``` +//! use enum_stringify::EnumStringify; +//! +//! #[derive(EnumStringify, Debug, PartialEq)] +//! #[enum_stringify(prefix = "Pre", suffix = "Post", case = "upper")] +//! enum Status { +//! #[enum_stringify(rename = "okay")] +//! Okk, +//! Error3, +//! } +//! +//! assert_eq!(Status::Okk.to_string(), "okay"); +//! assert_eq!(Status::Error3.to_string(), "PRE ERROR 3 POST"); +//! +//! assert_eq!(Status::try_from("okay").unwrap(), Status::Okk); +//! assert_eq!(Status::try_from("PRE ERROR 3 POST").unwrap(), Status::Error3); +//! ``` +//! +//! ## Error Handling +//! +//! When conversion from a string fails, the error type is `String`, containing a descriptive message: +//! +//! ``` +//! use enum_stringify::EnumStringify; +//! +//! #[derive(EnumStringify, Debug, PartialEq)] +//! #[enum_stringify(case = "lower")] +//! enum Numbers { +//! One, +//! Two, +//! } +//! +//! let result = Numbers::try_from("Three"); +//! assert!(result.is_err()); +//! assert_eq!(result.unwrap_err(), "Failed to parse string 'Three' for enum Numbers"); +//! ``` +//! +//! ## Generated Implementations +//! +//! The macro generates the following trait implementations: +//! +//! ```rust, no_run +//! enum Numbers { One, Two } +//! +//! impl ::std::fmt::Display for Numbers { +//! fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +//! match self { +//! Self::One => write!(f, "One"), +//! Self::Two => write!(f, "Two"), +//! } +//! } +//! } +//! +//! impl TryFrom<&str> for Numbers { +//! type Error = String; +//! +//! fn try_from(s: &str) -> Result { +//! match s { +//! "One" => Ok(Self::One), +//! "Two" => Ok(Self::Two), +//! _ => Err(format!("Invalid value '{}'", s)), +//! } +//! } +//! } +//! +//! impl TryFrom for Numbers { +//! type Error = String; +//! +//! fn try_from(s: String) -> Result { +//! s.as_str().try_into() +//! } +//! } +//! +//! impl ::std::str::FromStr for Numbers { +//! type Err = String; +//! +//! fn from_str(s: &str) -> Result { +//! s.try_into() +//! } +//! } +//! ``` use attributes::{Attributes, Variants}; use proc_macro::TokenStream; @@ -11,167 +212,9 @@ use syn::{parse_macro_input, DeriveInput}; mod attributes; mod case; -/// Derive [`std::fmt::Display`], [`std::str::FromStr`], [`TryFrom<&str>`] and -/// [`TryFrom`] for an enum. -/// -/// They simply take the name of the enum variant and convert it to a string. -/// -/// # Example -/// -/// ``` -/// use enum_stringify::EnumStringify; -/// use std::str::FromStr; -/// -/// #[derive(EnumStringify, Debug, PartialEq)] -/// enum Numbers { -/// One, -/// Two, -/// } -/// -/// assert_eq!(Numbers::One.to_string(), "One"); -/// assert_eq!(Numbers::Two.to_string(), "Two"); -/// -/// -/// assert_eq!(Numbers::try_from("One").unwrap(), Numbers::One); -/// assert_eq!(Numbers::try_from("Two").unwrap(), Numbers::Two); -/// -/// 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); -/// ``` -/// -/// # Case -/// -/// You can also set the case of the string representation of the enum variants. -/// Case conversion is provided by the [`convert_case`] crate. Refer to the variants -/// of the [`convert_case::Case`] enum for options (expressed in lower snake case). -/// The exception are the `Random` and `PseudoRandom` variants, which are not accepted. -/// -/// ``` -/// use enum_stringify::EnumStringify; -/// use std::str::FromStr; -/// -/// #[derive(EnumStringify, Debug, PartialEq)] -/// #[enum_stringify(case = "lower")] -/// enum Numbers { -/// One, -/// Two, -/// } -/// -/// assert_eq!(Numbers::One.to_string(), "one"); -/// assert_eq!(Numbers::Two.to_string(), "two"); -/// -/// assert_eq!(Numbers::try_from("one").unwrap(), Numbers::One); -/// assert_eq!(Numbers::try_from("two").unwrap(), Numbers::Two); -/// ``` -/// -/// # Rename variants -/// -/// You can rename the variants of the enum. -/// This is useful if you want to have a different name for the enum variants -/// and the string representation of the enum variants. -/// -/// ``` -/// use enum_stringify::EnumStringify; -/// use std::str::FromStr; -/// -/// #[derive(EnumStringify, Debug, PartialEq)] -/// enum Istari { -/// #[enum_stringify(rename = "Ólorin")] -/// Gandalf, -/// Saruman, -/// Radagast, -/// Alatar, -/// Pallando, -/// } -/// -/// assert_eq!(Istari::Gandalf.to_string(), "Ólorin"); -/// assert_eq!(Istari::Saruman.to_string(), "Saruman"); -/// assert_eq!(Istari::Radagast.to_string(), "Radagast"); -/// -/// assert_eq!(Istari::try_from("Ólorin").unwrap(), Istari::Gandalf); -/// assert_eq!(Istari::try_from("Saruman").unwrap(), Istari::Saruman); -/// assert_eq!(Istari::try_from("Radagast").unwrap(), Istari::Radagast); -/// ``` -/// -/// # Details -/// -/// The implementations of the above traits corresponds to this: -/// -/// ```rust, no_run -/// enum Numbers { -/// One, -/// Two, -/// } -/// -/// impl ::std::fmt::Display for Numbers { -/// fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { -/// match self { -/// Self::One => write!(f, "One"), -/// Self::Two => write!(f, "Two"), -/// } -/// } -/// } -/// -/// impl TryFrom<&str> for Numbers { -/// type Error = String; -/// -/// fn try_from(s: &str) -> Result { -/// match s { -/// "One" => Ok(Self::One), -/// "Two" => Ok(Self::Two), -/// _ => { -/// let mut err_msg = "Failed parse string ".to_string(); -/// err_msg.push_str(s); -/// err_msg.push_str(" for enum "); -/// err_msg.push_str("Numbers"); -/// Err(err_msg) -/// } -/// } -/// } -/// } -/// -/// impl TryFrom for Numbers { -/// type Error = String; -/// -/// fn try_from(s: String) -> Result { -/// s.as_str().try_into() -/// } -/// } -/// -/// impl ::std::str::FromStr for Numbers { -/// type Err = String; -/// -/// fn from_str(s: &str) -> Result { -/// s.try_into() -/// } -/// } -/// ``` #[proc_macro_derive(EnumStringify, attributes(enum_stringify))] pub fn enum_stringify(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - impl_enum_to_string(&ast) } @@ -191,66 +234,52 @@ fn impl_enum_to_string(ast: &syn::DeriveInput) -> TokenStream { let identifiers: Vec<&syn::Ident> = pairs.iter().map(|(i, _)| i).collect(); let names: Vec = pairs.iter().map(|(_, n)| n.clone()).collect(); - // Generate implementations for each trait. + // Generate implementations for each trait 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.extend(impl_try_from_str(name, &identifiers, &names)); + gen.extend(impl_try_from_string(name)); + gen.extend(impl_from_str(name)); gen } /// Implementation of [`std::fmt::Display`]. -fn impl_display( - name: &syn::Ident, - identifiers: &Vec<&syn::Ident>, - names: &Vec, -) -> TokenStream { - let gen = quote! { +fn impl_display(name: &syn::Ident, identifiers: &[&syn::Ident], names: &[String]) -> TokenStream { + quote! { impl ::std::fmt::Display for #name { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - #(Self::#identifiers=> write!(f, #names)),* + #(Self::#identifiers => write!(f, #names),)* } } } - }; - - gen.into() + } + .into() } /// Implementation of [`TryFrom<&str>`]. -fn impl_from_str( +fn impl_try_from_str( name: &syn::Ident, - identifiers: &Vec<&syn::Ident>, - names: &Vec, + identifiers: &[&syn::Ident], + names: &[String], ) -> TokenStream { - let name_str = name.to_string(); - let gen = quote! { + quote! { impl TryFrom<&str> for #name { type Error = String; fn try_from(s: &str) -> Result { match s { #(#names => Ok(Self::#identifiers),)* - _ => { - let mut err_msg = "Failed parse string ".to_string(); - err_msg.push_str(s); - err_msg.push_str(" for enum "); - err_msg.push_str(#name_str); - Err(err_msg) - } + _ => Err(format!("Failed to parse string '{}' for enum {}", s, stringify!(#name))), } } } - }; - - gen.into() + } + .into() } /// Implementation of [`TryFrom`]. -fn impl_from_string(name: &syn::Ident) -> TokenStream { - let gen = quote! { +fn impl_try_from_string(name: &syn::Ident) -> TokenStream { + quote! { impl TryFrom for #name { type Error = String; @@ -258,14 +287,13 @@ fn impl_from_string(name: &syn::Ident) -> TokenStream { s.as_str().try_into() } } - }; - - gen.into() + } + .into() } /// Implementation of [`std::str::FromStr`]. -fn impl_from_str_trait(name: &syn::Ident) -> TokenStream { - let gen = quote! { +fn impl_from_str(name: &syn::Ident) -> TokenStream { + quote! { impl ::std::str::FromStr for #name { type Err = String; @@ -273,7 +301,6 @@ fn impl_from_str_trait(name: &syn::Ident) -> TokenStream { s.try_into() } } - }; - - gen.into() + } + .into() } diff --git a/tests/all.rs b/tests/all.rs new file mode 100644 index 0000000..85e89e5 --- /dev/null +++ b/tests/all.rs @@ -0,0 +1,148 @@ +use enum_stringify::EnumStringify; +use std::convert::TryFrom; + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(case = "flat", prefix = "Pre", suffix = "Post")] +enum Action { + Start, + Stop, +} + +#[test] +fn test_flat_case_with_prefix_suffix() { + assert_eq!(Action::Start.to_string(), "prestartpost"); + assert_eq!(Action::Stop.to_string(), "prestoppost"); + + assert_eq!(Action::try_from("prestartpost").unwrap(), Action::Start); + assert_eq!(Action::try_from("prestoppost").unwrap(), Action::Stop); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(case = "upper_flat", prefix = "Start", suffix = "End")] +enum Phase { + Begin, + End, +} + +#[test] +fn test_upper_flat_case_with_prefix_suffix() { + assert_eq!(Phase::Begin.to_string(), "STARTBEGINEND"); + assert_eq!(Phase::End.to_string(), "STARTENDEND"); + + assert_eq!(Phase::try_from("STARTBEGINEND").unwrap(), Phase::Begin); + assert_eq!(Phase::try_from("STARTENDEND").unwrap(), Phase::End); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(case = "camel", prefix = "Begin", suffix = "Finish")] +enum Process { + StepOne, + StepTwo, +} + +#[test] +fn test_camel_case_with_prefix_suffix() { + assert_eq!(Process::StepOne.to_string(), "beginStepOneFinish"); + assert_eq!(Process::StepTwo.to_string(), "beginStepTwoFinish"); + + assert_eq!( + Process::try_from("beginStepOneFinish").unwrap(), + Process::StepOne + ); + assert_eq!( + Process::try_from("beginStepTwoFinish").unwrap(), + Process::StepTwo + ); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(case = "kebab", prefix = "start-", suffix = "-end")] +enum Task { + Initialize, + Complete, +} + +#[test] +fn test_kebab_case_with_prefix_suffix() { + assert_eq!(Task::Initialize.to_string(), "start-initialize-end"); + assert_eq!(Task::Complete.to_string(), "start-complete-end"); + + assert_eq!( + Task::try_from("start-initialize-end").unwrap(), + Task::Initialize + ); + assert_eq!( + Task::try_from("start-complete-end").unwrap(), + Task::Complete + ); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(case = "snake", prefix = "task_", suffix = "_done")] +enum Operation { + Start, + Stop, +} + +#[test] +fn test_snake_case_with_prefix_suffix() { + assert_eq!(Operation::Start.to_string(), "task_start_done"); + assert_eq!(Operation::Stop.to_string(), "task_stop_done"); + + assert_eq!( + Operation::try_from("task_start_done").unwrap(), + Operation::Start + ); + assert_eq!( + Operation::try_from("task_stop_done").unwrap(), + Operation::Stop + ); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(case = "upper_flat", prefix = "Prefix", suffix = "Suffix")] +enum Level { + Beginner, + Expert, +} + +#[test] +fn test_upper_case_with_prefix_suffix() { + assert_eq!(Level::Beginner.to_string(), "PREFIXBEGINNERSUFFIX"); + assert_eq!(Level::Expert.to_string(), "PREFIXEXPERTSUFFIX"); + + assert_eq!( + Level::try_from("PREFIXBEGINNERSUFFIX").unwrap(), + Level::Beginner + ); + assert_eq!( + Level::try_from("PREFIXEXPERTSUFFIX").unwrap(), + Level::Expert + ); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(case = "flat")] +enum CaseEnum { + FirstCase, + SecondCase, +} + +#[test] +fn test_special_characters_errors_detected() { + assert_eq!(CaseEnum::FirstCase.to_string(), "firstcase"); + assert_eq!(CaseEnum::SecondCase.to_string(), "secondcase"); + + assert_eq!( + CaseEnum::try_from("firstcase").unwrap(), + CaseEnum::FirstCase + ); + assert_eq!( + CaseEnum::try_from("secondcase").unwrap(), + CaseEnum::SecondCase + ); + + // Ensure incorrect casing fails + assert!(CaseEnum::try_from("FirstCase").is_err()); + assert!(CaseEnum::try_from("SECONDCASE").is_err()); +} diff --git a/tests/attributes.rs b/tests/attributes.rs index b6e152c..96b8433 100644 --- a/tests/attributes.rs +++ b/tests/attributes.rs @@ -1,6 +1,7 @@ +use enum_stringify::EnumStringify; use std::str::FromStr; -#[derive(Debug, PartialEq, enum_stringify::EnumStringify)] +#[derive(Debug, PartialEq, EnumStringify)] #[enum_stringify(suffix = "Suff")] enum Number1 { Zero, @@ -43,7 +44,7 @@ fn test_prefix_from_str() { assert_eq!(Number2::from_str("PrefTwo"), Ok(Number2::Two)); } -#[derive(Debug, PartialEq, enum_stringify::EnumStringify)] +#[derive(Debug, PartialEq, EnumStringify)] #[enum_stringify(prefix = "Pref", suffix = "Suff")] enum Number3 { Zero, @@ -65,9 +66,25 @@ fn test_prefix_suffix_from_str() { assert_eq!(Number3::from_str("PrefTwoSuff"), Ok(Number3::Two)); } +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(prefix = "Pre", suffix = "Post")] +enum Status { + Okk, + Error3, +} + +#[test] +fn test_prefix_suffix() { + assert_eq!(Status::Okk.to_string(), "PreOkkPost"); + assert_eq!(Status::Error3.to_string(), "PreError3Post"); + + assert_eq!(Status::try_from("PreOkkPost").unwrap(), Status::Okk); + assert_eq!(Status::try_from("PreError3Post").unwrap(), Status::Error3); +} + // Testing commutativity of prefix, suffix and case -#[derive(Debug, PartialEq, enum_stringify::EnumStringify)] +#[derive(Debug, PartialEq, EnumStringify)] #[enum_stringify(suffix = "Suff", prefix = "Pref", case = "flat")] enum Number4 { Zero, @@ -89,7 +106,7 @@ fn test_suffix_prefix_flat_from_str() { assert_eq!(Number4::from_str("preftwosuff"), Ok(Number4::Two)); } -#[derive(Debug, PartialEq, enum_stringify::EnumStringify)] +#[derive(Debug, PartialEq, EnumStringify)] #[enum_stringify(suffix = "Suff", prefix = "Pref", case = "upper_flat")] enum Number5 { Zero, @@ -111,7 +128,7 @@ fn test_suffix_prefix_upper_flat_from_str() { assert_eq!(Number5::from_str("PREFTWOSUFF"), Ok(Number5::Two)); } -#[derive(Debug, PartialEq, enum_stringify::EnumStringify)] +#[derive(Debug, PartialEq, EnumStringify)] #[enum_stringify(case = "lower")] enum Number6 { Zero, @@ -133,7 +150,7 @@ fn test_lower_from_str() { assert_eq!(Number6::from_str("two"), Ok(Number6::Two)); } -#[derive(Debug, PartialEq, enum_stringify::EnumStringify)] +#[derive(Debug, PartialEq, EnumStringify)] #[enum_stringify(case = "upper")] enum Number7 { Zero, @@ -154,3 +171,44 @@ fn test_upper_from_str() { assert_eq!(Number7::from_str("ONE"), Ok(Number7::One)); assert_eq!(Number7::from_str("TWO"), Ok(Number7::Two)); } + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(prefix = "😀", suffix = "🥳")] +enum UnicodeEnum { + Japanese, + Star, +} + +#[test] +fn test_unicode() { + assert_eq!(UnicodeEnum::Japanese.to_string(), "😀Japanese🥳"); + assert_eq!(UnicodeEnum::Star.to_string(), "😀Star🥳"); + + assert_eq!( + UnicodeEnum::try_from("😀Japanese🥳").unwrap(), + UnicodeEnum::Japanese + ); + assert_eq!( + UnicodeEnum::try_from("😀Star🥳").unwrap(), + UnicodeEnum::Star + ); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(prefix = "", suffix = "")] +enum Punctuated { + Excited, + Happy, +} + +#[test] +fn test_empty_suffix_prefix() { + assert_eq!(Punctuated::Excited.to_string(), "Excited"); + assert_eq!(Punctuated::Happy.to_string(), "Happy"); + + assert_eq!( + Punctuated::try_from("Excited").unwrap(), + Punctuated::Excited + ); + assert_eq!(Punctuated::try_from("Happy").unwrap(), Punctuated::Happy); +} diff --git a/tests/basic.rs b/tests/basic.rs index 35afbae..87b30b7 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,6 +1,8 @@ use std::str::FromStr; -#[derive(enum_stringify::EnumStringify, Debug, PartialEq)] +use enum_stringify::EnumStringify; + +#[derive(EnumStringify, Debug, PartialEq)] enum Numbers { One, Two, @@ -43,3 +45,61 @@ fn test_from_str_trait() { assert!(Numbers::from_str("Four").is_err()); } + +#[derive(EnumStringify, Debug, PartialEq)] +enum LargeEnum { + A1, + A2, + A3, + A4, + A5, + A6, + A7, + A8, + A9, + A10, + B1, + B2, + B3, + B4, + B5, + B6, + B7, + B8, + B9, + B10, + C1, + C2, + C3, + C4, + C5, + C6, + C7, + C8, + C9, + C10, +} + +#[test] +fn large_enum() { + assert_eq!(LargeEnum::A1.to_string(), "A1"); + assert_eq!(LargeEnum::C10.to_string(), "C10"); + + assert_eq!(LargeEnum::try_from("B5").unwrap(), LargeEnum::B5); + assert!(LargeEnum::try_from("Z100").is_err()); +} + +#[derive(EnumStringify, Debug, PartialEq)] +enum FuzzyMatch { + Alpha, + Beta, + Gamma, +} + +#[test] +fn wrong_similar_names() { + assert!(FuzzyMatch::try_from("alpha ").is_err()); // Extra space + assert!(FuzzyMatch::try_from("ALPHA").is_err()); // Wrong case + assert!(FuzzyMatch::try_from("alpHa").is_err()); // Mixed case + assert!(FuzzyMatch::try_from("Alphaa").is_err()); // Extra character +} diff --git a/tests/case.rs b/tests/case.rs new file mode 100644 index 0000000..da987ac --- /dev/null +++ b/tests/case.rs @@ -0,0 +1,55 @@ +use enum_stringify::EnumStringify; + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(case = "upper")] +enum Letters { + A, + B, +} + +#[test] +fn test_case_conversion_upper() { + assert_eq!(Letters::A.to_string(), "A"); + assert_eq!(Letters::B.to_string(), "B"); + + assert_eq!(Letters::try_from("A").unwrap(), Letters::A); + assert_eq!(Letters::try_from("B").unwrap(), Letters::B); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(case = "lower")] +enum Colors { + Red, + Blue, +} + +#[test] +fn test_case_conversion_lower() { + assert_eq!(Colors::Red.to_string(), "red"); + assert_eq!(Colors::Blue.to_string(), "blue"); + + assert_eq!(Colors::try_from("red").unwrap(), Colors::Red); + assert_eq!(Colors::try_from("blue").unwrap(), Colors::Blue); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(case = "upper_flat")] +enum Season { + Spring, + Summer, + Fall, + Winter, +} + +#[test] +fn test_upper_flat_case() { + assert_eq!(Season::Spring.to_string(), "SPRING"); + assert_eq!(Season::Summer.to_string(), "SUMMER"); + assert_eq!(Season::Fall.to_string(), "FALL"); + assert_eq!(Season::Winter.to_string(), "WINTER"); + + assert_eq!(Season::try_from("SPRING").unwrap(), Season::Spring); + assert_eq!(Season::try_from("SUMMER").unwrap(), Season::Summer); + assert_eq!(Season::try_from("FALL").unwrap(), Season::Fall); + assert_eq!(Season::try_from("WINTER").unwrap(), Season::Winter); +} diff --git a/tests/rename.rs b/tests/rename.rs index a06e316..48a472f 100644 --- a/tests/rename.rs +++ b/tests/rename.rs @@ -1,6 +1,7 @@ +use enum_stringify::EnumStringify; use std::str::FromStr; -#[derive(PartialEq, Debug, enum_stringify::EnumStringify)] +#[derive(PartialEq, Debug, EnumStringify)] enum Ainur { #[enum_stringify(rename = "Gods")] Valar, @@ -19,7 +20,7 @@ fn test_simple_rename_from_str() { assert_eq!(Ainur::from_str("Maiar"), Ok(Ainur::Maiar)); } -#[derive(PartialEq, Debug, enum_stringify::EnumStringify)] +#[derive(PartialEq, Debug, EnumStringify)] enum Ainur2 { #[enum_stringify(rename = "Gods")] Valar, @@ -39,7 +40,7 @@ fn test_simple_rename_from_str2() { assert_eq!(Ainur2::from_str("Raiam"), Ok(Ainur2::Maiar)); } -#[derive(PartialEq, Debug, enum_stringify::EnumStringify)] +#[derive(PartialEq, Debug, EnumStringify)] enum DoubleAniurRename { #[enum_stringify(rename = "Gods")] #[enum_stringify(rename = "Valar")] @@ -66,7 +67,7 @@ fn test_double_rename_from_str() { ); } -#[derive(PartialEq, Debug, enum_stringify::EnumStringify)] +#[derive(PartialEq, Debug, EnumStringify)] enum Seperator { #[enum_stringify(rename = " ")] Space, @@ -90,3 +91,116 @@ fn test_seperator_rename_from_str() { assert_eq!(Seperator::from_str(""), Ok(Seperator::Empty)); assert!(Seperator::from_str("|").is_err()); } + +#[derive(EnumStringify, Debug, PartialEq)] +enum Istari { + #[enum_stringify(rename = "Ólorin")] + Gandalf, + Saruman, +} + +#[test] +fn test_rename_variants() { + assert_eq!(Istari::Gandalf.to_string(), "Ólorin"); + assert_eq!(Istari::try_from("Ólorin").unwrap(), Istari::Gandalf); + assert_eq!(Istari::Saruman.to_string(), "Saruman"); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(prefix = "Pre", suffix = "Post", case = "upper_flat")] +enum Severity { + #[enum_stringify(rename = "critical")] + High, + Low, +} + +#[test] +fn test_all_options() { + assert_eq!(Severity::High.to_string(), "critical"); + assert_eq!(Severity::Low.to_string(), "PRELOWPOST"); + + assert_eq!(Severity::try_from("critical").unwrap(), Severity::High); + assert_eq!(Severity::try_from("PRELOWPOST").unwrap(), Severity::Low); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(prefix = "Pre", suffix = "Post", case = "upper_flat")] +enum Response { + #[enum_stringify(rename = "okay")] + Success, + ErroR, +} + +#[test] +fn test_combined_prefix_suffix_case_rename() { + assert_eq!(Response::Success.to_string(), "okay"); + assert_eq!(Response::ErroR.to_string(), "PREERRORPOST"); + + assert_eq!(Response::try_from("okay").unwrap(), Response::Success); + assert_eq!(Response::try_from("PREERRORPOST").unwrap(), Response::ErroR); +} + +#[derive(EnumStringify, Debug, PartialEq)] +enum SpecialChars { + #[enum_stringify(rename = "Hello, World!")] + HelloWorld, + + #[enum_stringify(rename = "100%Success")] + FullSuccess, + + #[enum_stringify(rename = "Café")] + Cafe, +} + +#[test] +fn test_special_characters() { + assert_eq!(SpecialChars::HelloWorld.to_string(), "Hello, World!"); + assert_eq!(SpecialChars::FullSuccess.to_string(), "100%Success"); + assert_eq!(SpecialChars::Cafe.to_string(), "Café"); + + assert_eq!( + SpecialChars::try_from("Hello, World!").unwrap(), + SpecialChars::HelloWorld + ); + assert_eq!( + SpecialChars::try_from("100%Success").unwrap(), + SpecialChars::FullSuccess + ); + assert_eq!(SpecialChars::try_from("Café").unwrap(), SpecialChars::Cafe); +} + +#[derive(EnumStringify, Debug, PartialEq)] +enum UnicodeEnumRename { + #[enum_stringify(rename = "日本語")] + Japanese, + + #[enum_stringify(rename = "🌟 Star")] + Star, +} + +#[test] +fn test_unicode_rename() { + assert_eq!(UnicodeEnumRename::Japanese.to_string(), "日本語"); + assert_eq!(UnicodeEnumRename::Star.to_string(), "🌟 Star"); + + assert_eq!( + UnicodeEnumRename::try_from("日本語").unwrap(), + UnicodeEnumRename::Japanese + ); + assert_eq!( + UnicodeEnumRename::try_from("🌟 Star").unwrap(), + UnicodeEnumRename::Star + ); +} + +#[derive(EnumStringify, Debug, PartialEq)] +enum EmptyString { + #[enum_stringify(rename = "")] + Silent, +} + +#[test] +fn empty_rename() { + assert_eq!(EmptyString::Silent.to_string(), ""); + assert_eq!(EmptyString::try_from("").unwrap(), EmptyString::Silent); +}