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//!         Hit(T),
12//!         Miss,
13//!     }
14//!
15//!     is_some => is_hit
16//!     is_none => is_miss
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
38/// Creates a new enum type that behaves like Rust's `Option<T>` but with custom names.
39///
40/// This macro allows you to create your own Option-like enum with customized names for the variants
41/// and boolean test methods, while providing automatic conversions to and from the standard Option type.
42///
43/// # Parameters
44///
45/// - `$(#[$meta:meta])*`: Optional attributes to apply to the enum (e.g., `#[derive(...)]`)
46/// - `$vis`: Visibility of the enum (e.g., `pub`)
47/// - `$name`: Name of the enum (e.g., `Cached`)
48/// - `$some`: Name of the variant that holds a value (e.g., `Hit`)
49/// - `$none`: Name of the empty variant (e.g., `Miss`)
50/// - `is_some => $is_some`: Name of the method that checks if the enum holds a value (e.g., `is_hit`)
51/// - `is_none => $is_none`: Name of the method that checks if the enum is empty (e.g., `is_miss`)
52#[macro_export]
53macro_rules! option_like {
54    (
55        $(#[$meta:meta])*
56        $vis:vis enum $name:ident<T> {
57            $(#[$some_meta:meta])*
58            $some:ident(T),
59            $(#[$none_meta:meta])*
60            $none:ident,
61        }
62
63        is_some => $is_some:ident
64        is_none => $is_none:ident
65    ) => {
66        $(#[$meta])*
67        $vis enum $name<T> {
68            $(#[$some_meta])*
69            $some(T),
70            $(#[$none_meta])*
71            $none,
72        }
73
74        use $name::*;
75
76        impl<T> $name<T> {
77            pub fn $is_some(&self) -> bool {
78                match self {
79                    $some(_) => true,
80                    $none => false,
81                }
82            }
83
84            pub fn $is_none(&self) -> bool {
85                match self {
86                    $some(_) => false,
87                    $none => true,
88                }
89            }
90
91            #[inline]
92            pub fn map<U, F>(self, f: F) -> $name<U>
93            where
94                F: FnOnce(T) -> U,
95            {
96                match self {
97                    $some(x) => $some(f(x)),
98                    $none => $none,
99                }
100            }
101
102            #[inline(always)]
103            #[track_caller]
104            pub fn unwrap(self) -> T {
105                match self {
106                    $some(val) => val,
107                    $none => unwrap_failed(),
108                }
109            }
110
111            #[inline]
112            pub fn unwrap_or_default(self) -> T
113            where
114                T: Default,
115            {
116                match self {
117                    $some(x) => x,
118                    $none => T::default(),
119                }
120            }
121
122            #[inline]
123            #[track_caller]
124            pub fn unwrap_or_else<F>(self, f: F) -> T
125            where
126                F: FnOnce() -> T,
127            {
128                match self {
129                    $some(x) => x,
130                    $none => f(),
131                }
132            }
133
134            #[inline]
135            #[track_caller]
136            pub fn expect(self, msg: &str) -> T {
137                match self {
138                    $some(val) => val,
139                    $none => expect_failed(msg),
140                }
141            }
142        }
143
144        impl<T> From<Option<T>> for $name<T> {
145            fn from(value: Option<T>) -> Self {
146                match value {
147                    Some(inner) => $some(inner),
148                    None => $none
149                }
150            }
151        }
152
153        impl<T> From<$name<T>> for Option<T> {
154            fn from(value: $name<T>) -> Option<T> {
155                match value {
156                    $some(inner) => Some(inner),
157                    $none => None
158                }
159            }
160        }
161
162        #[cold]
163        #[track_caller]
164        const fn unwrap_failed() -> ! {
165            panic!(stringify!("called `", $name, "::unwrap()` on a `", $none, "` value"))
166        }
167
168        #[cold]
169        #[track_caller]
170        const fn expect_failed(msg: &str) -> ! {
171            panic!("{}", msg)
172        }
173    };
174}
175
176#[cfg(test)]
177mod tests {
178    option_like!(
179        #[derive(Ord, PartialOrd, Eq, PartialEq, Default, Clone, Debug)]
180        enum Cached<T> {
181            Hit(T),
182            #[default]
183            Miss,
184        }
185
186        is_some => is_hit
187        is_none => is_miss
188    );
189
190    fn hit() -> Cached<bool> {
191        Hit(true)
192    }
193
194    fn miss() -> Cached<bool> {
195        Miss
196    }
197
198    #[test]
199    fn test_boolean_methods() {
200        assert!(hit().is_hit());
201        assert!(miss().is_miss());
202    }
203
204    #[test]
205    fn test_from() {
206        assert_eq!(Option::<bool>::from(hit()), Some(true));
207        assert_eq!(Option::<bool>::from(miss()), None);
208        assert_eq!(Cached::<bool>::from(Some(true)), Hit(true));
209        assert_eq!(Cached::<bool>::from(None), Miss);
210    }
211
212    #[test]
213    fn test_map() {
214        assert_eq!(hit().map(|t| !t), Hit(false));
215        assert_eq!(miss().map(|t| !t), Miss);
216    }
217
218    #[test]
219    fn test_unwrap_or_default() {
220        assert!(hit().unwrap_or_default());
221        assert!(!miss().unwrap_or_default());
222    }
223
224    #[test]
225    fn test_unwrap_or_else() {
226        assert!(hit().unwrap_or_else(|| false));
227        assert!(miss().unwrap_or_else(|| true));
228    }
229
230    #[test]
231    fn test_unwrap_no_panic() {
232        assert!(hit().unwrap());
233    }
234
235    #[test]
236    #[should_panic]
237    fn test_unwrap_panic() {
238        miss().unwrap();
239    }
240
241    #[test]
242    fn test_expect_no_panic() {
243        assert!(hit().expect("should not panic"));
244    }
245
246    #[test]
247    #[should_panic]
248    fn test_expect_panic() {
249        miss().expect("should panic");
250    }
251}