Skip to main content

option_like/
lib.rs

1//! Create your own enum type that behaves like Rust's `Option` but with custom names.
2//!
3//! ## Example
4//!
5//! ```
6//! use option_like::option_like;
7//!
8//! option_like!(
9//!     #[derive(Debug, PartialEq)]
10//!     pub enum Cached<T> {
11//!         Miss,
12//!         Hit(T),
13//!     }
14//!
15//!     is_none => is_miss
16//!     is_some => is_hit
17//! );
18//!
19//! // Create instances
20//! let c1 = Cached::<u32>::Hit(42);
21//! let c2 = Cached::<u32>::Miss;
22//!
23//! // Boolean tests
24//! assert!(c1.is_hit());
25//! assert!(c2.is_miss());
26//!
27//! // Convert to Option
28//! assert_eq!(Option::<u32>::from(c1), Some(42));
29//! assert_eq!(Option::<u32>::from(c2), None);
30//!
31//! // Convert from Option
32//! assert_eq!(Cached::<u32>::from(Some(42)), Cached::Hit(42));
33//! assert_eq!(Cached::<u32>::from(None), Cached::Miss);
34//! ```
35
36#![no_std]
37#![deny(clippy::arithmetic_side_effects)]
38#![cfg_attr(not(test), deny(unused_crate_dependencies))]
39
40/// Creates the inherent implementation block for an option-like enum.
41#[macro_export]
42macro_rules! option_like_impl {
43    (
44        $name:ident,
45        $none:ident,
46        $some:ident,
47        $is_none:ident,
48        $is_some:ident $(,)?
49    ) => {
50        impl<T> $name<T> {
51            pub fn $is_none(&self) -> bool {
52                match self {
53                    Self::$none => true,
54                    Self::$some(_) => false,
55                }
56            }
57
58            pub fn $is_some(&self) -> bool {
59                match self {
60                    Self::$none => false,
61                    Self::$some(_) => true,
62                }
63            }
64
65            #[inline]
66            pub fn map<U, F>(self, f: F) -> $name<U>
67            where
68                F: FnOnce(T) -> U,
69            {
70                match self {
71                    Self::$none => $name::$none,
72                    Self::$some(x) => $name::$some(f(x)),
73                }
74            }
75
76            #[inline(always)]
77            #[track_caller]
78            pub fn unwrap(self) -> T {
79                match self {
80                    Self::$none => Self::unwrap_failed(),
81                    Self::$some(val) => val,
82                }
83            }
84
85            #[inline]
86            pub fn unwrap_or_default(self) -> T
87            where
88                T: Default,
89            {
90                match self {
91                    Self::$none => T::default(),
92                    Self::$some(x) => x,
93                }
94            }
95
96            #[inline]
97            #[track_caller]
98            pub fn unwrap_or_else<F>(self, f: F) -> T
99            where
100                F: FnOnce() -> T,
101            {
102                match self {
103                    Self::$none => f(),
104                    Self::$some(x) => x,
105                }
106            }
107
108            #[inline]
109            #[track_caller]
110            pub fn expect(self, msg: &str) -> T {
111                match self {
112                    Self::$none => Self::expect_failed(msg),
113                    Self::$some(val) => val,
114                }
115            }
116
117            #[cold]
118            #[track_caller]
119            const fn unwrap_failed() -> ! {
120                panic!(stringify!("called `", $name, "::unwrap()` on a `", $none, "` value"))
121            }
122
123            #[cold]
124            #[track_caller]
125            const fn expect_failed(msg: &str) -> ! {
126                panic!("{}", msg)
127            }
128        }
129    };
130}
131
132/// Creates the `From<Option<T>>` and `Into<Option<T>>` conversions for an option-like enum.
133#[macro_export]
134macro_rules! option_like_from_into_option {
135    (
136        $name:ident,
137        $none:ident,
138        $some:ident $(,)?
139    ) => {
140        impl<T> From<Option<T>> for $name<T> {
141            fn from(value: Option<T>) -> Self {
142                match value {
143                    None => Self::$none,
144                    Some(inner) => Self::$some(inner),
145                }
146            }
147        }
148
149        impl<T> From<$name<T>> for Option<T> {
150            fn from(value: $name<T>) -> Option<T> {
151                match value {
152                    $name::$none => None,
153                    $name::$some(inner) => Some(inner),
154                }
155            }
156        }
157    };
158}
159
160/// Creates a new enum type that behaves like Rust's `Option<T>` but with custom names.
161///
162/// This macro allows you to create your own Option-like enum with customized names for the variants
163/// and boolean test methods, while providing automatic conversions to and from the standard Option type.
164///
165/// # Parameters
166///
167/// - `$(#[$meta:meta])*`: Optional attributes to apply to the enum (e.g., `#[derive(...)]`)
168/// - `$vis`: Visibility of the enum (e.g., `pub`)
169/// - `$name`: Name of the enum (e.g., `Cached`)
170/// - `$none`: Name of the empty variant (e.g., `Miss`)
171/// - `$some`: Name of the variant that holds a value (e.g., `Hit`)
172/// - `is_none => $is_none`: Name of the method that checks if the enum is empty (e.g., `is_miss`)
173/// - `is_some => $is_some`: Name of the method that checks if the enum holds a value (e.g., `is_hit`)
174#[macro_export]
175macro_rules! option_like {
176    (
177        $(#[$meta:meta])*
178        $vis:vis enum $name:ident<T> {
179            $(#[$none_meta:meta])*
180            $none:ident,
181            $(#[$some_meta:meta])*
182            $some:ident(T),
183        }
184
185        is_none => $is_none:ident
186        is_some => $is_some:ident
187    ) => {
188        $(#[$meta])*
189        $vis enum $name<T> {
190            $(#[$none_meta])*
191            $none,
192            $(#[$some_meta])*
193            $some(T),
194        }
195
196        $crate::option_like_impl!(
197            $name,
198            $none,
199            $some,
200            $is_none,
201            $is_some,
202        );
203
204        $crate::option_like_from_into_option!(
205            $name,
206            $none,
207            $some,
208        );
209    };
210}
211
212#[cfg(test)]
213mod tests {
214    option_like!(
215        #[derive(Ord, PartialOrd, Eq, PartialEq, Default, Clone, Debug)]
216        enum Cached<T> {
217            #[default]
218            Miss,
219            Hit(T),
220        }
221
222        is_none => is_miss
223        is_some => is_hit
224    );
225
226    use Cached::*;
227
228    fn hit() -> Cached<bool> {
229        Hit(true)
230    }
231
232    fn miss() -> Cached<bool> {
233        Miss
234    }
235
236    #[test]
237    fn test_boolean_methods() {
238        assert!(hit().is_hit());
239        assert!(miss().is_miss());
240    }
241
242    #[test]
243    fn test_from() {
244        assert_eq!(Option::<bool>::from(hit()), Some(true));
245        assert_eq!(Option::<bool>::from(miss()), None);
246        assert_eq!(Cached::<bool>::from(Some(true)), Hit(true));
247        assert_eq!(Cached::<bool>::from(None), Miss);
248    }
249
250    #[test]
251    fn test_map() {
252        assert_eq!(hit().map(|t| !t), Hit(false));
253        assert_eq!(miss().map(|t| !t), Miss);
254    }
255
256    #[test]
257    fn test_unwrap_or_default() {
258        assert!(hit().unwrap_or_default());
259        assert!(!miss().unwrap_or_default());
260    }
261
262    #[test]
263    fn test_unwrap_or_else() {
264        assert!(hit().unwrap_or_else(|| false));
265        assert!(miss().unwrap_or_else(|| true));
266    }
267
268    #[test]
269    fn test_unwrap_no_panic() {
270        assert!(hit().unwrap());
271    }
272
273    #[test]
274    #[should_panic]
275    fn test_unwrap_panic() {
276        miss().unwrap();
277    }
278
279    #[test]
280    fn test_expect_no_panic() {
281        assert!(hit().expect("should not panic"));
282    }
283
284    #[test]
285    #[should_panic]
286    fn test_expect_panic() {
287        miss().expect("should panic");
288    }
289}