From 701d4c0ae1e7ab612faf8ffc04bc67974b512448 Mon Sep 17 00:00:00 2001 From: Yago Iglesias Date: Wed, 12 Mar 2025 17:36:37 +0100 Subject: [PATCH 01/10] fix: parse_string is more general now and better error messages --- src/attributes.rs | 16 ++++++++-------- src/case.rs | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/attributes.rs b/src/attributes.rs index 245b490..9636c51 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -7,11 +7,11 @@ use syn::{DeriveInput, Meta}; 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()) +fn parse_string(s: &str) -> Result { + if let Some(stripped) = s.strip_prefix('"').and_then(|s| s.strip_suffix('"')) { + Ok(stripped.to_string()) } else { - Err(()) + Err("String must be enclosed in double quotes") } } @@ -20,13 +20,13 @@ fn parse_string(s: &str) -> Result { 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())?)) } else { - Err(()) + Err("Not a rename string") } } } @@ -71,7 +71,7 @@ enum RenameAttribute { } impl TryFrom<(String, String)> for RenameAttribute { - type Error = (); + type Error = &'static str; fn try_from(value: (String, String)) -> Result { if value.0 == "prefix" { @@ -81,7 +81,7 @@ impl TryFrom<(String, String)> for RenameAttribute { } else if value.0 == "case" { Ok(Self::Case(Case::try_from(value)?)) } else { - Err(()) + Err("Not a rename attribute") } } } 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")?, })) } } From c1ffbc11cb30f6e010ce6ef139a00263d97bdf69 Mon Sep 17 00:00:00 2001 From: Yago Iglesias Date: Wed, 12 Mar 2025 17:55:37 +0100 Subject: [PATCH 02/10] refactor: Extract renaming to Attributes --- src/attributes.rs | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/attributes.rs b/src/attributes.rs index 9636c51..6bca03e 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -186,6 +186,25 @@ impl Attributes { } Ok(result) } + + fn rename(&self, s: &str) -> String { + let mut new_name = String::new(); + if let Some(prefix) = &self.prefix { + new_name.push_str(prefix); + } + + new_name.push_str(s); + + if let Some(suffix) = &self.suffix { + new_name.push_str(suffix); + } + + if let Some(case) = &self.case { + new_name = case.to_case(&new_name); + } + + new_name + } } /// Stores enum variants and their optional renaming attributes. @@ -230,22 +249,7 @@ impl Variants { 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); + new_names.push(attributes.rename(name.to_string().as_str())); } let tmp = self From 4cabb7925a2139b619b694576ea07f063f3802f9 Mon Sep 17 00:00:00 2001 From: Yago Iglesias Date: Wed, 12 Mar 2025 18:06:15 +0100 Subject: [PATCH 03/10] refactor: Remove unnecessary enum --- src/attributes.rs | 109 ++++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 66 deletions(-) diff --git a/src/attributes.rs b/src/attributes.rs index 6bca03e..2ba7bfa 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -15,6 +15,37 @@ fn parse_string(s: &str) -> Result { } } +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}")), + } + + if let Some(comma_separator) = tokens.next() { + assert!( + comma_separator.to_string() == ",", + "Expected a comma separated attribute list" + ); + } + } + Ok(result) +} + #[derive(Clone)] /// Represents a rename attribute applied to an enum variant. struct VariantRename(String); @@ -49,7 +80,7 @@ impl VariantRename { if path == vec![ATTRIBUTE_NAME] { Some( - Attributes::parse_token_list::(&list.tokens) + parse_token_list::(&list.tokens) .ok()? .first()? .clone(), @@ -63,29 +94,6 @@ impl VariantRename { } } -// 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 = &'static str; - - 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("Not a rename attribute") - } - } -} - #[derive(Default)] pub(crate) struct Attributes { case: Option, @@ -132,10 +140,9 @@ impl Attributes { .collect::>(); if path == vec![ATTRIBUTE_NAME] { - let attributes = - Attributes::parse_token_list::(&list.tokens).ok()?; - for attr in attributes { - new.merge_attribute(attr); + let attributes = parse_token_list::<(String, String)>(&list.tokens).ok()?; + for value in attributes { + new.update_attribute(value); } Some(new) } else { @@ -146,45 +153,15 @@ impl Attributes { } } - /// 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), - } - } - - /// 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}")), - } - - if let Some(comma_separator) = tokens.next() { - assert!( - comma_separator.to_string() == ",", - "Expected a comma separated attribute list" - ); - } + /// Updates an attribute with a new parameter + fn update_attribute(&mut self, value: (String, String)) { + if value.0 == "prefix" { + self.prefix = Some(parse_string(value.1.as_str()).expect("Not a rename attribute")); + } else if value.0 == "suffix" { + self.suffix = Some(parse_string(value.1.as_str()).expect("Not a rename attribute")); + } else if value.0 == "case" { + self.case = Some(Case::try_from(value).expect("Not a rename attribute")); } - Ok(result) } fn rename(&self, s: &str) -> String { From 682dd9b487d931eefd9f4798e32d037713061c90 Mon Sep 17 00:00:00 2001 From: Yago Iglesias Date: Wed, 12 Mar 2025 19:37:15 +0100 Subject: [PATCH 04/10] refactor(attributes): Simplify apply --- src/attributes.rs | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/attributes.rs b/src/attributes.rs index 2ba7bfa..ed02de1 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -217,25 +217,22 @@ impl Variants { 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. + /// + /// This method determines the final string representation of each variant + /// based on the `#[enum_stringify(...)]` attributes, including renaming, + /// prefix/suffix, and case transformation. 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; - } - new_names.push(attributes.rename(name.to_string().as_str())); - } - - 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()) + }; + (ident.clone(), new_name) + }) + .collect() } } From 6297eaefefd6191b5f648ae0137ab90dd1a9e9b2 Mon Sep 17 00:00:00 2001 From: Yago Iglesias Date: Wed, 12 Mar 2025 19:55:57 +0100 Subject: [PATCH 05/10] refactor: big refactor --- src/attributes.rs | 166 ++++++++++++++++++++-------------------------- 1 file changed, 71 insertions(+), 95 deletions(-) diff --git a/src/attributes.rs b/src/attributes.rs index ed02de1..9755aff 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -1,20 +1,36 @@ +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. +/// 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 { - if let Some(stripped) = s.strip_prefix('"').and_then(|s| s.strip_suffix('"')) { - Ok(stripped.to_string()) - } else { - Err("String must be enclosed in double quotes") - } + s.strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + .map(|s| s.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)>, @@ -25,11 +41,14 @@ where 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(); + let Some(eq_token) = tokens.next() else { + return Err(format!("Expected '=' after '{}'", attribute_type)); + }; + if eq_token.to_string() != "=" { + return Err(format!("Unexpected token '{}', expected '='", eq_token)); + } + + 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), @@ -37,17 +56,16 @@ where } if let Some(comma_separator) = tokens.next() { - assert!( - comma_separator.to_string() == ",", - "Expected a comma separated attribute list" - ); + 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 { @@ -55,7 +73,7 @@ impl TryFrom<(String, String)> for VariantRename { 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("Not a rename string") } @@ -63,37 +81,23 @@ impl TryFrom<(String, String)> for VariantRename { } 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( - parse_token_list::(&list.tokens) - .ok()? - .first()? - .clone(), - ) - } else { - None - } - } + Meta::List(list) => parse_token_list::(&list.tokens) + .ok()? + .first() + .cloned(), _ => None, } } } +/// Represents attribute configurations for renaming enum variants. #[derive(Default)] pub(crate) struct Attributes { case: Option, @@ -102,95 +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 = parse_token_list::<(String, String)>(&list.tokens).ok()?; - for value in attributes { - new.update_attribute(value); - } - 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, } } - /// Updates an attribute with a new parameter fn update_attribute(&mut self, value: (String, String)) { - if value.0 == "prefix" { - self.prefix = Some(parse_string(value.1.as_str()).expect("Not a rename attribute")); - } else if value.0 == "suffix" { - self.suffix = Some(parse_string(value.1.as_str()).expect("Not a rename attribute")); - } else if value.0 == "case" { - self.case = Some(Case::try_from(value).expect("Not a rename attribute")); + 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(), + _ => {} } } - fn rename(&self, s: &str) -> String { - let mut new_name = String::new(); + /// 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(prefix) = &self.prefix { - new_name.push_str(prefix); + new_name = Cow::Owned(format!("{}{}", prefix, new_name)); } - - new_name.push_str(s); - if let Some(suffix) = &self.suffix { - new_name.push_str(suffix); + new_name = Cow::Owned(format!("{}{}", new_name, suffix)); } - if let Some(case) = &self.case { - new_name = case.to_case(&new_name); + 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(), @@ -204,24 +183,20 @@ 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 renaming rules to each enum variant name. - /// - /// This method determines the final string representation of each variant - /// based on the `#[enum_stringify(...)]` attributes, including renaming, - /// prefix/suffix, and case transformation. pub(crate) fn apply(&self, attributes: &Attributes) -> Vec<(syn::Ident, String)> { self.variant_renames .iter() @@ -229,10 +204,11 @@ impl Variants { let new_name = if let Some(rename) = rename { rename.0.clone() } else { - attributes.rename(ident.to_string().as_str()) + attributes.rename(ident.to_string().as_str()).into_owned() }; (ident.clone(), new_name) }) .collect() } } + From 98dd378417fcd06603112b4903b749983866d800 Mon Sep 17 00:00:00 2001 From: Yago Iglesias Date: Wed, 12 Mar 2025 19:56:57 +0100 Subject: [PATCH 06/10] chore: Minor lints --- src/attributes.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/attributes.rs b/src/attributes.rs index 9755aff..662f4c1 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -19,7 +19,7 @@ static ATTRIBUTE_NAME: &str = "enum_stringify"; fn parse_string(s: &str) -> Result { s.strip_prefix('"') .and_then(|s| s.strip_suffix('"')) - .map(|s| s.to_string()) + .map(std::string::ToString::to_string) .ok_or("String must be enclosed in double quotes") } @@ -42,10 +42,10 @@ where let attribute_type = attribute_type.to_string(); let Some(eq_token) = tokens.next() else { - return Err(format!("Expected '=' after '{}'", attribute_type)); + return Err(format!("Expected '=' after '{attribute_type}'")); }; if eq_token.to_string() != "=" { - return Err(format!("Unexpected token '{}', expected '='", eq_token)); + return Err(format!("Unexpected token '{eq_token}', expected '='")); } let value = tokens.next().ok_or("Value must be specified")?.to_string(); @@ -151,10 +151,10 @@ impl Attributes { let mut new_name = Cow::Borrowed(s); if let Some(prefix) = &self.prefix { - new_name = Cow::Owned(format!("{}{}", prefix, new_name)); + new_name = Cow::Owned(format!("{prefix}{new_name}")); } if let Some(suffix) = &self.suffix { - new_name = Cow::Owned(format!("{}{}", new_name, suffix)); + new_name = Cow::Owned(format!("{new_name}{suffix}")); } if let Some(case) = &self.case { new_name = Cow::Owned(case.to_case(&new_name)); From ca79f85613926553c744c060bcd76cfbab0c4e83 Mon Sep 17 00:00:00 2001 From: Yago Iglesias Date: Wed, 12 Mar 2025 20:37:39 +0100 Subject: [PATCH 07/10] doc: improve lib documentation --- src/attributes.rs | 1 - src/lib.rs | 417 ++++++++++++++++++++++++---------------------- 2 files changed, 220 insertions(+), 198 deletions(-) diff --git a/src/attributes.rs b/src/attributes.rs index 662f4c1..b4e4c6d 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -211,4 +211,3 @@ impl Variants { .collect() } } - diff --git a/src/lib.rs b/src/lib.rs index ecec732..70495c2 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,48 @@ 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 } /// 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( - name: &syn::Ident, - identifiers: &Vec<&syn::Ident>, - names: &Vec, -) -> TokenStream { - let name_str = name.to_string(); - let gen = quote! { +fn impl_from_str(name: &syn::Ident, identifiers: &[&syn::Ident], names: &[String]) -> TokenStream { + 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! { + quote! { impl TryFrom for #name { type Error = String; @@ -258,14 +283,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! { + quote! { impl ::std::str::FromStr for #name { type Err = String; @@ -273,7 +297,6 @@ fn impl_from_str_trait(name: &syn::Ident) -> TokenStream { s.try_into() } } - }; - - gen.into() + } + .into() } From 8782f646491f45d6468c97c0e4fa5e5d1c8ed871 Mon Sep 17 00:00:00 2001 From: Yago Iglesias Date: Wed, 12 Mar 2025 20:38:16 +0100 Subject: [PATCH 08/10] refactor: rename trait implementing functions to match their trait --- src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 70495c2..24529ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -236,9 +236,9 @@ fn impl_enum_to_string(ast: &syn::DeriveInput) -> TokenStream { // 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 } @@ -257,7 +257,7 @@ fn impl_display(name: &syn::Ident, identifiers: &[&syn::Ident], names: &[String] } /// Implementation of [`TryFrom<&str>`]. -fn impl_from_str(name: &syn::Ident, identifiers: &[&syn::Ident], names: &[String]) -> TokenStream { +fn impl_try_from_str(name: &syn::Ident, identifiers: &[&syn::Ident], names: &[String]) -> TokenStream { quote! { impl TryFrom<&str> for #name { type Error = String; @@ -274,7 +274,7 @@ fn impl_from_str(name: &syn::Ident, identifiers: &[&syn::Ident], names: &[String } /// Implementation of [`TryFrom`]. -fn impl_from_string(name: &syn::Ident) -> TokenStream { +fn impl_try_from_string(name: &syn::Ident) -> TokenStream { quote! { impl TryFrom for #name { type Error = String; @@ -288,7 +288,7 @@ fn impl_from_string(name: &syn::Ident) -> TokenStream { } /// Implementation of [`std::str::FromStr`]. -fn impl_from_str_trait(name: &syn::Ident) -> TokenStream { +fn impl_from_str(name: &syn::Ident) -> TokenStream { quote! { impl ::std::str::FromStr for #name { type Err = String; From 428379335c538434290881a817192811be667096 Mon Sep 17 00:00:00 2001 From: Yago Iglesias Date: Wed, 12 Mar 2025 20:54:36 +0100 Subject: [PATCH 09/10] test: Add more tests --- src/lib.rs | 6 +- tests/battery.rs | 465 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 tests/battery.rs diff --git a/src/lib.rs b/src/lib.rs index 24529ae..0ab9e03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -257,7 +257,11 @@ fn impl_display(name: &syn::Ident, identifiers: &[&syn::Ident], names: &[String] } /// Implementation of [`TryFrom<&str>`]. -fn impl_try_from_str(name: &syn::Ident, identifiers: &[&syn::Ident], names: &[String]) -> TokenStream { +fn impl_try_from_str( + name: &syn::Ident, + identifiers: &[&syn::Ident], + names: &[String], +) -> TokenStream { quote! { impl TryFrom<&str> for #name { type Error = String; diff --git a/tests/battery.rs b/tests/battery.rs new file mode 100644 index 0000000..da1c0f8 --- /dev/null +++ b/tests/battery.rs @@ -0,0 +1,465 @@ +use enum_stringify::EnumStringify; +use std::convert::TryFrom; +use std::str::FromStr; + +#[derive(EnumStringify, Debug, PartialEq)] +enum Numbers { + One, + Two, +} + +#[test] +fn test_display() { + assert_eq!(Numbers::One.to_string(), "One"); + assert_eq!(Numbers::Two.to_string(), "Two"); +} + +#[test] +fn test_try_from_str() { + 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()); +} + +#[test] +fn test_try_from_string() { + assert_eq!(Numbers::try_from("One".to_string()).unwrap(), Numbers::One); + assert_eq!(Numbers::try_from("Two".to_string()).unwrap(), Numbers::Two); + assert!(Numbers::try_from("Three".to_string()).is_err()); +} + +#[test] +fn test_from_str() { + assert_eq!(Numbers::from_str("One").unwrap(), Numbers::One); + assert_eq!(Numbers::from_str("Two").unwrap(), Numbers::Two); + assert!(Numbers::from_str("Three").is_err()); +} + +#[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); +} + +#[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 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_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")] +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); +} + +#[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 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_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()); +} + +#[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); +} + +#[derive(EnumStringify, Debug, PartialEq)] +#[enum_stringify(prefix = "😀", suffix = "🥳")] +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_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 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 EmptyString { + #[enum_stringify(rename = "")] + Silent, +} + +#[test] +fn empty_rename() { + assert_eq!(EmptyString::Silent.to_string(), ""); + assert_eq!(EmptyString::try_from("").unwrap(), EmptyString::Silent); +} + +#[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 +} From 346a342f90afff8bf9c0ceac052083c6731cab9a Mon Sep 17 00:00:00 2001 From: Yago Iglesias Date: Wed, 12 Mar 2025 21:07:17 +0100 Subject: [PATCH 10/10] tests: move tests around --- tests/all.rs | 148 ++++++++++++++ tests/attributes.rs | 70 ++++++- tests/basic.rs | 62 +++++- tests/battery.rs | 465 -------------------------------------------- tests/case.rs | 55 ++++++ tests/rename.rs | 122 +++++++++++- 6 files changed, 446 insertions(+), 476 deletions(-) create mode 100644 tests/all.rs delete mode 100644 tests/battery.rs create mode 100644 tests/case.rs 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/battery.rs b/tests/battery.rs deleted file mode 100644 index da1c0f8..0000000 --- a/tests/battery.rs +++ /dev/null @@ -1,465 +0,0 @@ -use enum_stringify::EnumStringify; -use std::convert::TryFrom; -use std::str::FromStr; - -#[derive(EnumStringify, Debug, PartialEq)] -enum Numbers { - One, - Two, -} - -#[test] -fn test_display() { - assert_eq!(Numbers::One.to_string(), "One"); - assert_eq!(Numbers::Two.to_string(), "Two"); -} - -#[test] -fn test_try_from_str() { - 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()); -} - -#[test] -fn test_try_from_string() { - assert_eq!(Numbers::try_from("One".to_string()).unwrap(), Numbers::One); - assert_eq!(Numbers::try_from("Two".to_string()).unwrap(), Numbers::Two); - assert!(Numbers::try_from("Three".to_string()).is_err()); -} - -#[test] -fn test_from_str() { - assert_eq!(Numbers::from_str("One").unwrap(), Numbers::One); - assert_eq!(Numbers::from_str("Two").unwrap(), Numbers::Two); - assert!(Numbers::from_str("Three").is_err()); -} - -#[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); -} - -#[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 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_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")] -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); -} - -#[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 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_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()); -} - -#[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); -} - -#[derive(EnumStringify, Debug, PartialEq)] -#[enum_stringify(prefix = "😀", suffix = "🥳")] -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_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 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 EmptyString { - #[enum_stringify(rename = "")] - Silent, -} - -#[test] -fn empty_rename() { - assert_eq!(EmptyString::Silent.to_string(), ""); - assert_eq!(EmptyString::try_from("").unwrap(), EmptyString::Silent); -} - -#[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); +}