enum_stringify/
lib.rs

1//! # enum-stringify
2//!
3//! A procedural macro that derives implementations for:
4//! - [`std::fmt::Display`]: Converts enum variants to their string representations.
5//! - [`std::str::FromStr`]: Parses a string into an enum variant.
6//! - [`TryFrom<&str>`] and [`TryFrom<String>`]: Alternative conversion methods.
7//!
8//! ## Example
9//!
10//! ```
11//! use enum_stringify::EnumStringify;
12//! use std::str::FromStr;
13//!
14//! #[derive(EnumStringify, Debug, PartialEq)]
15//! enum Numbers {
16//!    One,
17//!    Two,
18//! }
19//!
20//! assert_eq!(Numbers::One.to_string(), "One");
21//! assert_eq!(Numbers::Two.to_string(), "Two");
22//!
23//!
24//! assert_eq!(Numbers::try_from("One").unwrap(), Numbers::One);
25//! assert_eq!(Numbers::try_from("Two").unwrap(), Numbers::Two);
26//!
27//! assert!(Numbers::try_from("Three").is_err());
28//! ```
29//!
30//! ## Custom Prefix and Suffix
31//!
32//! You can add a prefix and/or suffix to the string representation:
33//!
34//! ```
35//! use enum_stringify::EnumStringify;
36//!
37//! #[derive(EnumStringify, Debug, PartialEq)]
38//! #[enum_stringify(prefix = "Pre", suffix = "Post")]
39//! enum Numbers {
40//!     One,
41//!     Two,
42//! }
43//!
44//! assert_eq!(Numbers::One.to_string(), "PreOnePost");
45//! assert_eq!(Numbers::try_from("PreOnePost").unwrap(), Numbers::One);
46//! ```
47//!
48//! ## Case Conversion
49//!
50//! Convert enum variant names to different cases using the [`convert_case`] crate.
51//!
52//! ```
53//! use enum_stringify::EnumStringify;
54//!
55//! #[derive(EnumStringify, Debug, PartialEq)]
56//! #[enum_stringify(case = "flat")]
57//! enum Numbers {
58//!     One,
59//!     Two,
60//! }
61//!
62//! assert_eq!(Numbers::One.to_string(), "one");
63//! assert_eq!(Numbers::try_from("one").unwrap(), Numbers::One);
64//! ```
65//!
66//! ## Rename Variants
67//!
68//! Customize the string representation of specific variants:
69//!
70//! ```
71//! use enum_stringify::EnumStringify;
72//!
73//! #[derive(EnumStringify, Debug, PartialEq)]
74//! enum Istari {
75//!     #[enum_stringify(rename = "Ólorin")]
76//!     Gandalf,
77//!     Saruman,
78//! }
79//!
80//! assert_eq!(Istari::Gandalf.to_string(), "Ólorin");
81//! assert_eq!(Istari::try_from("Ólorin").unwrap(), Istari::Gandalf);
82//! ```
83//!
84//! This takes precedence over the other attributes :
85//!
86//! ```
87//! use enum_stringify::EnumStringify;
88//!
89//! #[derive(EnumStringify, Debug, PartialEq)]
90//! #[enum_stringify(prefix = "Pre", suffix = "Post", case = "upper")]
91//! enum Istari {
92//!     #[enum_stringify(rename = "Ólorin")]
93//!     Gandalf,
94//! }
95//!
96//! assert_eq!(Istari::Gandalf.to_string(), "Ólorin");
97//! assert_eq!(Istari::try_from("Ólorin").unwrap(), Istari::Gandalf);
98//! ```
99//!
100//! ## Using All Options Together
101//!
102//! You can combine all options: renaming, prefix, suffix, and case conversion.
103//!
104//! ```
105//! use enum_stringify::EnumStringify;
106//!
107//! #[derive(EnumStringify, Debug, PartialEq)]
108//! #[enum_stringify(prefix = "Pre", suffix = "Post", case = "upper_flat")]
109//! enum Status {
110//!     #[enum_stringify(rename = "okay")]
111//!     Okk,
112//!     Error3,
113//! }
114//!
115//! assert_eq!(Status::Okk.to_string(), "okay");
116//! assert_eq!(Status::Error3.to_string(), "PREERROR3POST");
117//!
118//! assert_eq!(Status::try_from("okay").unwrap(), Status::Okk);
119//! assert_eq!(Status::try_from("PREERROR3POST").unwrap(), Status::Error3);
120//! ```
121//!
122//! And using another case :
123//!
124//!
125//! ```
126//! use enum_stringify::EnumStringify;
127//!
128//! #[derive(EnumStringify, Debug, PartialEq)]
129//! #[enum_stringify(prefix = "Pre", suffix = "Post", case = "upper")]
130//! enum Status {
131//!     #[enum_stringify(rename = "okay")]
132//!     Okk,
133//!     Error3,
134//! }
135//!
136//! assert_eq!(Status::Okk.to_string(), "okay");
137//! assert_eq!(Status::Error3.to_string(), "PRE ERROR 3 POST");
138//!
139//! assert_eq!(Status::try_from("okay").unwrap(), Status::Okk);
140//! assert_eq!(Status::try_from("PRE ERROR 3 POST").unwrap(), Status::Error3);
141//! ```
142//!
143//! ## Error Handling
144//!
145//! When conversion from a string fails, the error type is `String`, containing a descriptive message:
146//!
147//! ```
148//! use enum_stringify::EnumStringify;
149//!
150//! #[derive(EnumStringify, Debug, PartialEq)]
151//! #[enum_stringify(case = "lower")]
152//! enum Numbers {
153//!     One,
154//!     Two,
155//! }
156//!
157//! let result = Numbers::try_from("Three");
158//! assert!(result.is_err());
159//! assert_eq!(result.unwrap_err(), "Failed to parse string 'Three' for enum Numbers");
160//! ```
161//!
162//! ## Generated Implementations
163//!
164//! The macro generates the following trait implementations:
165//!
166//! ```rust, no_run
167//! enum Numbers { One, Two }
168//!
169//! impl ::std::fmt::Display for Numbers {
170//!     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
171//!         match self {
172//!             Self::One => write!(f, "One"),
173//!             Self::Two => write!(f, "Two"),
174//!         }
175//!     }
176//! }
177//!
178//! impl TryFrom<&str> for Numbers {
179//!     type Error = String;
180//!
181//!     fn try_from(s: &str) -> Result<Self, Self::Error> {
182//!         match s {
183//!             "One" => Ok(Self::One),
184//!             "Two" => Ok(Self::Two),
185//!             _ => Err(format!("Invalid value '{}'", s)),
186//!         }
187//!     }
188//! }
189//!
190//! impl TryFrom<String> for Numbers {
191//!     type Error = String;
192//!
193//!     fn try_from(s: String) -> Result<Self, Self::Error> {
194//!         s.as_str().try_into()
195//!     }
196//! }
197//!
198//! impl ::std::str::FromStr for Numbers {
199//!     type Err = String;
200//!
201//!     fn from_str(s: &str) -> Result<Self, Self::Err> {
202//!         s.try_into()
203//!     }
204//! }
205//! ```
206
207use attributes::{Attributes, Variants};
208use proc_macro::TokenStream;
209use quote::quote;
210use syn::{parse_macro_input, DeriveInput};
211
212mod attributes;
213mod case;
214
215#[proc_macro_derive(EnumStringify, attributes(enum_stringify))]
216pub fn enum_stringify(input: TokenStream) -> TokenStream {
217    let ast = parse_macro_input!(input as DeriveInput);
218    impl_enum_to_string(&ast)
219}
220
221/// Generates the implementation of `Display`, `FromStr`, `TryFrom<&str>`, and `TryFrom<String>`
222/// for the given enum.
223fn impl_enum_to_string(ast: &syn::DeriveInput) -> TokenStream {
224    // Extract attributes and variant information from the given AST.
225    let attributes = Attributes::new(ast);
226    let variants = Variants::new(ast);
227
228    // Apply rename attributes to the enum variants.
229    let pairs = variants.apply(&attributes);
230
231    // Extract the enum name.
232    let name = &ast.ident;
233
234    let identifiers: Vec<&syn::Ident> = pairs.iter().map(|(i, _)| i).collect();
235    let names: Vec<String> = pairs.iter().map(|(_, n)| n.clone()).collect();
236
237    // Generate implementations for each trait
238    let mut gen = impl_display(name, &identifiers, &names);
239    gen.extend(impl_try_from_str(name, &identifiers, &names));
240    gen.extend(impl_try_from_string(name));
241    gen.extend(impl_from_str(name));
242    gen
243}
244
245/// Implementation of [`std::fmt::Display`].
246fn impl_display(name: &syn::Ident, identifiers: &[&syn::Ident], names: &[String]) -> TokenStream {
247    quote! {
248        impl ::std::fmt::Display for #name {
249            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
250                match self {
251                    #(Self::#identifiers => write!(f, #names),)*
252                }
253            }
254        }
255    }
256    .into()
257}
258
259/// Implementation of [`TryFrom<&str>`].
260fn impl_try_from_str(
261    name: &syn::Ident,
262    identifiers: &[&syn::Ident],
263    names: &[String],
264) -> TokenStream {
265    quote! {
266        impl TryFrom<&str> for #name {
267            type Error = String;
268
269            fn try_from(s: &str) -> Result<Self, Self::Error> {
270                match s {
271                    #(#names => Ok(Self::#identifiers),)*
272                    _ => Err(format!("Failed to parse string '{}' for enum {}", s, stringify!(#name))),
273                }
274            }
275        }
276    }
277    .into()
278}
279
280/// Implementation of [`TryFrom<String>`].
281fn impl_try_from_string(name: &syn::Ident) -> TokenStream {
282    quote! {
283        impl TryFrom<String> for #name {
284            type Error = String;
285
286            fn try_from(s: String) -> Result<Self, Self::Error> {
287                s.as_str().try_into()
288            }
289        }
290    }
291    .into()
292}
293
294/// Implementation of [`std::str::FromStr`].
295fn impl_from_str(name: &syn::Ident) -> TokenStream {
296    quote! {
297        impl ::std::str::FromStr for #name {
298            type Err = String;
299
300            fn from_str(s: &str) -> Result<Self, Self::Err> {
301                s.try_into()
302            }
303        }
304    }
305    .into()
306}