Repository: Peternator7/strum (rust)

Issue:
Add `#[strum(transparent)]` argument
This crate is amazing for tackling a lot of boilerplate code and also ensuring variant name changes propagate to the trait implementations. I use `AsRefStr` and `EnumString` quite a lot.

However, a lot of times I've found myself in the situation of having something like this:
```rust
enum MyEnum {
    A,
    B,
    C,
    Other(String)
}
```

Afaik there's no way of propagating the `AsRef<str>` or `FromStr` implementations to the underlying `String`. What do you think about implementing a `transparent` argument to the attribute, similar to `#[serde(transparent)]` - only for one field provided in an unnamed/named fashion.

So then we could do:
```rust
#[derive(AsRefStr, EnumStr)]
enum MyEnum {
    A,
    B,
    C,
    #[strum(transparent)]
    Other(String)
}
```

I could try and pick this up if you like the idea. I think it would be handy for a lot of people.

Pull request description:
Add `#[strum(transparent)]` attribute
Adds the `transparent` attribute, usable with `AsRefStr`, `Display` and `IntoStaticStr`. This delegates the derived trait impl to the underlying (single) field of the enum variant.

Implementing this for `EnumString` would be nice, but it doesn't really act the same way as for the other traits. For `EnumString`, something like `#[strum(other)]` (similar to `#[serde(other)]`) would make more sense, because there you would have a last single attempt to parse the input into a `catch all` variant, whereas `trasparent` can be added to multiple variants.

Therefore, if desired, that should be handled in a separate PR.

Closes #258 

Patch:
diff --git a/strum/src/additional_attributes.rs b/strum/src/additional_attributes.rs
index a29f1d6..98bee85 100644
--- a/strum/src/additional_attributes.rs
+++ b/strum/src/additional_attributes.rs
@@ -73,6 +73,10 @@
 //!     ```
 //!     The plugin will fail if the data doesn't implement From<&str>. You can only have one `default`
 //!     on your enum.
+//! 
+//! - `transparent`: Signals that the inner field's implementation should be used, instead of generating 
+//!    one for this variant. Only applicable to enum variants with a single field. Compatible with the 
+//!    `AsRefStr`, `Display` and `IntoStaticStr` derive macros.
 //!
 //! - `disabled`: removes variant from generated code.
 //!
diff --git a/strum_macros/src/helpers/metadata.rs b/strum_macros/src/helpers/metadata.rs
index 94100a7..9ded40c 100644
--- a/strum_macros/src/helpers/metadata.rs
+++ b/strum_macros/src/helpers/metadata.rs
@@ -29,6 +29,7 @@ pub mod kw {
     custom_keyword!(detailed_message);
     custom_keyword!(serialize);
     custom_keyword!(to_string);
+    custom_keyword!(transparent);
     custom_keyword!(disabled);
     custom_keyword!(default);
     custom_keyword!(default_with);
@@ -164,6 +165,7 @@ pub enum VariantMeta {
         kw: kw::to_string,
         value: LitStr,
     },
+    Transparent(kw::transparent),
     Disabled(kw::disabled),
     Default(kw::default),
     DefaultWith {
@@ -203,6 +205,8 @@ impl Parse for VariantMeta {
             let _: Token![=] = input.parse()?;
             let value = input.parse()?;
             Ok(VariantMeta::ToString { kw, value })
+        } else if lookahead.peek(kw::transparent) {
+            Ok(VariantMeta::Transparent(input.parse()?))
         } else if lookahead.peek(kw::disabled) {
             Ok(VariantMeta::Disabled(input.parse()?))
         } else if lookahead.peek(kw::default) {
diff --git a/strum_macros/src/helpers/mod.rs b/strum_macros/src/helpers/mod.rs
index 23d60b5..9ac17dc 100644
--- a/strum_macros/src/helpers/mod.rs
+++ b/strum_macros/src/helpers/mod.rs
@@ -25,6 +25,13 @@ pub fn non_unit_variant_error() -> syn::Error {
     )
 }
 
+pub fn non_single_field_variant_error(attr: &str) -> syn::Error {
+    syn::Error::new(
+        Span::call_site(),
+        format_args!("The [`{}`] attribute only supports enum variants with a single field", attr),
+    )
+}
+
 pub fn strum_discriminants_passthrough_error(span: &impl Spanned) -> syn::Error {
     syn::Error::new(
         span.span(),
diff --git a/strum_macros/src/helpers/variant_props.rs b/strum_macros/src/helpers/variant_props.rs
index d6e0cab..d4d2356 100644
--- a/strum_macros/src/helpers/variant_props.rs
+++ b/strum_macros/src/helpers/variant_props.rs
@@ -11,6 +11,7 @@ pub trait HasStrumVariantProperties {
 
 #[derive(Clone, Eq, PartialEq, Debug, Default)]
 pub struct StrumVariantProperties {
+    pub transparent: Option<kw::transparent>,
     pub disabled: Option<kw::disabled>,
     pub default: Option<kw::default>,
     pub default_with: Option<LitStr>,
@@ -63,6 +64,7 @@ impl HasStrumVariantProperties for Variant {
 
         let mut message_kw = None;
         let mut detailed_message_kw = None;
+        let mut transparent_kw = None;
         let mut disabled_kw = None;
         let mut default_kw = None;
         let mut default_with_kw = None;
@@ -100,6 +102,14 @@ impl HasStrumVariantProperties for Variant {
                     to_string_kw = Some(kw);
                     output.to_string = Some(value);
                 }
+                VariantMeta::Transparent(kw) => {
+                    if let Some(fst_kw) = transparent_kw {
+                        return Err(occurrence_error(fst_kw, kw, "transparent"));
+                    }
+
+                    transparent_kw = Some(kw);
+                    output.transparent = Some(kw);
+                }
                 VariantMeta::Disabled(kw) => {
                     if let Some(fst_kw) = disabled_kw {
                         return Err(occurrence_error(fst_kw, kw, "disabled"));
diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs
index 72f8a37..24ef191 100644
--- a/strum_macros/src/lib.rs
+++ b/strum_macros/src/lib.rs
@@ -249,9 +249,9 @@ pub fn static_variants_array(input: proc_macro::TokenStream) -> proc_macro::Toke
 pub fn as_static_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
-    let toks = macros::as_ref_str::as_static_str_inner(
+    let toks = macros::into_static_str::as_static_str_inner(
         &ast,
-        &macros::as_ref_str::GenerateTraitVariant::AsStaticStr,
+        &macros::into_static_str::GenerateTraitVariant::AsStaticStr,
     )
     .unwrap_or_else(|err| err.to_compile_error());
     debug_print_generated(&ast, &toks);
@@ -291,9 +291,9 @@ pub fn as_static_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream
 pub fn into_static_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
-    let toks = macros::as_ref_str::as_static_str_inner(
+    let toks = macros::into_static_str::as_static_str_inner(
         &ast,
-        &macros::as_ref_str::GenerateTraitVariant::From,
+        &macros::into_static_str::GenerateTraitVariant::From,
     )
     .unwrap_or_else(|err| err.to_compile_error());
     debug_print_generated(&ast, &toks);
diff --git a/strum_macros/src/macros/mod.rs b/strum_macros/src/macros/mod.rs
index fafc048..a1a51ec 100644
--- a/strum_macros/src/macros/mod.rs
+++ b/strum_macros/src/macros/mod.rs
@@ -15,4 +15,5 @@ mod strings;
 pub use self::strings::as_ref_str;
 pub use self::strings::display;
 pub use self::strings::from_string;
+pub use self::strings::into_static_str;
 pub use self::strings::to_string;
diff --git a/strum_macros/src/macros/strings/as_ref_str.rs b/strum_macros/src/macros/strings/as_ref_str.rs
index 617b887..561664b 100644
--- a/strum_macros/src/macros/strings/as_ref_str.rs
+++ b/strum_macros/src/macros/strings/as_ref_str.rs
@@ -1,8 +1,10 @@
 use proc_macro2::TokenStream;
 use quote::quote;
-use syn::{parse_quote, Data, DeriveInput, Fields};
+use syn::{Data, DeriveInput, Fields};
 
-use crate::helpers::{non_enum_error, HasStrumVariantProperties, HasTypeProperties};
+use crate::helpers::{
+    non_enum_error, non_single_field_variant_error, HasStrumVariantProperties, HasTypeProperties,
+};
 
 fn get_arms(ast: &DeriveInput) -> syn::Result<Vec<TokenStream>> {
     let name = &ast.ident;
@@ -22,17 +24,34 @@ fn get_arms(ast: &DeriveInput) -> syn::Result<Vec<TokenStream>> {
             continue;
         }
 
-        // Look at all the serialize attributes.
-        // Use `to_string` attribute (not `as_ref_str` or something) to keep things consistent
-        // (i.e. always `enum.as_ref().to_string() == enum.to_string()`).
-        let output = variant_properties.get_preferred_name(type_properties.case_style);
-        let params = match variant.fields {
-            Fields::Unit => quote! {},
-            Fields::Unnamed(..) => quote! { (..) },
-            Fields::Named(..) => quote! { {..} },
+        let arm = if variant_properties.transparent.is_some() {
+            let arm_end = match &variant.fields {
+                Fields::Unnamed(f) if f.unnamed.len() == 1 => {
+                    quote! { (ref v) => ::core::convert::AsRef::<str>::as_ref(v) }
+                }
+                Fields::Named(f) if f.named.len() == 1 => {
+                    let ident = f.named.last().unwrap().ident.as_ref().unwrap();
+                    quote! { {ref #ident} => ::core::convert::AsRef::<str>::as_ref(#ident) }
+                }
+                _ => return Err(non_single_field_variant_error("transparent")),
+            };
+
+            quote! { #name::#ident #arm_end }
+        } else {
+            // Look at all the serialize attributes.
+            // Use `to_string` attribute (not `as_ref_str` or something) to keep things consistent
+            // (i.e. always `enum.as_ref().to_string() == enum.to_string()`).
+            let output = variant_properties.get_preferred_name(type_properties.case_style);
+            let params = match variant.fields {
+                Fields::Unit => quote! {},
+                Fields::Unnamed(..) => quote! { (..) },
+                Fields::Named(..) => quote! { {..} },
+            };
+
+            quote! { #name::#ident #params => #output }
         };
 
-        arms.push(quote! { #name::#ident #params => #output });
+        arms.push(arm);
     }
 
     if arms.len() < variants.len() {
@@ -61,57 +80,3 @@ pub fn as_ref_str_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
         }
     })
 }
-
-pub enum GenerateTraitVariant {
-    AsStaticStr,
-    From,
-}
-
-pub fn as_static_str_inner(
-    ast: &DeriveInput,
-    trait_variant: &GenerateTraitVariant,
-) -> syn::Result<TokenStream> {
-    let name = &ast.ident;
-    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
-    let arms = get_arms(ast)?;
-    let type_properties = ast.get_type_properties()?;
-    let strum_module_path = type_properties.crate_module_path();
-
-    let mut generics = ast.generics.clone();
-    generics
-        .params
-        .push(syn::GenericParam::Lifetime(syn::LifetimeParam::new(
-            parse_quote!('_derivative_strum),
-        )));
-    let (impl_generics2, _, _) = generics.split_for_impl();
-    let arms2 = arms.clone();
-    let arms3 = arms.clone();
-
-    Ok(match trait_variant {
-        GenerateTraitVariant::AsStaticStr => quote! {
-            impl #impl_generics #strum_module_path::AsStaticRef<str> for #name #ty_generics #where_clause {
-                fn as_static(&self) -> &'static str {
-                    match *self {
-                        #(#arms),*
-                    }
-                }
-            }
-        },
-        GenerateTraitVariant::From => 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 {
-                        #(#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 {
-                    match *x {
-                        #(#arms3),*
-                    }
-                }
-            }
-        },
-    })
-}
diff --git a/strum_macros/src/macros/strings/display.rs b/strum_macros/src/macros/strings/display.rs
index 47432f5..f5c0290 100644
--- a/strum_macros/src/macros/strings/display.rs
+++ b/strum_macros/src/macros/strings/display.rs
@@ -2,7 +2,9 @@ use proc_macro2::{Ident, TokenStream};
 use quote::quote;
 use syn::{punctuated::Punctuated, Data, DeriveInput, Fields, LitStr, Token};
 
-use crate::helpers::{non_enum_error, HasStrumVariantProperties, HasTypeProperties};
+use crate::helpers::{
+    non_enum_error, non_single_field_variant_error, HasStrumVariantProperties, HasTypeProperties,
+};
 
 pub fn display_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
     let name = &ast.ident;
@@ -23,6 +25,22 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
             continue;
         }
 
+        if variant_properties.transparent.is_some() {
+            let arm_end = match &variant.fields {
+                Fields::Unnamed(f) if f.unnamed.len() == 1 => {
+                    quote! { (ref v) => ::core::fmt::Display::fmt(v, f)}
+                }
+                Fields::Named(f) if f.named.len() == 1 => {
+                    let ident = f.named.last().unwrap().ident.as_ref().unwrap();
+                    quote! { {ref #ident} => ::core::fmt::Display::fmt(#ident, f) }
+                }
+                _ => return Err(non_single_field_variant_error("transparent")),
+            };
+
+            arms.push(quote! { #name::#ident #arm_end });
+            continue;
+        }
+
         // Look at all the serialize attributes.
         let mut output = variant_properties.get_preferred_name(type_properties.case_style);
         if let Some(prefix) = &type_properties.prefix {
diff --git a/strum_macros/src/macros/strings/into_static_str.rs b/strum_macros/src/macros/strings/into_static_str.rs
new file mode 100644
index 0000000..ee05b3b
--- /dev/null
+++ b/strum_macros/src/macros/strings/into_static_str.rs
@@ -0,0 +1,121 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{parse_quote, Data, DeriveInput, Fields};
+
+use crate::helpers::{
+    non_enum_error, non_single_field_variant_error, HasStrumVariantProperties, HasTypeProperties,
+};
+
+fn get_arms(ast: &DeriveInput) -> syn::Result<Vec<TokenStream>> {
+    let name = &ast.ident;
+    let mut arms = Vec::new();
+    let variants = match &ast.data {
+        Data::Enum(v) => &v.variants,
+        _ => return Err(non_enum_error()),
+    };
+
+    let type_properties = ast.get_type_properties()?;
+
+    for variant in variants {
+        let ident = &variant.ident;
+        let variant_properties = variant.get_variant_properties()?;
+
+        if variant_properties.disabled.is_some() {
+            continue;
+        }
+
+        let arm = if variant_properties.transparent.is_some() {
+            let arm_end = match &variant.fields {
+                Fields::Unnamed(f) if f.unnamed.len() == 1 => {
+                    quote! { (ref v) => ::core::convert::From::from(v) }
+                }
+                Fields::Named(f) if f.named.len() == 1 => {
+                    let ident = f.named.last().unwrap().ident.as_ref().unwrap();
+                    quote! { {ref #ident} => ::core::convert::From::from(#ident) }
+                }
+                _ => return Err(non_single_field_variant_error("transparent")),
+            };
+
+            quote! { #name::#ident #arm_end }
+        } else {
+            // Look at all the serialize attributes.
+            // Use `to_string` attribute (not `as_ref_str` or something) to keep things consistent
+            // (i.e. always `enum.as_ref().to_string() == enum.to_string()`).
+            let output = variant_properties.get_preferred_name(type_properties.case_style);
+            let params = match variant.fields {
+                Fields::Unit => quote! {},
+                Fields::Unnamed(..) => quote! { (..) },
+                Fields::Named(..) => quote! { {..} },
+            };
+
+            quote! { #name::#ident #params => #output }
+        };
+
+        arms.push(arm);
+    }
+
+    if arms.len() < variants.len() {
+        arms.push(quote! {
+            _ => panic!(
+                "AsRef::<str>::as_ref() or AsStaticRef::<str>::as_static() \
+                 called on disabled variant.",
+            )
+        });
+    }
+
+    Ok(arms)
+}
+
+pub enum GenerateTraitVariant {
+    AsStaticStr,
+    From,
+}
+
+pub fn as_static_str_inner(
+    ast: &DeriveInput,
+    trait_variant: &GenerateTraitVariant,
+) -> syn::Result<TokenStream> {
+    let name = &ast.ident;
+    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
+    let arms = get_arms(ast)?;
+    let type_properties = ast.get_type_properties()?;
+    let strum_module_path = type_properties.crate_module_path();
+
+    let mut generics = ast.generics.clone();
+    generics
+        .params
+        .push(syn::GenericParam::Lifetime(syn::LifetimeParam::new(
+            parse_quote!('_derivative_strum),
+        )));
+    let (impl_generics2, _, _) = generics.split_for_impl();
+    let arms2 = arms.clone();
+    let arms3 = arms.clone();
+
+    Ok(match trait_variant {
+        GenerateTraitVariant::AsStaticStr => quote! {
+            impl #impl_generics #strum_module_path::AsStaticRef<str> for #name #ty_generics #where_clause {
+                fn as_static(&self) -> &'static str {
+                    match *self {
+                        #(#arms),*
+                    }
+                }
+            }
+        },
+        GenerateTraitVariant::From => 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 {
+                        #(#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 {
+                    match *x {
+                        #(#arms3),*
+                    }
+                }
+            }
+        },
+    })
+}
diff --git a/strum_macros/src/macros/strings/mod.rs b/strum_macros/src/macros/strings/mod.rs
index e416f4b..9c27565 100644
--- a/strum_macros/src/macros/strings/mod.rs
+++ b/strum_macros/src/macros/strings/mod.rs
@@ -2,3 +2,4 @@ pub mod as_ref_str;
 pub mod display;
 pub mod from_string;
 pub mod to_string;
+pub mod into_static_str;
\ No newline at end of file


Test patch:
diff --git a/strum_tests/tests/as_ref_no_strum.rs b/strum_tests/tests/as_ref_no_strum.rs
index e8261f4..9d1ea15 100644
--- a/strum_tests/tests/as_ref_no_strum.rs
+++ b/strum_tests/tests/as_ref_no_strum.rs
@@ -12,6 +12,8 @@ enum Color {
     Yellow,
     #[strum(default)]
     Green(String),
+    #[strum(transparent)]
+    Other(String),
 }
 
 #[test]
@@ -33,3 +35,8 @@ fn as_yellow_str() {
 fn as_green_str() {
     assert_eq!("Green", (Color::Green(String::default())).as_ref());
 }
+
+#[test]
+fn as_other_str() {
+    assert_eq!("color", (Color::Other("color".to_owned())).as_ref());
+}
diff --git a/strum_tests/tests/as_ref_str.rs b/strum_tests/tests/as_ref_str.rs
index d492f17..ab8b7dd 100644
--- a/strum_tests/tests/as_ref_str.rs
+++ b/strum_tests/tests/as_ref_str.rs
@@ -5,6 +5,19 @@ use strum::{AsRefStr, AsStaticRef, AsStaticStr, EnumString, IntoStaticStr};
 
 mod core {} // ensure macros call `::core`
 
+#[derive(Debug, Eq, PartialEq, EnumString, AsRefStr, AsStaticStr, IntoStaticStr)]
+enum InnerColor {
+    Purple,
+    Violet,
+    Fuchsia,
+}
+
+impl Default for InnerColor {
+    fn default() -> Self {
+        InnerColor::Purple
+    }
+}
+
 #[derive(Debug, Eq, PartialEq, EnumString, AsRefStr, AsStaticStr, IntoStaticStr)]
 enum Color {
     #[strum(to_string = "RedRed")]
@@ -15,6 +28,8 @@ enum Color {
     Yellow,
     #[strum(default)]
     Green(String),
+    #[strum(transparent)]
+    Inner(InnerColor),
 }
 
 #[test]
@@ -41,6 +56,11 @@ fn as_green_str() {
     let _: &'static str = (Color::Green(String::default())).as_static();
 }
 
+#[test]
+fn as_fuchsia_str() {
+    assert_eq!("Purple", (Color::Inner(InnerColor::Purple)).as_ref());
+}
+
 #[derive(IntoStaticStr)]
 enum Foo<'a> {
     A,
@@ -73,6 +93,10 @@ fn test_into_static_str() {
     assert_eq!("RedRed", <&'static str>::from(&Color::Red));
     assert_eq!("blue", <&'static str>::from(&Color::Blue { hue: 0 }));
     assert_eq!("yellow", <&'static str>::from(&Color::Yellow));
+    assert_eq!(
+        "Purple",
+        <&'static str>::from(&Color::Inner(InnerColor::Purple))
+    );
 
     assert_eq!("A", <&'static str>::from(Foo::A));
     assert_eq!("C", <&'static str>::from(Foo::C(&17)));
diff --git a/strum_tests/tests/display.rs b/strum_tests/tests/display.rs
index c3b5041..fe3d069 100644
--- a/strum_tests/tests/display.rs
+++ b/strum_tests/tests/display.rs
@@ -2,6 +2,18 @@ use strum::EnumString;
 
 mod core {} // ensure macros call `::core`
 
+#[derive(Debug, Eq, PartialEq, EnumString, strum::Display)]
+enum InnerColor {
+    Violet,
+    Fuchsia,
+}
+
+impl Default for InnerColor {
+    fn default() -> Self {
+        InnerColor::Violet
+    }
+}
+
 #[derive(Debug, Eq, PartialEq, EnumString, strum::Display)]
 enum Color {
     #[strum(to_string = "RedRed")]
@@ -14,6 +26,8 @@ enum Color {
     Purple { sat: usize },
     #[strum(default)]
     Green(String),
+    #[strum(transparent)]
+    Inner(InnerColor),
 }
 
 #[test]
@@ -64,6 +78,14 @@ fn to_green_string() {
     );
 }
 
+#[test]
+fn to_violet_string() {
+    assert_eq!(
+        String::from("Violet"),
+        format!("{}", Color::Inner(InnerColor::Violet))
+    );
+}
+
 #[derive(Debug, Eq, PartialEq, EnumString, strum::Display)]
 enum ColorWithDefaultAndToString {
     #[strum(default, to_string = "GreenGreen")]
@@ -119,3 +141,21 @@ fn non_string_default_to_string() {
         format!("{:04}", NonStringDefault::Number(14))
     );
 }
+
+#[derive(strum::Display, Debug, Eq, PartialEq)]
+#[strum(serialize_all = "snake_case")]
+enum TransparentString {
+    #[strum(transparent)]
+    Something(String),
+}
+
+#[test]
+fn transparent_string() {
+    assert_eq!(
+        String::from("string in here"),
+        format!(
+            "{}",
+            TransparentString::Something("string in here".to_owned())
+        )
+    );
+}