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}