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