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}