Repository: Peternator7/strum
Pull request #185 (2021-10-16 17:07:06)

EnumIndex implementation
@Peternator7 This is an initial pass at an implementation of an index function that is `const`. The following lists some of the outstanding items. In addition, I'm really not sure I captured all of the desired behavior in that macro implementation. 

- [ ] Is this the best name for the macro name or the method name (currently its `index` and `const_index`)
  - [ ] Should there by `try_` variants of the functions so that the non-`try` versions panic on unknown value similar to `Index` trait returns an `Option`?
- [x] Add documentation
- [x] The argument to the get/index function should be the same type as the `repr` (ie if the enum is `repr(u8)`)
- [ ] Add ability to rename get/index function?
- Discriminant
  - [x] Add error messages related to discriminant code
  - [ ] Make sure the implementation is correct beyond the basic tests that are currently written.

drewkett: I went ahead and updated some things.
- Added documentation
- Renamed macro to `EnumIndex`
- The macro now generates up to two functions. `index` in all cases (that don't generate an error due to the existence of lifetimes). And `const_index` when all the variants are unit variants (`Default::default` is not `const`). 
- Renamed macro to `EnumIndex`
drewkett: I got a version working to handle custom `repr` working. The code needs cleaning up in general but just wanted to get something committed that was working, since it ended up being a bit more complicated that I thought it would be
drewkett: Think I fixed everything mentioned. I added `rustversion` as well. The only weird thing was the I made the `const_index` doc tests conditional on the rust version. Not sure if you have any other ideas for that. 

Patch:
diff --git a/strum/src/lib.rs b/strum/src/lib.rs
index 9f7b9de..fe12b92 100644
--- a/strum/src/lib.rs
+++ b/strum/src/lib.rs
@@ -217,6 +217,7 @@ DocumentMacroRexports! {
     EnumProperty,
     EnumString,
     EnumVariantNames,
+    FromRepr,
     IntoStaticStr,
     ToString
 }
diff --git a/strum_macros/Cargo.toml b/strum_macros/Cargo.toml
index ee84210..fcf643c 100644
--- a/strum_macros/Cargo.toml
+++ b/strum_macros/Cargo.toml
@@ -22,6 +22,7 @@ name = "strum_macros"
 heck = "0.3"
 proc-macro2 = "1.0"
 quote = "1.0"
+rustversion = "1.0"
 syn = { version = "1.0", features = ["parsing", "extra-traits"] }
 
 [dev-dependencies]
diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs
index 38eb0b3..1ccaf5b 100644
--- a/strum_macros/src/lib.rs
+++ b/strum_macros/src/lib.rs
@@ -30,7 +30,8 @@ fn debug_print_generated(ast: &DeriveInput, toks: &TokenStream) {
 
 /// Converts strings to enum variants based on their name.
 ///
-/// auto-derives `std::str::FromStr` on the enum. Each variant of the enum will match on it's own name.
+/// auto-derives `std::str::FromStr` on the enum (for Rust 1.34 and above, std::convert::TryFrom<&str>
+/// will be derived as well). Each variant of the enum will match on it's own name.
 /// This can be overridden using `serialize="DifferentName"` or `to_string="DifferentName"`
 /// on the attribute as shown below.
 /// Multiple deserializations can be added to the same variant. If the variant contains additional data,
@@ -374,6 +375,86 @@ pub fn enum_iter(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     toks.into()
 }
 
+/// Add a function to enum that allows accessing variants by its discriminant
+///
+/// This macro adds a standalone function to obtain an enum variant by its discriminant. The macro adds
+/// `from_repr(discriminant: usize) -> Option<YourEnum>` as a standalone function on the enum. For
+/// variants with additional data, the returned variant will use the `Default` trait to fill the
+/// data. The discriminant follows the same rules as `rustc`. The first discriminant is zero and each
+/// successive variant has a discriminant of one greater than the previous variant, expect where an
+/// explicit discriminant is specified. The type of the discriminant will match the `repr` type if
+/// it is specifed.
+///
+/// When the macro is applied using rustc >= 1.46 and when there is no additional data on any of
+/// the variants, the `from_repr` function is marked `const`. rustc >= 1.46 is required
+/// to allow `match` statements in `const fn`. The no additional data requirement is due to the
+/// inability to use `Default::default()` in a `const fn`.
+///
+/// You cannot derive `FromRepr` on any type with a lifetime bound (`<'a>`) because the function would surely
+/// create [unbounded lifetimes](https://doc.rust-lang.org/nightly/nomicon/unbounded-lifetimes.html).
+///
+/// ```
+///
+/// use strum_macros::FromRepr;
+///
+/// #[derive(FromRepr, Debug, PartialEq)]
+/// enum Color {
+///     Red,
+///     Green { range: usize },
+///     Blue(usize),
+///     Yellow,
+/// }
+///
+/// assert_eq!(Some(Color::Red), Color::from_repr(0));
+/// assert_eq!(Some(Color::Green {range: 0}), Color::from_repr(1));
+/// assert_eq!(Some(Color::Blue(0)), Color::from_repr(2));
+/// assert_eq!(Some(Color::Yellow), Color::from_repr(3));
+/// assert_eq!(None, Color::from_repr(4));
+///
+/// // Custom discriminant tests
+/// #[derive(FromRepr, Debug, PartialEq)]
+/// #[repr(u8)]
+/// enum Vehicle {
+///     Car = 1,
+///     Truck = 3,
+/// }
+///
+/// assert_eq!(None, Vehicle::from_repr(0));
+/// ```
+#[rustversion::attr(since(1.46),doc="
+`const` tests (only works in rust >= 1.46)
+```
+use strum_macros::FromRepr;
+
+#[derive(FromRepr, Debug, PartialEq)]
+#[repr(u8)]
+enum Number {
+    One = 1,
+    Three = 3,
+}
+
+// This test confirms that the function works in a `const` context
+const fn number_from_repr(d: u8) -> Option<Number> {
+    Number::from_repr(d)
+}
+assert_eq!(None, number_from_repr(0));
+assert_eq!(Some(Number::One), number_from_repr(1));
+assert_eq!(None, number_from_repr(2));
+assert_eq!(Some(Number::Three), number_from_repr(3));
+assert_eq!(None, number_from_repr(4));
+```
+")]
+
+#[proc_macro_derive(FromRepr, attributes(strum))]
+pub fn from_repr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    let ast = syn::parse_macro_input!(input as DeriveInput);
+
+    let toks = macros::from_repr::from_repr_inner(&ast)
+        .unwrap_or_else(|err| err.to_compile_error());
+    debug_print_generated(&ast, &toks);
+    toks.into()
+}
+
 /// Add a verbose message to an enum variant.
 ///
 /// Encode strings into the enum itself. The `strum_macros::EmumMessage` macro implements the `strum::EnumMessage` trait.
diff --git a/strum_macros/src/macros/from_repr.rs b/strum_macros/src/macros/from_repr.rs
new file mode 100644
index 0000000..3ea6d1b
--- /dev/null
+++ b/strum_macros/src/macros/from_repr.rs
@@ -0,0 +1,140 @@
+use proc_macro2::{Span, TokenStream};
+use quote::{format_ident, quote};
+use syn::{Data, DeriveInput, PathArguments, Type, TypeParen};
+
+use crate::helpers::{non_enum_error, HasStrumVariantProperties};
+
+pub fn from_repr_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
+    let name = &ast.ident;
+    let gen = &ast.generics;
+    let (impl_generics, ty_generics, where_clause) = gen.split_for_impl();
+    let vis = &ast.vis;
+    let attrs = &ast.attrs;
+
+    let mut discriminant_type: Type = syn::parse("usize".parse().unwrap()).unwrap();
+    for attr in attrs {
+        let path = &attr.path;
+        let tokens = &attr.tokens;
+        if path.leading_colon.is_some() {
+            continue;
+        }
+        if path.segments.len() != 1 {
+            continue;
+        }
+        let segment = path.segments.first().unwrap();
+        if segment.ident != "repr" {
+            continue;
+        }
+        if segment.arguments != PathArguments::None {
+            continue;
+        }
+        let typ_paren = match syn::parse2::<Type>(tokens.clone()) {
+            Ok(Type::Paren(TypeParen { elem, .. })) => *elem,
+            _ => continue,
+        };
+        let inner_path = match &typ_paren {
+            Type::Path(t) => t,
+            _ => continue,
+        };
+        if let Some(seg) = inner_path.path.segments.last() {
+            for t in &[
+                "u8", "u16", "u32", "u64", "usize", "i8", "i16", "i32", "i64", "isize",
+            ] {
+                if seg.ident == t {
+                    discriminant_type = typ_paren;
+                    break;
+                }
+            }
+        }
+    }
+
+    if gen.lifetimes().count() > 0 {
+        return Err(syn::Error::new(
+            Span::call_site(),
+            "This macro doesn't support enums with lifetimes. \
+             The resulting enums would be unbounded.",
+        ));
+    }
+
+    let variants = match &ast.data {
+        Data::Enum(v) => &v.variants,
+        _ => return Err(non_enum_error()),
+    };
+
+    let mut arms = Vec::new();
+    let mut constant_defs = Vec::new();
+    let mut has_additional_data = false;
+    let mut prev_const_var_ident = None;
+    for variant in variants {
+        use syn::Fields::*;
+
+        if variant.get_variant_properties()?.disabled.is_some() {
+            continue;
+        }
+
+        let ident = &variant.ident;
+        let params = match &variant.fields {
+            Unit => quote! {},
+            Unnamed(fields) => {
+                has_additional_data = true;
+                let defaults = ::std::iter::repeat(quote!(::core::default::Default::default()))
+                    .take(fields.unnamed.len());
+                quote! { (#(#defaults),*) }
+            }
+            Named(fields) => {
+                has_additional_data = true;
+                let fields = fields
+                    .named
+                    .iter()
+                    .map(|field| field.ident.as_ref().unwrap());
+                quote! { {#(#fields: ::core::default::Default::default()),*} }
+            }
+        };
+
+        use heck::ShoutySnakeCase;
+        let const_var_str = format!("{}_DISCRIMINANT", variant.ident).to_shouty_snake_case();
+        let const_var_ident = format_ident!("{}", const_var_str);
+
+        let const_val_expr = match &variant.discriminant {
+            Some((_, expr)) => quote! { #expr },
+            None => match &prev_const_var_ident {
+                Some(prev) => quote! { #prev + 1 },
+                None => quote! { 0 },
+            },
+        };
+
+        constant_defs
+            .push(quote! {const #const_var_ident: #discriminant_type = #const_val_expr;});
+        arms.push(quote! {v if v == #const_var_ident => ::core::option::Option::Some(#name::#ident #params)});
+
+        prev_const_var_ident = Some(const_var_ident);
+    }
+
+    arms.push(quote! { _ => ::core::option::Option::None });
+
+    let const_if_possible = if has_additional_data {
+        quote! {}
+    } else {
+        #[rustversion::before(1.46)]
+        fn filter_by_rust_version(s: TokenStream) -> TokenStream {
+            quote! {}
+        }
+
+        #[rustversion::since(1.46)]
+        fn filter_by_rust_version(s: TokenStream) -> TokenStream {
+            s
+        }
+        filter_by_rust_version(quote! { const })
+    };
+
+    Ok(quote! {
+        impl #impl_generics #name #ty_generics #where_clause {
+            #vis #const_if_possible fn from_repr(discriminant: #discriminant_type) -> Option<#name #ty_generics> {
+                #(#constant_defs)*
+                match discriminant {
+                    #(#arms),*
+                }
+            }
+        }
+    })
+}
diff --git a/strum_macros/src/macros/mod.rs b/strum_macros/src/macros/mod.rs
index 9b707fb..b412969 100644
--- a/strum_macros/src/macros/mod.rs
+++ b/strum_macros/src/macros/mod.rs
@@ -4,6 +4,7 @@ pub mod enum_iter;
 pub mod enum_messages;
 pub mod enum_properties;
 pub mod enum_variant_names;
+pub mod from_repr;
 
 mod strings;
 
diff --git a/strum_macros/src/macros/strings/from_string.rs b/strum_macros/src/macros/strings/from_string.rs
index 2977fd1..879dd87 100644
--- a/strum_macros/src/macros/strings/from_string.rs
+++ b/strum_macros/src/macros/strings/from_string.rs
@@ -19,7 +19,7 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
 
     let mut default_kw = None;
     let mut default =
-        quote! { _ => ::std::result::Result::Err(#strum_module_path::ParseError::VariantNotFound) };
+        quote! { _ => ::core::result::Result::Err(#strum_module_path::ParseError::VariantNotFound) };
     let mut arms = Vec::new();
     for variant in variants {
         let ident = &variant.ident;
@@ -46,7 +46,7 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
 
             default_kw = Some(kw);
             default = quote! {
-                default => ::std::result::Result::Ok(#name::#ident(default.into()))
+                default => ::core::result::Result::Ok(#name::#ident(default.into()))
             };
             continue;
         }
@@ -82,20 +82,63 @@ pub fn from_string_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
             }
         };
 
-        arms.push(quote! { #(#attrs => ::std::result::Result::Ok(#name::#ident #params)),* });
+        arms.push(quote! { #(#attrs => ::core::result::Result::Ok(#name::#ident #params)),* });
     }
 
     arms.push(default);
 
-    Ok(quote! {
+    let from_str = quote! {
         #[allow(clippy::use_self)]
-        impl #impl_generics ::std::str::FromStr for #name #ty_generics #where_clause {
+        impl #impl_generics ::core::str::FromStr for #name #ty_generics #where_clause {
             type Err = #strum_module_path::ParseError;
-            fn from_str(s: &str) -> ::std::result::Result< #name #ty_generics , Self::Err> {
+            fn from_str(s: &str) -> ::core::result::Result< #name #ty_generics , Self::Err> {
                 match s {
                     #(#arms),*
                 }
             }
         }
+    };
+
+    let try_from_str = try_from_str(
+        name,
+        impl_generics,
+        ty_generics,
+        where_clause,
+        strum_module_path,
+    );
+
+    Ok(quote! {
+        #from_str
+        #try_from_str
     })
 }
+
+#[rustversion::before(1.34)]
+fn try_from_str(
+    _name: &proc_macro2::Ident,
+    _impl_generics: syn::ImplGenerics,
+    _ty_generics: syn::TypeGenerics,
+    _where_clause: Option<&syn::WhereClause>,
+    _strum_module_path: syn::Path,
+) -> TokenStream {
+    Default::default()
+}
+
+#[rustversion::since(1.34)]
+fn try_from_str(
+    name: &proc_macro2::Ident,
+    impl_generics: syn::ImplGenerics,
+    ty_generics: syn::TypeGenerics,
+    where_clause: Option<&syn::WhereClause>,
+    strum_module_path: syn::Path,
+) -> TokenStream {
+    quote! {
+        #[allow(clippy::use_self)]
+        impl #impl_generics ::std::convert::TryFrom<&str> for #name #ty_generics #where_clause {
+            type Error = #strum_module_path::ParseError;
+            fn try_from(s: &str) -> ::std::result::Result< #name #ty_generics , Self::Error> {
+                ::std::str::FromStr::from_str(s)
+            }
+        }
+    }
+}


Test patch:
diff --git a/strum_tests/Cargo.toml b/strum_tests/Cargo.toml
index a4b3470..28664a2 100644
--- a/strum_tests/Cargo.toml
+++ b/strum_tests/Cargo.toml
@@ -13,4 +13,7 @@ structopt = "0.2.18"
 bitflags = "=1.2"
 
 [build-dependencies]
-version_check = "0.9.2"
\ No newline at end of file
+version_check = "0.9.2"
+
+[dev-dependencies]
+rustversion = "1.0"
diff --git a/strum_tests/tests/from_repr.rs b/strum_tests/tests/from_repr.rs
new file mode 100644
index 0000000..bb7263f
--- /dev/null
+++ b/strum_tests/tests/from_repr.rs
@@ -0,0 +1,63 @@
+use strum::FromRepr;
+
+#[derive(Debug, FromRepr, PartialEq)]
+#[repr(u8)]
+enum Week {
+    Sunday,
+    Monday,
+    Tuesday,
+    Wednesday,
+    Thursday,
+    Friday = 4 + 3,
+    Saturday = 8,
+}
+
+#[test]
+fn simple_test() {
+    assert_eq!(Week::from_repr(0), Some(Week::Sunday));
+    assert_eq!(Week::from_repr(1), Some(Week::Monday));
+    assert_eq!(Week::from_repr(6), None);
+    assert_eq!(Week::from_repr(7), Some(Week::Friday));
+    assert_eq!(Week::from_repr(8), Some(Week::Saturday));
+    assert_eq!(Week::from_repr(9), None);
+}
+
+#[rustversion::since(1.46)]
+#[test]
+fn const_test() {
+    // This is to test that it works in a const fn
+    const fn from_repr(discriminant: u8) -> Option<Week> {
+        Week::from_repr(discriminant)
+    }
+    assert_eq!(from_repr(0), Some(Week::Sunday));
+    assert_eq!(from_repr(1), Some(Week::Monday));
+    assert_eq!(from_repr(6), None);
+    assert_eq!(from_repr(7), Some(Week::Friday));
+    assert_eq!(from_repr(8), Some(Week::Saturday));
+    assert_eq!(from_repr(9), None);
+}
+
+#[test]
+fn crate_module_path_test() {
+    pub mod nested {
+        pub mod module {
+            pub use strum;
+        }
+    }
+
+    #[derive(Debug, FromRepr, PartialEq)]
+    #[strum(crate = "nested::module::strum")]
+    enum Week {
+        Sunday,
+        Monday,
+        Tuesday,
+        Wednesday,
+        Thursday,
+        Friday,
+        Saturday,
+    }
+
+    assert_eq!(Week::from_repr(0), Some(Week::Sunday));
+    assert_eq!(Week::from_repr(6), Some(Week::Saturday));
+    assert_eq!(Week::from_repr(7), None);
+}
diff --git a/strum_tests/tests/from_str.rs b/strum_tests/tests/from_str.rs
index 77922e9..463e3b5 100644
--- a/strum_tests/tests/from_str.rs
+++ b/strum_tests/tests/from_str.rs
@@ -17,39 +17,56 @@ enum Color {
     Black,
 }
 
+#[rustversion::since(1.34)]
+fn assert_from_str<'a, T>(a: T, from: &'a str)
+where
+    T: PartialEq + std::str::FromStr + std::convert::TryFrom<&'a str> + std::fmt::Debug,
+    <T as std::str::FromStr>::Err: std::fmt::Debug,
+    <T as std::convert::TryFrom<&'a str>>::Error: std::fmt::Debug,
+{
+    assert_eq!(a, T::from_str(from).unwrap());
+    assert_eq!(a, std::convert::TryFrom::try_from(from).unwrap());
+}
+
+#[rustversion::before(1.34)]
+fn assert_from_str<T>(a: T, from: &str)
+where
+    T: PartialEq + std::str::FromStr + std::fmt::Debug,
+    <T as std::str::FromStr>::Err: std::fmt::Debug,
+{
+    assert_eq!(a, T::from_str(from).unwrap());
+}
+
 #[test]
 fn color_simple() {
-    assert_eq!(Color::Red, Color::from_str("Red").unwrap());
+    assert_from_str(Color::Red, "Red");
 }
 
 #[test]
 fn color_value() {
-    assert_eq!(Color::Blue { hue: 0 }, Color::from_str("Blue").unwrap());
+    assert_from_str(Color::Blue { hue: 0 }, "Blue");
 }
 
 #[test]
 fn color_serialize() {
-    assert_eq!(Color::Yellow, Color::from_str("y").unwrap());
-    assert_eq!(Color::Yellow, Color::from_str("yellow").unwrap());
+    assert_from_str(Color::Yellow, "y");
+    assert_from_str(Color::Yellow, "yellow");
 }
 
 #[test]
 fn color_to_string() {
-    assert_eq!(Color::Purple, Color::from_str("purp").unwrap());
+    assert_from_str(Color::Purple, "purp");
 }
 
 #[test]
 fn color_default() {
-    assert_eq!(
-        Color::Green(String::from("not found")),
-        Color::from_str("not found").unwrap()
-    );
+    assert_from_str(Color::Green(String::from("not found")), "not found");
 }
 
 #[test]
 fn color_ascii_case_insensitive() {
-    assert_eq!(Color::Black, Color::from_str("BLK").unwrap());
-    assert_eq!(Color::Black, Color::from_str("bLaCk").unwrap());
+    assert_from_str(Color::Black, "BLK");
+    assert_from_str(Color::Black, "bLaCk");
 }
 
 #[derive(Debug, Eq, PartialEq, EnumString)]
@@ -65,18 +82,9 @@ enum Brightness {
 
 #[test]
 fn brightness_serialize_all() {
-    assert_eq!(
-        Brightness::DarkBlack,
-        Brightness::from_str("dark_black").unwrap()
-    );
-    assert_eq!(
-        Brightness::Dim { glow: 0 },
-        Brightness::from_str("dim").unwrap()
-    );
-    assert_eq!(
-        Brightness::BrightWhite,
-        Brightness::from_str("Bright").unwrap()
-    );
+    assert_from_str(Brightness::DarkBlack, "dark_black");
+    assert_from_str(Brightness::Dim { glow: 0 }, "dim");
+    assert_from_str(Brightness::BrightWhite, "Bright");
 }
 
 #[derive(Debug, Eq, PartialEq, EnumString)]
@@ -100,13 +108,13 @@ fn week_not_found() {
 
 #[test]
 fn week_found() {
-    assert_eq!(Result::Ok(Week::Sunday), Week::from_str("Sunday"));
-    assert_eq!(Result::Ok(Week::Monday), Week::from_str("Monday"));
-    assert_eq!(Result::Ok(Week::Tuesday), Week::from_str("Tuesday"));
-    assert_eq!(Result::Ok(Week::Wednesday), Week::from_str("Wednesday"));
-    assert_eq!(Result::Ok(Week::Thursday), Week::from_str("Thursday"));
-    assert_eq!(Result::Ok(Week::Friday), Week::from_str("Friday"));
-    assert_eq!(Result::Ok(Week::Saturday), Week::from_str("Saturday"));
+    assert_from_str(Week::Sunday, "Sunday");
+    assert_from_str(Week::Monday, "Monday");
+    assert_from_str(Week::Tuesday, "Tuesday");
+    assert_from_str(Week::Wednesday, "Wednesday");
+    assert_from_str(Week::Thursday, "Thursday");
+    assert_from_str(Week::Friday, "Friday");
+    assert_from_str(Week::Saturday, "Saturday");
 }
 
 #[derive(Debug, Eq, PartialEq, EnumString)]
@@ -117,7 +125,7 @@ enum Lifetime<'a> {
 
 #[test]
 fn lifetime_test() {
-    assert_eq!(Lifetime::Life(""), Lifetime::from_str("Life").unwrap());
+    assert_from_str(Lifetime::Life(""), "Life");
 }
 
 #[derive(Debug, Eq, PartialEq, EnumString)]
@@ -128,7 +136,7 @@ enum Generic<T: Default> {
 
 #[test]
 fn generic_test() {
-    assert_eq!(Generic::Gen(""), Generic::from_str("Gen").unwrap());
+    assert_from_str(Generic::Gen(""), "Gen");
 }
 
 #[derive(Debug, Eq, PartialEq, EnumString)]
@@ -143,29 +151,27 @@ enum CaseInsensitiveEnum {
 
 #[test]
 fn case_insensitive_enum_no_attr() {
-    assert_eq!(
-        CaseInsensitiveEnum::NoAttr,
-        CaseInsensitiveEnum::from_str("noattr").unwrap()
-    );
+    assert_from_str(CaseInsensitiveEnum::NoAttr, "noattr");
 }
 
 #[test]
 fn case_insensitive_enum_no_case_insensitive() {
-    assert_eq!(
-        CaseInsensitiveEnum::NoCaseInsensitive,
-        CaseInsensitiveEnum::from_str("NoCaseInsensitive").unwrap(),
-    );
+    assert_from_str(CaseInsensitiveEnum::NoCaseInsensitive, "NoCaseInsensitive");
     assert!(CaseInsensitiveEnum::from_str("nocaseinsensitive").is_err());
 }
 
+#[rustversion::since(1.34)]
 #[test]
-fn case_insensitive_enum_case_insensitive() {
-    assert_eq!(
-        CaseInsensitiveEnum::CaseInsensitive,
-        CaseInsensitiveEnum::from_str("CaseInsensitive").unwrap(),
-    );
-    assert_eq!(
-        CaseInsensitiveEnum::CaseInsensitive,
-        CaseInsensitiveEnum::from_str("caseinsensitive").unwrap(),
+fn case_insensitive_enum_no_case_insensitive_try_from() {
+    assert_from_str(CaseInsensitiveEnum::NoCaseInsensitive, "NoCaseInsensitive");
+    assert!(
+        <CaseInsensitiveEnum as std::convert::TryFrom<&str>>::try_from("nocaseinsensitive")
+            .is_err()
     );
 }
+
+#[test]
+fn case_insensitive_enum_case_insensitive() {
+    assert_from_str(CaseInsensitiveEnum::CaseInsensitive, "CaseInsensitive");
+    assert_from_str(CaseInsensitiveEnum::CaseInsensitive, "caseinsensitive");
+}