Skip to main content

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//! - `#![no_std]`.
18//! - The generated [`FromStr::Err`](core::str::FromStr) provides a helpful error message.
19//! - You may ask for a `const` slice of all the variants.
20//! - You may ask for a custom zero-sized error type rather than using this crate's [`ParseError`].
21
22#![no_std]
23
24use core::fmt;
25
26/// Give the passed-in enum a [`FromStr`](core::str::FromStr) and [`Display`](core::fmt::Display)
27/// implementation.
28///
29/// ```
30/// strum_lite::strum! {
31///     #[derive(Default)]
32///     pub enum Casing {
33///         Kebab = "kebab-case" | "kebab" = 100,
34///         #[default]
35///         ScreamingSnake = "SCREAMING_SNAKE",
36///     }
37///     pub const ALL_VARIANTS; // optional
38///     throws #[derive(Clone)] ParseCasingError; // optional
39/// }
40///
41/// let derives_are_passed_through = Casing::default();
42/// let implements_display = Casing::Kebab.to_string();
43/// let implements_from_str = "kebab".parse::<Casing>().unwrap();
44///
45/// assert_eq!(Casing::Kebab as i32, 100);     // discriminants are passed through
46/// assert_eq!(Casing::ALL_VARIANTS.len(), 2); // generated constant
47/// ```
48#[macro_export]
49macro_rules! strum {
50    // Entry point.
51    (
52        $(#[$enum_meta:meta])*
53        $enum_vis:vis enum $enum_name:ident {
54            $(
55                $(#[$variant_meta:meta])*
56                $variant_name:ident = $string:literal $(| $alias:literal)* $(= $discriminant:expr)?
57            ),* $(,)?
58        }
59        $($rest:tt)*
60    ) => {
61        $crate::__strum! {@tail
62            {
63                [$(#[$enum_meta])*]
64                [$enum_vis]
65                $enum_name
66                [$([$(#[$variant_meta])*] $variant_name [$string $(| $alias)*] [$($discriminant)?])*]
67                [$($string)*]
68            }
69            $($rest)*
70        }
71    };
72
73}
74
75#[macro_export]
76#[doc(hidden)]
77macro_rules! __strum {
78    // Dispatch on the optional trailing clauses.
79    (@tail
80        { $metas:tt $vis:tt $enum_name:ident $variants:tt [$($string:literal)*] }
81    ) => {
82        $crate::__strum! {@define $metas $vis $enum_name $variants []
83            [$crate::ParseError]
84            [$crate::ParseError({
85                const ALL: &'static [&'static ::core::primitive::str] = &[$($string),*];
86                &ALL
87            })]
88        }
89    };
90    (@tail
91        { $metas:tt $vis:tt $enum_name:ident $variants:tt [$($string:literal)*] }
92        $(#[$const_meta:meta])* $const_vis:vis const $const_name:ident;
93    ) => {
94        $crate::__strum! {@define $metas $vis $enum_name $variants
95            [[$(#[$const_meta])*] $const_vis const $const_name]
96            [$crate::ParseError]
97            [$crate::ParseError({
98                const ALL: &'static [&'static ::core::primitive::str] = &[$($string),*];
99                &ALL
100            })]
101        }
102    };
103    (@tail
104        { $metas:tt $vis:tt $enum_name:ident $variants:tt [$($string:literal)*] }
105        throws $(#[$error_meta:meta])* $error_name:ident $(;)?
106    ) => {
107        $crate::__strum! {@define $metas $vis $enum_name $variants []
108            [$error_name]
109            [$error_name]
110        }
111        $crate::__strum! {@error [$(#[$error_meta])*] $vis $enum_name $error_name [$($string)*]}
112    };
113    (@tail
114        { $metas:tt $vis:tt $enum_name:ident $variants:tt [$($string:literal)*] }
115        $(#[$const_meta:meta])* $const_vis:vis const $const_name:ident;
116        throws $(#[$error_meta:meta])* $error_name:ident $(;)?
117    ) => {
118        $crate::__strum! {@define $metas $vis $enum_name $variants
119            [[$(#[$const_meta])*] $const_vis const $const_name]
120            [$error_name]
121            [$error_name]
122        }
123        $crate::__strum! {@error [$(#[$error_meta])*] $vis $enum_name $error_name [$($string)*]}
124    };
125    // The enum itself, its optional const of variants, and its impls.
126    (@define
127        [$(#[$enum_meta:meta])*]
128        [$enum_vis:vis]
129        $enum_name:ident
130        [$(
131            [$(#[$variant_meta:meta])*]
132            $variant_name:ident
133            [$string:literal $(| $alias:literal)*]
134            [$($discriminant:expr)?]
135        )*]
136        $konst:tt
137        [$error_ty:ty]
138        [$error_new:expr]
139    ) => {
140        $(#[$enum_meta])*
141        $enum_vis enum $enum_name {
142            $(
143                $(#[$variant_meta])*
144                #[doc = ::core::concat!(" String representation: `", $string, "`")]
145                $variant_name $(= $discriminant)?,
146            )*
147        }
148        $crate::__strum! {@konst $enum_name $konst [$($variant_name)*]}
149        const _: () = {
150            use ::core;
151            impl core::fmt::Display for $enum_name {
152                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153                    fn as_str(e: &$enum_name) -> &core::primitive::str {
154                        match *e {
155                            $($enum_name::$variant_name => $string),*
156                        }
157                    }
158                    core::fmt::Formatter::write_str(f, as_str(self))
159                }
160            }
161            impl core::str::FromStr for $enum_name {
162                type Err = $error_ty;
163                fn from_str(s: &core::primitive::str) -> core::result::Result<Self, Self::Err> {
164                    match s {
165                        $(
166                            $string $(| $alias )* => core::result::Result::Ok(Self::$variant_name),
167                        )*
168                        _ => core::result::Result::Err($error_new)
169                    }
170                }
171            }
172        };
173    };
174    (@konst $enum_name:ident [] $variant_names:tt) => {};
175    (@konst $enum_name:ident [[] $vis:vis const $konst:ident] $variant_names:tt) => {
176        $crate::__strum! {@konst $enum_name
177            [[#[doc = " Every variant of this enum, in declaration order."]] $vis const $konst]
178            $variant_names
179        }
180    };
181    (@konst $enum_name:ident [[$(#[$const_meta:meta])+] $vis:vis const $konst:ident] [$($variant_name:ident)*]) => {
182        impl $enum_name {
183            $(#[$const_meta])+
184            $vis const $konst: [Self; <[Self]>::len(&[$(Self::$variant_name),*])] =
185                [$(Self::$variant_name),*];
186        }
187    };
188    // A zero-sized error struct whose messages list the expected strings.
189    (@error
190        [$(#[$error_meta:meta])*]
191        [$vis:vis]
192        $enum_name:ident
193        $error_name:ident
194        [$($string:literal)*]
195    ) => {
196        $(#[$error_meta])*
197        #[doc = ::core::concat!(" Error returned when parsing [`", ::core::stringify!($enum_name), "`] from a string.")]
198        $vis struct $error_name;
199        const _: () = {
200            use ::core;
201            impl core::fmt::Display for $error_name {
202                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
203                    const ALL: &'static [&'static core::primitive::str] = &[$($string),*];
204                    core::fmt::Display::fmt(&$crate::ParseError(&ALL), f)
205                }
206            }
207            impl core::fmt::Debug for $error_name {
208                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
209                    let mut f = core::fmt::Formatter::debug_tuple(f, core::stringify!($error_name));
210                    core::fmt::DebugTuple::field(&mut f, &core::format_args!("{}", self));
211                    core::fmt::DebugTuple::finish(&mut f)
212                }
213            }
214            impl core::error::Error for $error_name {}
215        };
216    };
217}
218
219/// Pointer-wide shared error type for [`strum!`].
220#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
221pub struct ParseError(#[doc(hidden)] pub &'static &'static [&'static str]);
222
223impl fmt::Display for ParseError {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        match self.0 {
226            [] => f.write_str("Uninhabited type is impossible to parse"),
227            [first] => f.write_fmt(format_args!("Expected string `{first}`")),
228            [first, second] => f.write_fmt(format_args!("Expected `{first}` or `{second}`")),
229            [first, rest @ .., last] => {
230                f.write_fmt(format_args!("Expected one of `{first}`"))?;
231                for it in rest {
232                    f.write_fmt(format_args!(", `{it}`"))?
233                }
234                f.write_fmt(format_args!(", or `{last}`"))
235            }
236        }
237    }
238}
239
240impl fmt::Debug for ParseError {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        f.debug_tuple("ParseError")
243            .field(&format_args!("{self}"))
244            .finish()
245    }
246}
247
248impl core::error::Error for ParseError {}