Repository: Peternator7/strum
Pull request #153 (2021-03-03 08:10:02)

Added a pass-through attribute support to EnumDiscriminants
The use-cases:

* I wanted to combine `EnumMessage` with `EnumDiscriminants` and found that not all the attributes are passed
* `#[serde]` attributes on discriminant variants could also be useful

Peternator7: This makes tons of sense to me! Thanks for the PR :) Could you add a few tests in [this file](https://github.com/Peternator7/strum/blob/master/strum_tests/tests/enum_discriminants.rs)

Patch:
diff --git a/strum_macros/Cargo.toml b/strum_macros/Cargo.toml
index 5eb2308..fb9e72c 100644
--- a/strum_macros/Cargo.toml
+++ b/strum_macros/Cargo.toml
@@ -26,17 +26,3 @@ syn = { version = "1.0", features = ["parsing", "extra-traits"] }
 
 [dev-dependencies]
 strum = "0.20"
-
-[features]
-verbose-enumstring-name = []
-verbose-asrefstr-name = []
-verbose-variant-names = []
-verbose-asstaticstr-name = []
-verbose-intostaticstr-name = []
-verbose-tostring-name = []
-verbose-display-name = []
-verbose-enumiter-name = []
-verbose-enummessage-name = []
-verbose-enumproperty-name = []
-verbose-enumdiscriminants-name = []
-verbose-enumcount-name = []
diff --git a/strum_macros/src/helpers/mod.rs b/strum_macros/src/helpers/mod.rs
index 77049eb..30787b2 100644
--- a/strum_macros/src/helpers/mod.rs
+++ b/strum_macros/src/helpers/mod.rs
@@ -9,11 +9,19 @@ pub mod variant_props;
 
 use proc_macro2::Span;
 use quote::ToTokens;
+use syn::spanned::Spanned;
 
 pub fn non_enum_error() -> syn::Error {
     syn::Error::new(Span::call_site(), "This macro only supports enums.")
 }
 
+pub fn strum_discriminants_passthrough_error(span: impl Spanned) -> syn::Error {
+    syn::Error::new(
+        span.span(),
+        "expected a pass-through attribute, e.g. #[strum_discriminants(serde(rename = \"var0\"))]",
+    )
+}
+
 pub fn occurrence_error<T: ToTokens>(fst: T, snd: T, attr: &str) -> syn::Error {
     let mut e = syn::Error::new_spanned(
         snd,
diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs
index 2c94a21..4bd6fb1 100644
--- a/strum_macros/src/lib.rs
+++ b/strum_macros/src/lib.rs
@@ -96,14 +96,7 @@ fn debug_print_generated(ast: &DeriveInput, toks: &TokenStream) {
 /// // however the variant is still normally usable
 /// println!("{:?}", Color::Yellow);
 /// ```
-#[cfg_attr(
-    not(feature = "verbose-enumstring-name"),
-    proc_macro_derive(EnumString, attributes(strum))
-)]
-#[cfg_attr(
-    feature = "verbose-enumstring-name",
-    proc_macro_derive(StrumEnumString, attributes(strum))
-)]
+#[proc_macro_derive(EnumString, attributes(strum))]
 pub fn from_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -148,14 +141,7 @@ pub fn from_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 ///     Color::Green { range: 42 }.as_ref()
 /// );
 /// ```
-#[cfg_attr(
-    not(feature = "verbose-asrefstr-name"),
-    proc_macro_derive(AsRefStr, attributes(strum))
-)]
-#[cfg_attr(
-    feature = "verbose-asrefstr-name",
-    proc_macro_derive(StrumAsRefStr, attributes(strum))
-)]
+#[proc_macro_derive(AsRefStr, attributes(strum))]
 pub fn as_ref_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -186,14 +172,7 @@ pub fn as_ref_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 /// }
 /// assert_eq!(["red", "blue", "yellow", "rebecca-purple"], Color::VARIANTS);
 /// ```
-#[cfg_attr(
-    not(feature = "verbose-variant-names"),
-    proc_macro_derive(EnumVariantNames, attributes(strum))
-)]
-#[cfg_attr(
-    feature = "verbose-variant-names",
-    proc_macro_derive(StrumEnumVariantNames, attributes(strum))
-)]
+#[proc_macro_derive(EnumVariantNames, attributes(strum))]
 pub fn variant_names(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -203,14 +182,7 @@ pub fn variant_names(input: proc_macro::TokenStream) -> proc_macro::TokenStream
     toks.into()
 }
 
-#[cfg_attr(
-    feature = "verbose-asstaticstr-name",
-    proc_macro_derive(StrumAsStaticStr, attributes(strum))
-)]
-#[cfg_attr(
-    not(feature = "verbose-asstaticstr-name"),
-    proc_macro_derive(AsStaticStr, attributes(strum))
-)]
+#[proc_macro_derive(AsStaticStr, attributes(strum))]
 pub fn as_static_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -252,14 +224,7 @@ pub fn as_static_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream
 ///
 /// verify_state(&"hello world".to_string());
 /// ```
-#[cfg_attr(
-    feature = "verbose-intostaticstr-name",
-    proc_macro_derive(StrumIntoStaticStr, attributes(strum))
-)]
-#[cfg_attr(
-    not(feature = "verbose-intostaticstr-name"),
-    proc_macro_derive(IntoStaticStr, attributes(strum))
-)]
+#[proc_macro_derive(IntoStaticStr, attributes(strum))]
 pub fn into_static_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -297,14 +262,7 @@ pub fn into_static_str(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
 /// let yellow = Color::Yellow;
 /// assert_eq!(String::from("Yellow"), yellow.to_string());
 /// ```
-#[cfg_attr(
-    feature = "verbose-tostring-name",
-    proc_macro_derive(StrumToString, attributes(strum))
-)]
-#[cfg_attr(
-    not(feature = "verbose-tostring-name"),
-    proc_macro_derive(ToString, attributes(strum))
-)]
+#[proc_macro_derive(ToString, attributes(strum))]
 pub fn to_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -354,14 +312,7 @@ pub fn to_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 ///     Color::Green { range: 42 }
 /// );
 /// ```
-#[cfg_attr(
-    feature = "verbose-display-name",
-    proc_macro_derive(StrumDisplay, attributes(strum))
-)]
-#[cfg_attr(
-    not(feature = "verbose-display-name"),
-    proc_macro_derive(Display, attributes(strum))
-)]
+#[proc_macro_derive(Display, attributes(strum))]
 pub fn display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -403,14 +354,7 @@ pub fn display(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 /// assert_eq!(Some(Color::Yellow), ci.next());
 /// assert_eq!(None, ci.next());
 /// ```
-#[cfg_attr(
-    feature = "verbose-enumiter-name",
-    proc_macro_derive(StrumEnumIter, attributes(strum))
-)]
-#[cfg_attr(
-    not(feature = "verbose-enumiter-name"),
-    proc_macro_derive(EnumIter, attributes(strum))
-)]
+#[proc_macro_derive(EnumIter, attributes(strum))]
 pub fn enum_iter(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -484,14 +428,7 @@ pub fn enum_iter(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 /// assert_eq!("This is very red", c.get_detailed_message().unwrap());
 /// assert_eq!(["Red"], c.get_serializations());
 /// ```
-#[cfg_attr(
-    feature = "verbose-enummessage-name",
-    proc_macro_derive(StrumEnumMessage, attributes(strum))
-)]
-#[cfg_attr(
-    not(feature = "verbose-enummessage-name"),
-    proc_macro_derive(EnumMessage, attributes(strum))
-)]
+#[proc_macro_derive(EnumMessage, attributes(strum))]
 pub fn enum_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -543,14 +480,7 @@ pub fn enum_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream
 /// assert_eq!("My color is Red. It\'s RGB is 255,0,0", &display);
 /// ```
 
-#[cfg_attr(
-    feature = "verbose-enumproperty-name",
-    proc_macro_derive(StrumEnumProperty, attributes(strum))
-)]
-#[cfg_attr(
-    not(feature = "verbose-enumproperty-name"),
-    proc_macro_derive(EnumProperty, attributes(strum))
-)]
+#[proc_macro_derive(EnumProperty, attributes(strum))]
 pub fn enum_properties(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -572,11 +502,16 @@ pub fn enum_properties(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
 /// You can add additional derives using the `#[strum_discriminants(derive(AdditionalDerive))]`
 /// attribute.
 ///
+/// Note, the variant attributes passed to the discriminant enum are filtered to avoid compilation
+/// errors due to the derives mismatches, thus only `#[doc]`, `#[cfg]`, `#[allow]`, and `#[deny]`
+/// are passed through by default. If you want to specify a custom attribute on the discriminant
+/// variant, wrap it with `#[strum_discriminants(...)]` attribute.
+///
 /// ```
 /// // Bring trait into scope
 /// use std::str::FromStr;
-/// use strum::IntoEnumIterator;
-/// use strum_macros::{EnumDiscriminants, EnumIter, EnumString};
+/// use strum::{IntoEnumIterator, EnumMessage};
+/// use strum_macros::{EnumDiscriminants, EnumIter, EnumString, EnumMessage};
 ///
 /// #[derive(Debug)]
 /// struct NonDefault;
@@ -584,8 +519,9 @@ pub fn enum_properties(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
 /// // simple example
 /// # #[allow(dead_code)]
 /// #[derive(Debug, EnumDiscriminants)]
-/// #[strum_discriminants(derive(EnumString))]
+/// #[strum_discriminants(derive(EnumString, EnumMessage))]
 /// enum MyEnum {
+///     #[strum_discriminants(strum(message = "Variant zero"))]
 ///     Variant0(NonDefault),
 ///     Variant1 { a: NonDefault },
 /// }
@@ -621,6 +557,12 @@ pub fn enum_properties(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
 ///     MyEnumDiscriminants::Variant0,
 ///     MyEnumDiscriminants::from(MyEnum::Variant0(NonDefault))
 /// );
+///
+/// // Make use of the EnumMessage on the `MyEnumDiscriminants` discriminant.
+/// assert_eq!(
+///     MyEnumDiscriminants::Variant0.get_message(),
+///     Some("Variant zero")
+/// );
 /// ```
 ///
 /// It is also possible to specify the visibility (e.g. `pub`/`pub(crate)`/etc.)
@@ -650,14 +592,7 @@ pub fn enum_properties(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
 ///     inner::PubDiscriminants::Variant1,
 /// );
 /// ```
-#[cfg_attr(
-    feature = "verbose-enumdiscriminants-name",
-    proc_macro_derive(StrumEnumDiscriminants, attributes(strum, strum_discriminants))
-)]
-#[cfg_attr(
-    not(feature = "verbose-enumdiscriminants-name"),
-    proc_macro_derive(EnumDiscriminants, attributes(strum, strum_discriminants))
-)]
+#[proc_macro_derive(EnumDiscriminants, attributes(strum, strum_discriminants))]
 pub fn enum_discriminants(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
 
@@ -691,14 +626,7 @@ pub fn enum_discriminants(input: proc_macro::TokenStream) -> proc_macro::TokenSt
 /// assert_eq!(Week::iter().count(), Week::COUNT);
 ///
 /// ```
-#[cfg_attr(
-    feature = "verbose-enumcount-name",
-    proc_macro_derive(StrumEnumCount, attributes(strum))
-)]
-#[cfg_attr(
-    not(feature = "verbose-enumcount-name"),
-    proc_macro_derive(EnumCount, attributes(strum))
-)]
+#[proc_macro_derive(EnumCount, attributes(strum))]
 pub fn enum_count(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     let ast = syn::parse_macro_input!(input as DeriveInput);
     let toks =
diff --git a/strum_macros/src/macros/enum_discriminants.rs b/strum_macros/src/macros/enum_discriminants.rs
index 129c142..e4b9af2 100644
--- a/strum_macros/src/macros/enum_discriminants.rs
+++ b/strum_macros/src/macros/enum_discriminants.rs
@@ -1,15 +1,15 @@
-use proc_macro2::{Span, TokenStream};
-use quote::quote;
+use proc_macro2::{Span, TokenStream, TokenTree};
+use quote::{quote, ToTokens};
 use syn::parse_quote;
 use syn::{Data, DeriveInput};
 
-use crate::helpers::{non_enum_error, HasTypeProperties};
+use crate::helpers::{non_enum_error, strum_discriminants_passthrough_error, HasTypeProperties};
 
 /// Attributes to copy from the main enum's variants to the discriminant enum's variants.
 ///
 /// Attributes not in this list may be for other `proc_macro`s on the main enum, and may cause
 /// compilation problems when copied across.
-const ATTRIBUTES_TO_COPY: &[&str] = &["doc", "cfg", "allow", "deny"];
+const ATTRIBUTES_TO_COPY: &[&str] = &["doc", "cfg", "allow", "deny", "strum_discriminants"];
 
 pub fn enum_discriminants_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
     let name = &ast.ident;
@@ -36,7 +36,9 @@ pub fn enum_discriminants_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
     );
 
     let discriminants_name = type_properties.discriminant_name.unwrap_or(default_name);
-    let discriminants_vis = type_properties.discriminant_vis.unwrap_or_else(|| vis.clone());
+    let discriminants_vis = type_properties
+        .discriminant_vis
+        .unwrap_or_else(|| vis.clone());
 
     // Pass through all other attributes
     let pass_though_attributes = type_properties.discriminant_others;
@@ -46,12 +48,39 @@ pub fn enum_discriminants_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
     for variant in variants {
         let ident = &variant.ident;
 
-        // Don't copy across the "strum" meta attribute.
-        let attrs = variant.attrs.iter().filter(|attr| {
-            ATTRIBUTES_TO_COPY
-                .iter()
-                .any(|attr_whitelisted| attr.path.is_ident(attr_whitelisted))
-        });
+        // Don't copy across the "strum" meta attribute. Only passthrough the whitelisted
+        // attributes and proxy `#[strum_discriminants(...)]` attributes
+        let attrs = variant
+            .attrs
+            .iter()
+            .filter(|attr| {
+                ATTRIBUTES_TO_COPY
+                    .iter()
+                    .any(|attr_whitelisted| attr.path.is_ident(attr_whitelisted))
+            })
+            .map(|attr| {
+                if attr.path.is_ident("strum_discriminants") {
+                    let passthrough_group = attr
+                        .tokens
+                        .clone()
+                        .into_iter()
+                        .next()
+                        .ok_or_else(|| strum_discriminants_passthrough_error(attr))?;
+                    let passthrough_attribute = match passthrough_group {
+                        TokenTree::Group(ref group) => group.stream(),
+                        _ => {
+                            return Err(strum_discriminants_passthrough_error(passthrough_group));
+                        }
+                    };
+                    if passthrough_attribute.is_empty() {
+                        return Err(strum_discriminants_passthrough_error(passthrough_group));
+                    }
+                    Ok(quote! { #[#passthrough_attribute] })
+                } else {
+                    Ok(attr.to_token_stream())
+                }
+            })
+            .collect::<Result<Vec<_>, _>>()?;
 
         discriminants.push(quote! { #(#attrs)* #ident });
     }


Test patch:
diff --git a/strum_tests/tests/enum_discriminants.rs b/strum_tests/tests/enum_discriminants.rs
index 0bdd69b..69026ba 100644
--- a/strum_tests/tests/enum_discriminants.rs
+++ b/strum_tests/tests/enum_discriminants.rs
@@ -1,5 +1,5 @@
 use enum_variant_type::EnumVariantType;
-use strum::{Display, EnumDiscriminants, EnumIter, EnumString, IntoEnumIterator};
+use strum::{Display, EnumDiscriminants, EnumIter, EnumMessage, EnumString, IntoEnumIterator};
 
 #[allow(dead_code)]
 #[derive(Debug, Eq, PartialEq, EnumDiscriminants)]
@@ -82,6 +82,28 @@ fn with_default_test() {
     assert!(WithDefaultDiscriminants::A != WithDefaultDiscriminants::B);
 }
 
+// This test exists to ensure that we can pass attributes to the discriminant variants.
+#[allow(dead_code)]
+#[derive(Debug, EnumDiscriminants)]
+#[strum_discriminants(derive(EnumMessage))]
+enum WithPassthroughAttributes {
+    #[strum_discriminants(strum(message = "AAA"))]
+    A(String),
+    B,
+}
+
+#[test]
+fn with_passthrough_attributes_test() {
+    assert_eq!(
+        WithPassthroughAttributesDiscriminants::A.get_message(),
+        Some("AAA")
+    );
+    assert_eq!(
+        WithPassthroughAttributesDiscriminants::B.get_message(),
+        None
+    );
+}
+
 #[allow(dead_code)]
 #[derive(Debug, Eq, PartialEq, EnumDiscriminants)]
 #[strum_discriminants(name(EnumBoo), derive(EnumIter))]
diff --git a/strum_tests_rename/Cargo.toml b/strum_tests_rename/Cargo.toml
deleted file mode 100644
index 5d47794..0000000
--- a/strum_tests_rename/Cargo.toml
+++ /dev/null
@@ -1,24 +0,0 @@
-[package]
-name = "strum_tests_rename"
-version = "0.20.0"
-edition = "2018"
-authors = ["Peter Glotfelty <peglotfe@microsoft.com>"]
-
-[dependencies]
-strum = { path = "../strum", features = ["derive"] }
-
-[dependencies.strum_macros]
-path = "../strum_macros"
-features = [
-    "verbose-enumcount-name",
-    "verbose-enumstring-name",
-    "verbose-asrefstr-name",
-    "verbose-asstaticstr-name",
-    "verbose-intostaticstr-name",
-    "verbose-tostring-name",
-    "verbose-display-name",
-    "verbose-enumiter-name",
-    "verbose-enummessage-name",
-    "verbose-enumproperty-name",
-    "verbose-enumdiscriminants-name"
-]
diff --git a/strum_tests_rename/src/main.rs b/strum_tests_rename/src/main.rs
deleted file mode 100644
index e7a11a9..0000000
--- a/strum_tests_rename/src/main.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-fn main() {
-    println!("Hello, world!");
-}
diff --git a/strum_tests_rename/tests/color.rs b/strum_tests_rename/tests/color.rs
deleted file mode 100644
index 0db926c..0000000
--- a/strum_tests_rename/tests/color.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use strum::*;
-
-#[derive(
-    StrumEnumString,
-    StrumAsRefStr,
-    StrumAsStaticStr,
-    StrumIntoStaticStr,
-    StrumDisplay,
-    StrumEnumIter,
-    StrumEnumMessage,
-    StrumEnumProperty,
-    StrumEnumDiscriminants,
-    StrumEnumCount,
-)]
-pub enum Color {
-    Red,
-    Blue,
-    Green,
-}
-
-// You can't have ToString and Display on the same type.
-#[derive(StrumToString)]
-pub enum Color2 {
-    Red,
-    Blue,
-    Green,
-}