bijective_enum_map/injective.rs
1/// Map an enum into and from another type (or two types) using `From` and `TryFrom`
2/// (with unit error).
3///
4/// The enum type must be specified, followed by the type to map the enum into (`$into`),
5/// optionally followed by the type to try to map into the enum (`$try_from`).
6/// If `$try_from` is not specified, it is set to `$into`.
7///
8/// The two types must be similar enough that the same value expression (e.g., a numeric or string
9/// literal) works for either; moreover, the expression must also be a valid pattern for a match
10/// arm, so arbitrarily complicated expressions are not permitted.
11/// In practice, the types should usually be the same, but specifying
12/// two different types is useful for converting into `&'static str` and trying to convert
13/// from `&str`, for example. Unintended options like mapping into `Range<u8>` and from `u8`
14/// might be possible, but are not tested here. Note that non-unit variants are supported.
15///
16/// This map is intended to be "injective"; different enum variants should map into different
17/// values, so that they can be mapped back unambiguously. The map may (or may not) also be
18/// "surjective", in which any possible value of the target type is associated with some enum
19/// variant, in which case the `TryFrom` implementation would not be able to fail (but this macro
20/// does not check for surjectivity). You should use `bijective_enum_map` if the mapping is
21/// also surjective.
22///
23/// If the map is not injective, and multiple enum variants map to the same value, then a compiler
24/// warning from `#[warn(unreachable_patterns)]` *should* be printed in most circumstances,
25/// but it could be a silent logic error. In such a case, only the first enum variant
26/// listed will be mapped from the duplicate value. Such a warning should also occur if an enum
27/// variant is repeated.
28///
29/// # Examples
30///
31/// ## Map into and from two other types:
32/// ```
33/// use bijective_enum_map::injective_enum_map;
34/// #[derive(Debug, PartialEq, Eq)]
35/// enum AtMostTwo {
36/// Zero,
37/// One,
38/// Two,
39/// }
40///
41/// injective_enum_map! {
42/// AtMostTwo, u8,
43/// Zero <=> 0,
44/// One <=> 1,
45/// Two <=> 2,
46/// }
47///
48/// injective_enum_map! {
49/// AtMostTwo, &'static str, &str,
50/// Zero <=> "zero",
51/// One <=> "one",
52/// Two <=> "two",
53/// }
54///
55/// assert_eq!(u8::from(AtMostTwo::One), 1_u8);
56/// assert_eq!(AtMostTwo::try_from(2_u8), Ok(AtMostTwo::Two));
57/// assert_eq!(AtMostTwo::try_from(4_u8), Err(()));
58/// // `<&str>::from` would also work
59/// assert_eq!(<&'static str>::from(AtMostTwo::One), "one");
60/// assert_eq!(AtMostTwo::try_from("two"), Ok(AtMostTwo::Two));
61/// assert_eq!(AtMostTwo::try_from("four"), Err(()));
62/// ```
63///
64/// ## Empty enum, mapped into and from different types:
65/// ```
66/// use bijective_enum_map::injective_enum_map;
67/// #[derive(Debug, PartialEq, Eq)]
68/// enum Empty {}
69///
70/// // The trailing comma is always optional
71/// injective_enum_map! { Empty, &'static str, &str }
72///
73/// assert_eq!(Empty::try_from("42"), Err(()))
74/// ```
75///
76/// ## Intentionally violate injectivity:
77/// ```
78/// use bijective_enum_map::injective_enum_map;
79/// #[derive(Debug, PartialEq, Eq)]
80/// enum Version {
81/// V1,
82/// V2Any,
83/// V2,
84/// V2Alternate,
85/// V3,
86/// }
87///
88/// // Slightly extreme option to silence warnings; you should probably run your code without
89/// // a similar allow attribute in order to check that the warning which occurs is as expected.
90/// // It seems that `#[allow(unreachable_patterns)]` is overridden by
91/// // the `#[warn(unreachable_patterns)]` inside the macro.
92/// #[allow(warnings)]
93/// {
94/// injective_enum_map! {
95/// Version, u8,
96/// // Because injectivity is violated, the order is significant.
97/// // With this order, `2` is mapped to the `V2Any` variant.
98/// V1 <=> 1,
99/// V2Any <=> 2,
100/// V2 <=> 2,
101/// V2Alternate <=> 2,
102/// V3 <=> 3,
103/// }
104/// }
105///
106/// assert_eq!(u8::from(Version::V2), 2);
107/// assert_eq!(u8::from(Version::V3), 3);
108/// assert_eq!(Version::try_from(1_u8), Ok(Version::V1));
109/// assert_eq!(Version::try_from(2_u8), Ok(Version::V2Any));
110/// assert_eq!(Version::try_from(5_u8), Err(()));
111/// ```
112///
113/// ## Map into and from another enum:
114/// ```
115/// use bijective_enum_map::injective_enum_map;
116/// #[derive(Debug, PartialEq, Eq)]
117/// enum Enum {
118/// One,
119/// Two,
120/// Three,
121/// }
122///
123/// #[derive(Debug, PartialEq, Eq)]
124/// enum Other {
125/// Uno,
126/// Dos,
127/// Tres,
128/// }
129///
130/// // Writing `Other` twice has the same effect as writing it once
131/// injective_enum_map! {
132/// Enum, Other, Other,
133/// One <=> Other::Uno,
134/// Two <=> Other::Dos,
135/// Three <=> Other::Tres,
136/// }
137///
138/// assert_eq!(Other::from(Enum::Three), Other::Tres);
139/// // Note that this conversion cannot fail, but `injective_enum_map` does not know that.
140/// // You could use `bijective_enum_map` instead.
141/// assert_eq!(Enum::try_from(Other::Uno), Ok(Enum::One));
142/// ```
143#[macro_export]
144macro_rules! injective_enum_map {
145 { $enum_ty:ty, $into:ty, $try_from:ty, $($body:tt)* } => {
146 $crate::__impl_from_enum! { $enum_ty, $into, $($body)* }
147 $crate::__impl_enum_try_from! { $enum_ty, $try_from, $($body)* }
148 };
149
150 { $enum_ty:ty, $into:ty, $try_from:ty } => {
151 $crate::__impl_from_enum! { $enum_ty, $into }
152 $crate::__impl_enum_try_from! { $enum_ty, $try_from }
153 };
154
155 { $enum_ty:ty, $both:ty, $($body:tt)* } => {
156 $crate::__impl_from_enum! { $enum_ty, $both, $($body)* }
157 $crate::__impl_enum_try_from! { $enum_ty, $both, $($body)* }
158 };
159
160 { $enum_ty:ty, $both:ty } => {
161 $crate::__impl_from_enum! { $enum_ty, $both }
162 $crate::__impl_enum_try_from! { $enum_ty, $both }
163 };
164}
165
166
167#[cfg(test)]
168mod tests {
169 use crate::injective_enum_map;
170
171 #[test]
172 fn empty_both_specified() {
173 #[derive(Debug, PartialEq, Eq)]
174 enum Empty {}
175
176 injective_enum_map! {Empty, u8, u32}
177
178 assert_eq!(Empty::try_from(2_u32), Err(()));
179 }
180
181 #[test]
182 fn empty_one_specified() {
183 #[derive(Debug, PartialEq, Eq)]
184 enum Empty {}
185
186 injective_enum_map! {Empty, u8}
187
188 assert_eq!(Empty::try_from(2_u8), Err(()));
189 }
190
191 #[test]
192 fn nonempty_both_specified() {
193 #[derive(Debug, PartialEq, Eq)]
194 enum AtMostTwo {
195 Zero,
196 One,
197 Two,
198 }
199
200 injective_enum_map! {
201 AtMostTwo, u8, u32,
202 Zero <=> 0,
203 One <=> 1,
204 Two <=> 2,
205 }
206
207 assert_eq!(u8::from(AtMostTwo::One), 1_u8);
208 assert_eq!(AtMostTwo::try_from(2_u32), Ok(AtMostTwo::Two));
209 assert_eq!(AtMostTwo::try_from(4_u32), Err(()));
210 }
211
212 #[test]
213 fn nonempty_one_specified() {
214 #[derive(Debug, PartialEq, Eq)]
215 enum AtMostTwo {
216 Zero,
217 One,
218 Two,
219 }
220
221 injective_enum_map! {
222 AtMostTwo, u32,
223 Zero <=> 0,
224 One <=> 1,
225 Two <=> 2,
226 }
227
228 assert_eq!(u32::from(AtMostTwo::One), 1_u32);
229 assert_eq!(AtMostTwo::try_from(2_u32), Ok(AtMostTwo::Two));
230 assert_eq!(AtMostTwo::try_from(4_u32), Err(()));
231 }
232
233 #[test]
234 fn nonempty_to_enum_bijective() {
235 #[derive(Debug, PartialEq, Eq)]
236 enum Enum {
237 One,
238 Two,
239 Three,
240 }
241
242 #[derive(Debug, PartialEq, Eq)]
243 enum Other {
244 Uno,
245 Dos,
246 Tres,
247 }
248
249 injective_enum_map! {
250 Enum, Other, Other,
251 One <=> Other::Uno,
252 Two <=> Other::Dos,
253 Three <=> Other::Tres,
254 }
255
256 assert_eq!(Other::from(Enum::Three), Other::Tres);
257 // Note that this conversion cannot fail, but `injective_enum_map` does not know that.
258 assert_eq!(Enum::try_from(Other::Uno), Ok(Enum::One));
259 }
260
261 #[test]
262 fn nonempty_to_enum_injective() {
263 #[derive(Debug, PartialEq, Eq)]
264 enum Enum {
265 One,
266 Two,
267 Three,
268 }
269
270 #[derive(Debug, PartialEq, Eq)]
271 enum Other {
272 Uno,
273 Dos,
274 Tres,
275 Cuatro,
276 }
277
278 injective_enum_map! {
279 Enum, Other, Other,
280 One <=> Other::Uno,
281 Two <=> Other::Dos,
282 Three <=> Other::Tres,
283 }
284
285 assert_eq!(Other::from(Enum::Three), Other::Tres);
286 assert_eq!(Enum::try_from(Other::Uno), Ok(Enum::One));
287 assert_eq!(Enum::try_from(Other::Cuatro), Err(()));
288 }
289
290 #[test]
291 fn enum_to_string() {
292 #[derive(Debug, PartialEq, Eq)]
293 enum Empty {}
294
295 #[derive(Debug, PartialEq, Eq)]
296 enum Nonempty {
297 Something,
298 }
299
300 injective_enum_map! {Empty, &'static str, &str}
301 injective_enum_map! {
302 Nonempty, &'static str, &str,
303 Something <=> "Something",
304 }
305
306 assert_eq!(Empty::try_from("Anything"), Err(()));
307 assert_eq!(Nonempty::try_from("Something"), Ok(Nonempty::Something));
308 assert_eq!(Nonempty::try_from("Nothing"), Err(()));
309 }
310
311 #[test]
312 fn trailing_commas() {
313 enum Empty {}
314 enum Nonempty {
315 Something,
316 }
317
318 injective_enum_map!(Empty, u8, u8);
319 injective_enum_map! { Empty, u16 };
320 injective_enum_map! {
321 Empty, i8, i8,
322 };
323 injective_enum_map! { Empty, i16, };
324
325 injective_enum_map!(Nonempty, u8, u8, Something <=> 0);
326 injective_enum_map! { Nonempty, u16, Something <=> 0};
327 injective_enum_map! {
328 Nonempty, i8, i8, Something <=> 0,
329 };
330 injective_enum_map! { Nonempty, i16, Something <=> 0,};
331 }
332
333 #[test]
334 fn non_unit_variant() {
335 // These would be strings, but don't want to pull in `alloc`.
336 #[derive(Debug, PartialEq)]
337 enum Thing {
338 Player {
339 name: &'static str,
340 hp: u32,
341 strength: f32,
342 },
343 PhysicalObject {
344 name: &'static str,
345 hp: u32,
346 },
347 Spell {
348 name: &'static str,
349 strength: f32,
350 },
351 Marker(&'static str),
352 HardcodedMarker(&'static str, u32),
353 }
354
355 type Stuff = (u8, Option<&'static str>, Option<u32>, Option<f32>);
356
357 injective_enum_map! {
358 Thing, Stuff,
359 Player { name, hp, strength } <=> (0, Some(name), Some(hp), Some(strength)),
360 PhysicalObject { name, hp } <=> (1, Some(name), Some(hp), None),
361 Spell { name, strength } <=> (2, Some(name), None, Some(strength)),
362 Marker(name) <=> (3, Some(name), None, None),
363 HardcodedMarker(name, id) <=> (4, Some(name), Some(id), None),
364 }
365
366 assert_eq!(
367 Stuff::from(Thing::Player { name: "person", hp: 2, strength: 1.5 }),
368 (0, Some("person"), Some(2), Some(1.5)),
369 );
370 assert_eq!(
371 Stuff::from(Thing::Marker("place")),
372 (3, Some("place"), None, None),
373 );
374 assert_eq!(
375 Thing::try_from((1_u8, Some("object"), Some(100_u32), None)),
376 Ok(Thing::PhysicalObject { name: "object", hp: 100 }),
377 );
378 assert_eq!(
379 Thing::try_from((1_u8, Some("object"), Some(100_u32), Some(1e30))),
380 Err(()),
381 );
382 }
383
384 #[test]
385 fn intentionally_non_injective() {
386 #[derive(Debug, PartialEq, Eq)]
387 enum Enum {
388 One,
389 Two,
390 Three,
391 }
392
393 #[derive(Debug, PartialEq, Eq)]
394 enum Other {
395 Uno,
396 Dos,
397 Tres,
398 Cuatro,
399 }
400
401 #[allow(warnings)]
402 {
403 injective_enum_map! {
404 Other, Enum,
405 Uno <=> Enum::One,
406 Dos <=> Enum::Two,
407 Tres <=> Enum::Three,
408 Cuatro <=> Enum::Three,
409 }
410 }
411
412 assert_eq!(Other::try_from(Enum::Three), Ok(Other::Tres));
413 assert_eq!(Enum::from(Other::Uno), Enum::One);
414 assert_eq!(Enum::from(Other::Cuatro), Enum::Three);
415 }
416}
417
418#[cfg(doctest)]
419pub mod compile_fail_tests {
420 /// ```compile_fail,E0004
421 /// use bijective_enum_map::injective_enum_map;
422 /// #[derive(Debug, PartialEq, Eq)]
423 /// enum Nonempty {
424 /// Something,
425 /// }
426 ///
427 /// injective_enum_map! {Nonempty, u8}
428 ///
429 /// assert_eq!(Nonempty::try_from(2_u8), Err(()));
430 /// ```
431 pub fn _nonempty_but_nothing_provided() {}
432
433 /// ```compile_fail,E0004
434 /// use bijective_enum_map::injective_enum_map;
435 /// #[derive(Debug, PartialEq, Eq)]
436 /// enum Nonempty {
437 /// Something,
438 /// SomethingElse,
439 /// }
440 ///
441 /// injective_enum_map! { Nonempty, u8, Something <=> 0 }
442 ///
443 /// assert_eq!(Nonempty::try_from(2_u8), Err(()));
444 /// ```
445 pub fn _nonempty_but_not_enough_provided() {}
446
447 /// ```compile_fail
448 /// #![deny(warnings)]
449 ///
450 /// use bijective_enum_map::injective_enum_map;
451 /// #[derive(Debug, PartialEq, Eq)]
452 /// enum AtMostTwo {
453 /// Zero,
454 /// One,
455 /// Two,
456 /// }
457 ///
458 /// injective_enum_map! {
459 /// AtMostTwo, u8,
460 /// Zero <=> 0,
461 /// One <=> 1,
462 /// Two <=> 0,
463 /// }
464 /// ```
465 pub fn _nonempty_not_injective_warning() {}
466
467 /// ```compile_fail
468 /// #![deny(warnings)]
469 ///
470 /// use bijective_enum_map::injective_enum_map;
471 /// enum AtMostTwo {
472 /// Zero,
473 /// One,
474 /// Two,
475 /// }
476 ///
477 /// enum BoolEnum {
478 /// True,
479 /// False,
480 /// }
481 ///
482 /// injective_enum_map! {
483 /// bool, AtMostTwo,
484 /// False <=> AtMostTwo::Zero,
485 /// True <=> AtMostTwo::One,
486 /// False <=> AtMostTwo::Two,
487 /// }
488 /// ```
489 pub fn _nonempty_not_surjective_warning() {}
490
491 // A warning is printed, but unfortunately, #[deny] doesn't work very well in doctests.
492 // /// ```compile_fail
493 // /// #![deny(unreachable_patterns)]
494 // ///
495 // /// use bijective_enum_map::injective_enum_map;
496 // /// #[derive(Debug, PartialEq, Eq)]
497 // /// enum AtMostTwo {
498 // /// Zero,
499 // /// One,
500 // /// Two,
501 // /// }
502 // ///
503 // /// #[deny(unreachable_patterns)]
504 // /// injective_enum_map! {
505 // /// AtMostTwo, u8,
506 // /// Zero <=> 0,
507 // /// One <=> 1,
508 // /// Two <=> 0,
509 // /// }
510 // /// ```
511 // pub fn _nonempty_not_injective() {}
512
513 /// ```compile_fail
514 /// #![deny(warnings)]
515 ///
516 /// use bijective_enum_map::injective_enum_map;
517 /// #[derive(Debug, PartialEq, Eq)]
518 /// enum AtMostTwo {
519 /// Zero,
520 /// One,
521 /// Two,
522 /// }
523 ///
524 /// enum Other {
525 /// Uno,
526 /// Dos,
527 /// }
528 ///
529 /// injective_enum_map! {
530 /// AtMostTwo, Other,
531 /// Zero <=> Other::Uno,
532 /// One <=> Other::Uno,
533 /// Two <=> Other::Dos,
534 /// }
535 ///
536 /// let _ = AtMostTwo::try_from(Other::Uno);
537 /// ```
538 pub fn _nonempty_to_enum_not_injective_warning() {}
539
540 // Surprisingly, this compiles. It defaults to `&'static str`, as far as I can tell.
541 // /// ```compile_fail
542 // /// use bijective_enum_map::injective_enum_map;
543 // /// enum Nonempty {
544 // /// Something,
545 // /// }
546 // ///
547 // /// injective_enum_map! {
548 // /// Nonempty, &str,
549 // /// Something <=> "Something",
550 // /// }
551 // ///
552 // /// let _ = <&str>::from(Nonempty::Something);
553 // /// ```
554 // pub fn _enum_to_string_bad_lifetimes() {}
555
556 // Doesn't seem to have a compiler error number
557 /// ```compile_fail
558 /// use bijective_enum_map::injective_enum_map;
559 /// enum Nonempty {
560 /// Something,
561 /// }
562 ///
563 /// injective_enum_map! {
564 /// Nonempty, u8
565 /// Something <=> 0
566 /// }
567 /// ```
568 pub fn _missing_comma() {}
569}