strum_lite/
lib.rs

1//! Lightweight declarative macro for sets of strings.
2//!
3//! ```
4//! strum_lite::strum! {
5//!     pub enum Casing {
6//!         Kebab = "kebab-case",
7//!         ScreamingSnake = "SCREAMING_SNAKE",
8//!     }
9//! }
10//! ```
11//!
12//! # Features
13//! - Implements [`FromStr`](core::str::FromStr) and [`Display`](core::fmt::Display).
14//! - Attributes (docs, `#[derive(..)]`s) are passed through to the definition and variants.
15//! - Aliases are supported.
16//! - Custom enum discriminants are passed through.
17//! - Generated code is `#![no_std]`.
18//! - The generated [`FromStr::Err`](core::str::FromStr) provides a helpful error message.
19//! - You may ask for a custom error type rather than using this crate's [`ParseError`].
20//!
21//! ```
22//! strum_lite::with_error! {
23//!     #[derive(Default)]
24//!     #[repr(u8)]
25//!     pub enum Casing {
26//!         Kebab = "kebab-case" | "kebab" = 1,
27//!         #[default]
28//!         ScreamingSnake = "SCREAMING_SNAKE" = 1 + 2,
29//!     }
30//!     throws
31//!     #[derive(Clone, Copy)]
32//!     ParseCasingError;
33//! }
34//! ```
35
36#![no_std]
37
38#[doc(hidden)]
39pub use core;
40
41use core::fmt;
42
43/// Pointer-wide shared error type for [`strum!`].
44#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub struct ParseError(#[doc(hidden)] pub &'static &'static [&'static str]);
46
47impl fmt::Display for ParseError {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self.0 {
50            [] => f.write_str("Uninhabited type is impossible to parse"),
51            [first] => f.write_fmt(format_args!("Expected string `{first}`")),
52            [first, second] => f.write_fmt(format_args!("Expected `{first}` or `{second}`")),
53            [first, rest @ .., last] => {
54                f.write_fmt(format_args!("Expected one of `{first}`"))?;
55                for it in rest {
56                    f.write_fmt(format_args!(", `{it}`"))?
57                }
58                f.write_fmt(format_args!(", or `{last}`"))
59            }
60        }
61    }
62}
63
64impl fmt::Debug for ParseError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.debug_tuple("ParseError")
67            .field(&DebugWithDisplay(self))
68            .finish()
69    }
70}
71
72impl core::error::Error for ParseError {}
73
74struct DebugWithDisplay<T>(T);
75impl<T: fmt::Display> fmt::Debug for DebugWithDisplay<T> {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        self.0.fmt(f)
78    }
79}
80
81/// Give the passed in enum a [`FromStr`](core::str::FromStr) and [`Display`](core::fmt::Display)
82/// implementation.
83///
84/// ```
85/// strum_lite::strum! {
86///     #[derive(Default)]
87///     pub enum Casing {
88///         Kebab = "kebab-case" | "kebab",
89///         #[default]
90///         ScreamingSnake = "SCREAMING_SNAKE",
91///     }
92/// }
93///
94/// let derives_are_passed_through = Casing::default();
95/// let implements_display = Casing::Kebab.to_string();
96/// let implements_from_str = "kebab".parse::<Casing>().unwrap();
97/// ```
98///
99/// See [crate documentation](mod@self) for more.
100#[macro_export]
101macro_rules! strum {
102    (
103        $(#[$enum_meta:meta])*
104        $enum_vis:vis enum $enum_name:ident {
105            $(
106                $(#[$variant_meta:meta])*
107                $variant_name:ident = $string:literal $(| $alias:literal)* $(= $discriminant:expr)?
108            ),* $(,)?
109        }
110    ) => {
111        $(#[$enum_meta])*
112        $enum_vis enum $enum_name {
113            $(
114                $(#[$variant_meta])*
115                #[doc = $crate::core::concat!(" String representation: `", $string, "`")]
116                $variant_name $(= $discriminant)?,
117            )*
118        }
119        const _: () = {
120            use $crate::core;
121            impl core::fmt::Display for $enum_name {
122                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
123                    fn as_str(e: &$enum_name) -> &core::primitive::str {
124                        match *e {
125                            $($enum_name::$variant_name => $string),*
126                        }
127                    }
128                    core::fmt::Formatter::write_str(f, as_str(self))
129                }
130            }
131            impl core::str::FromStr for $enum_name {
132                type Err = $crate::ParseError;
133                fn from_str(s: &core::primitive::str) -> core::result::Result<Self, $crate::ParseError> {
134                    const ALL: &[&core::primitive::str] = &[$($string),*];
135                    match s {
136                        $(
137                            $string $(| $alias )* => core::result::Result::Ok(Self::$variant_name),
138                        )*
139                        _ => core::result::Result::Err($crate::ParseError(&ALL))
140                    }
141                }
142            }
143        };
144    };
145}
146
147/// Like [`strum!`], but also declares a custom zero-sized error type,
148/// rather than using the pointer-wide [`ParseError`].
149///
150/// The error type will implement [`Debug`](core::fmt::Debug),
151/// [`Display`](core::fmt::Debug), and [`Error`](core::error::Error),
152/// as well as custom derives.
153///
154/// ```
155/// strum_lite::with_error! {
156///     #[derive(Default)]
157///     pub enum Casing {
158///         Kebab = "kebab-case" | "kebab",
159///         #[default]
160///         ScreamingSnake = "SCREAMING_SNAKE",
161///     }
162///     throws #[derive(Default)] ParseCasingError;
163/// }
164/// ```
165///
166/// See [crate documentation](mod@self) for more.
167///
168/// You are encouraged to copy-paste this macro into your own code.
169#[macro_export]
170macro_rules! with_error {
171    (
172        $(#[$enum_meta:meta])*
173        $enum_vis:vis enum $enum_name:ident {
174            $(
175                $(#[$variant_meta:meta])*
176                $variant_name:ident = $string:literal $(| $alias:literal)* $(= $discriminant:expr)?
177            ),* $(,)?
178        }
179        throws
180        $(#[$error_meta:meta])*
181        $error_name:ident $(;)?
182    ) => {
183        $(#[$enum_meta])*
184        $enum_vis enum $enum_name {
185            $(
186                $(#[$variant_meta])*
187                #[doc = $crate::core::concat!(" String representation: `", $string, "`")]
188                $variant_name $(= $discriminant)?,
189            )*
190        }
191
192        const _: () = {
193            use $crate::core;
194            impl core::fmt::Display for $enum_name {
195                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
196                    fn as_str(e: &$enum_name) -> &core::primitive::str {
197                        match *e {
198                            $($enum_name::$variant_name => $string),*
199                        }
200                    }
201                    core::fmt::Formatter::write_str(f, as_str(self))
202                }
203            }
204
205            impl core::str::FromStr for $enum_name {
206                type Err = $error_name;
207                fn from_str(s: &core::primitive::str) -> core::result::Result<Self, $error_name> {
208                    match s {
209                        $(
210                            $string $(| $alias )* => core::result::Result::Ok(Self::$variant_name),
211                        )*
212                        _ => core::result::Result::Err($error_name)
213                    }
214                }
215            }
216        };
217
218        $(#[$error_meta])*
219        #[doc = $crate::core::concat!(" Error returned when parsing [`", $crate::core::stringify!($enum_name), "`] from a string.")]
220        $enum_vis struct $error_name;
221
222        const _: () = {
223            use $crate::core;
224            impl core::fmt::Display for $error_name {
225                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
226                    let all: &[&core::primitive::str] = &[
227                        $($string),*
228                    ];
229                    let write_fmt = core::fmt::Formatter::write_fmt;
230                    match all {
231                        [] => core::fmt::Formatter::write_str(f, "Uninhabited type is impossible to parse"),
232                        [first] => write_fmt(f, core::format_args!("Expected string `{}`", first)),
233                        [first, second] => write_fmt(f, core::format_args!("Expected `{}` or `{}`", first, second)),
234                        [first, rest @ .., last] => {
235                            write_fmt(f, core::format_args!("Expected one of `{}`", first))?;
236                            for it in rest {
237                                write_fmt(f, core::format_args!(", `{}`", it))?
238                            }
239                            write_fmt(f, core::format_args!(", or `{}`", last))
240                        }
241                    }
242                }
243            }
244            impl core::fmt::Debug for $error_name {
245                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
246                    use core::fmt;
247                    struct DebugWithDisplay<T>(T);
248                    impl<T: fmt::Display> fmt::Debug for DebugWithDisplay<T> {
249                        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250                            self.0.fmt(f)
251                        }
252                    }
253                    let mut f = fmt::Formatter::debug_tuple(f, core::stringify!($error_name));
254                    fmt::DebugTuple::field(&mut f, &DebugWithDisplay(self));
255                    fmt::DebugTuple::finish(&mut f)
256                }
257            }
258            impl core::error::Error for $error_name {}
259        };
260    };
261}