Repository: Peternator7/strum (rust)

Issue:
Support for const_into_str to Enable Static String Conversion in Const Context
Currently, the `strum` library does not support converting enum variants into static strings within a const context. This limitation arises because the library uses the `From` trait, which cannot have const methods, even in nightly Rust. This issue becomes particularly significant when attempting to use [concatcp](https://docs.rs/const_format/latest/const_format/macro.concatcp.html) to concatenate two static strings in a const context.

To address this limitation, I propose the addition of a new enum meta attribute, `const_into_str`. This attribute would enable generating an additional public const function for enums, allowing conversion to static strings in a const context.

This enhancement would greatly improve `strum`'s functionality by allowing more flexible and performant use of enums in constant expressions. I have prepared an implementation and will submit a pull request for review.


Pull request description:
Add support for `const_into_str` attribute to enable static string conversions in const contexts
This pull request introduces the `const_into_str` attribute to the `strum` macros, enabling enums to be converted into static strings in const contexts. This feature is particularly useful when working with compile-time string manipulations, such as those required by `concatcp`.

**Key Changes:**
1. Addition of `const_into_str` keyword in enum metadata processing.
2. Conditional generation of a `const fn into_str()` method for enums marked with `const_into_str`.
3. Adjustment in the generation of the `From` trait to utilize `into_str()` when `const_into_str` is enabled.

The implementation ensures that existing functionality is preserved and provides users with the option to opt into this new feature as needed. This feature could be especially beneficial for users needing efficient, compile-time safe string operations involving enums.

Please review the changes and let me know if there are any modifications or improvements required.

resolves #352 

Patch:
diff --git a/strum_macros/src/helpers/metadata.rs b/strum_macros/src/helpers/metadata.rs
index 94100a7..384debf 100644
--- a/strum_macros/src/helpers/metadata.rs
+++ b/strum_macros/src/helpers/metadata.rs
@@ -16,6 +16,7 @@ pub mod kw {
 
     // enum metadata
     custom_keyword!(serialize_all);
+    custom_keyword!(const_into_str);
     custom_keyword!(use_phf);
     custom_keyword!(prefix);
 
@@ -51,6 +52,7 @@ pub enum EnumMeta {
         kw: kw::prefix,
         prefix: LitStr,
     },
+    ConstIntoStr(kw::const_into_str)
 }
 
 impl Parse for EnumMeta {
@@ -80,6 +82,8 @@ impl Parse for EnumMeta {
             input.parse::<Token![=]>()?;
             let prefix = input.parse()?;
             Ok(EnumMeta::Prefix { kw, prefix })
+        } else if lookahead.peek(kw::const_into_str) {
+            Ok(EnumMeta::ConstIntoStr(input.parse()?))
         } else {
             Err(lookahead.error())
         }
diff --git a/strum_macros/src/helpers/type_props.rs b/strum_macros/src/helpers/type_props.rs
index 7302853..c04b358 100644
--- a/strum_macros/src/helpers/type_props.rs
+++ b/strum_macros/src/helpers/type_props.rs
@@ -23,6 +23,7 @@ pub struct StrumTypeProperties {
     pub use_phf: bool,
     pub prefix: Option<LitStr>,
     pub enum_repr: Option<TokenStream>,
+    pub const_into_str: bool,
 }
 
 impl HasTypeProperties for DeriveInput {
@@ -37,6 +38,8 @@ impl HasTypeProperties for DeriveInput {
         let mut use_phf_kw = None;
         let mut crate_module_path_kw = None;
         let mut prefix_kw = None;
+        let mut const_into_str = None;
+
         for meta in strum_meta {
             match meta {
                 EnumMeta::SerializeAll { case_style, kw } => {
@@ -82,6 +85,14 @@ impl HasTypeProperties for DeriveInput {
                     prefix_kw = Some(kw);
                     output.prefix = Some(prefix);
                 }
+                EnumMeta::ConstIntoStr(kw) => {
+                    if let Some(fst_kw) = const_into_str {
+                        return Err(occurrence_error(fst_kw, kw, "const_into_str"));
+                    }
+
+                    const_into_str = Some(kw);
+                    output.const_into_str = true;
+                }
             }
         }
 
diff --git a/strum_macros/src/macros/strings/as_ref_str.rs b/strum_macros/src/macros/strings/as_ref_str.rs
index 8d44f81..eb535df 100644
--- a/strum_macros/src/macros/strings/as_ref_str.rs
+++ b/strum_macros/src/macros/strings/as_ref_str.rs
@@ -99,7 +99,7 @@ pub fn as_static_str_inner(
                 }
             }
         },
-        GenerateTraitVariant::From => quote! {
+        GenerateTraitVariant::From if !type_properties.const_into_str => quote! {
             impl #impl_generics ::core::convert::From<#name #ty_generics> for &'static str #where_clause {
                 fn from(x: #name #ty_generics) -> &'static str {
                     match x {
@@ -115,5 +115,27 @@ pub fn as_static_str_inner(
                 }
             }
         },
+        GenerateTraitVariant::From  => quote! {
+            impl #impl_generics #name #ty_generics #where_clause {
+                pub const fn into_str(&self) -> &'static str {
+                    match self {
+                        #(#arms3),*
+                    }
+                }
+            }
+
+            impl #impl_generics ::core::convert::From<#name #ty_generics> for &'static str #where_clause {
+                fn from(x: #name #ty_generics) -> &'static str {
+                    match x {
+                        #(#arms2),*
+                    }
+                }
+            }
+            impl #impl_generics2 ::core::convert::From<&'_derivative_strum #name #ty_generics> for &'static str #where_clause {
+                fn from(x: &'_derivative_strum #name #ty_generics) -> &'static str {
+                    x.into_str()
+                }
+            }
+        },
     })
 }


Test patch:
diff --git a/strum_tests/tests/as_ref_str.rs b/strum_tests/tests/as_ref_str.rs
index d492f17..c0cf30b 100644
--- a/strum_tests/tests/as_ref_str.rs
+++ b/strum_tests/tests/as_ref_str.rs
@@ -111,3 +111,77 @@ fn brightness_serialize_all() {
     assert_eq!("dim", <&'static str>::from(Brightness::Dim { glow: 0 }));
     assert_eq!("Bright", <&'static str>::from(Brightness::BrightWhite));
 }
+
+#[derive(IntoStaticStr)]
+#[strum(const_into_str)]
+enum Bar<'a, T>
+    where
+        T: AsRef<str>,
+{
+    A(T),
+    B,
+    C(&'a i32),
+    #[strum(serialize = "Dark")]
+    D,
+    #[strum(to_string = "Green")]
+    G,
+    #[strum(serialize = "b", to_string = "blue")]
+    Blue { hue: usize },
+    #[strum(serialize = "y", serialize = "yellow")]
+    Yellow,
+}
+
+#[derive(IntoStaticStr)]
+#[strum(const_into_str)]
+enum Baz<'a, T> {
+    A(T),
+    C(&'a i32),
+}
+
+#[derive(IntoStaticStr)]
+#[strum(serialize_all = "snake_case")]
+#[strum(const_into_str)]
+enum BrightnessConst {
+    DarkBlack,
+    Dim {
+        glow: usize,
+    },
+    #[strum(serialize = "Bright")]
+    BrightWhite,
+}
+
+#[test]
+fn test_const_into_static_str() {
+
+    const A: &'static str = Bar::A("foo").into_str();
+    assert_eq!("A", A);
+    const B: &'static str = Bar::B::<&str>.into_str();
+    assert_eq!("B", B);
+    const C: &'static str = Bar::C::<&str>(&12).into_str();
+    assert_eq!("C", C);
+
+    const D: &'static str = Bar::D::<&str>.into_str();
+    assert_eq!("Dark", D);
+
+    const G: &'static str = Bar::G::<&str>.into_str();
+    assert_eq!("Green", G);
+
+    const BLUE: &'static str = Bar::Blue::<&str>{ hue: 2 }.into_str();
+    assert_eq!("blue", BLUE);
+
+    const YELLOW: &'static str = Bar::Yellow::<&str>.into_str();
+    assert_eq!("yellow", YELLOW);
+
+    const BAZ_A: &'static str = Baz::A("foo").into_str();
+    assert_eq!("A", BAZ_A);
+
+    const BAZ_C: &'static str = Baz::C::<&str>(&6).into_str();
+    assert_eq!("C", BAZ_C);
+
+    const DARK_BLACK: &'static str = BrightnessConst::DarkBlack.into_str();
+    assert_eq!("dark_black", DARK_BLACK);
+    const DIM: &'static str = BrightnessConst::Dim {glow:1}.into_str();
+    assert_eq!("dim", DIM);
+    const BRIGHT_WHITE: &'static str = BrightnessConst::BrightWhite.into_str();
+    assert_eq!("Bright", BRIGHT_WHITE);
+}