Repository: Peternator7/strum
Pull request #345 (2024-03-11 10:33:39)

Interpolate unnamed enum variant fields in to_string attribute
I'm thinking it would also make sense to emit an error, or at least a warning, if `{}`-brackets are found in the `to_string`-literal on unit variants. I'd be happy to add that.

Peternator7: Looks great! Only change is let's error on the empy `{}`. In the example from the PR: `"hue is {1}, saturation is {}"`, mixing them feels bad to me so let's just not allow it by disallowing the empty braces
gin-ahirsch: I was going for an equivalency to how `print!()` works. I agree mixing them looks bad, in the example I just wanted to show either syntax works.
I would think very commonly unnamed enum fields only have a single member anyways.
But if you're against it on principle I'll remove support for empty `{}`.
gin-ahirsch: What about disallowing `{}` on unit variants? Currently that will format "as-is", i.e. an `on_string="{}"` is like `format!("{{}}")` on a unit variant.

Patch:
diff --git a/README.md b/README.md
index 4099439..04b37fc 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,6 @@ Strumming is also a very whimsical motion, much like writing Rust code.
 [EnumDiscriminants]: https://docs.rs/strum_macros/latest/strum_macros/derive.EnumDiscriminants.html
 [EnumCount]: https://docs.rs/strum_macros/latest/strum_macros/derive.EnumCount.html
 [FromRepr]: https://docs.rs/strum_macros/latest/strum_macros/derive.FromRepr.html
-[VariantArray]: https://docs.rs/strum_macros/latest/strum_macros/derive.StaticVariantsArray.html
-[VariantNames]: https://docs.rs/strum_macros/latest/strum_macros/derive.EnumVariantNames.html
+[VariantArray]: https://docs.rs/strum_macros/latest/strum_macros/derive.VariantArray.html
+[VariantNames]: https://docs.rs/strum_macros/latest/strum_macros/derive.VariantNames.html
 [EnumTable]: https://docs.rs/strum_macros/latest/strum_macros/derive.EnumTable.html
diff --git a/strum/src/additional_attributes.rs b/strum/src/additional_attributes.rs
index a29f1d6..3971ba1 100644
--- a/strum/src/additional_attributes.rs
+++ b/strum/src/additional_attributes.rs
@@ -78,7 +78,7 @@
 //!
 //! - `ascii_case_insensitive`: makes the comparison to this variant case insensitive (ASCII only).
 //!   If the whole enum is marked `ascii_case_insensitive`, you can specify `ascii_case_insensitive = false`
-//!   to disable case insensitivity on this v ariant.
+//!   to disable case insensitivity on this variant.
 //!
 //! - `message=".."`: Adds a message to enum variant. This is used in conjunction with the `EnumMessage`
 //!    trait to associate a message with a variant. If `detailed_message` is not provided,
diff --git a/strum_macros/Cargo.toml b/strum_macros/Cargo.toml
index 5f36880..16a52c7 100644
--- a/strum_macros/Cargo.toml
+++ b/strum_macros/Cargo.toml
@@ -19,7 +19,7 @@ proc-macro = true
 name = "strum_macros"
 
 [dependencies]
-heck = "0.4.1"
+heck = "0.5.0"
 proc-macro2 = "1.0"
 quote = "1.0"
 rustversion = "1.0"
diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs
index b337fb3..f9282b2 100644
--- a/strum_macros/src/lib.rs
+++ b/strum_macros/src/lib.rs
@@ -367,7 +367,7 @@ pub fn to_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 /// 3. The name of the variant will be used if there are no `serialize` or `to_string` attributes.
 /// 4. If the enum has a `strum(prefix = "some_value_")`, every variant will have that prefix prepended
 ///    to the serialization.
-/// 5. Enums with named fields support named field interpolation. The value will be interpolated into the output string.
+/// 5. Enums with fields support string interpolation.
 ///    Note this means the variant will not "round trip" if you then deserialize the string.
 ///
 ///    ```rust
@@ -375,6 +375,8 @@ pub fn to_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 ///    pub enum Color {
 ///        #[strum(to_string = "saturation is {sat}")]
 ///        Red { sat: usize },
+///        #[strum(to_string = "hue is {1}, saturation is {0}")]
+///        Blue(usize, usize),
 ///    }
 ///    ```
 ///
@@ -588,7 +590,7 @@ pub fn enum_table(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 /// 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, except where an
 /// explicit discriminant is specified. The type of the discriminant will match the `repr` type if
-/// it is specifed.
+/// it is specified.
 ///
 /// 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
@@ -668,7 +670,7 @@ pub fn from_repr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 ///
 /// Encode strings into the enum itself. The `strum_macros::EmumMessage` macro implements the `strum::EnumMessage` trait.
 /// `EnumMessage` looks for `#[strum(message="...")]` attributes on your variants.
-/// You can also provided a `detailed_message="..."` attribute to create a seperate more detailed message than the first.
+/// You can also provided a `detailed_message="..."` attribute to create a separate more detailed message than the first.
 ///
 /// `EnumMessage` also exposes the variants doc comments through `get_documentation()`. This is useful in some scenarios,
 /// but `get_message` should generally be preferred. Rust doc comments are intended for developer facing documentation,
@@ -754,7 +756,7 @@ pub fn enum_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream
 
 /// Add custom properties to enum variants.
 ///
-/// Enables the encoding of arbitary constants into enum variants. This method
+/// Enables the encoding of arbitrary constants into enum variants. This method
 /// currently only supports adding additional string values. Other types of literals are still
 /// experimental in the rustc compiler. The generated code works by nesting match statements.
 /// The first match statement matches on the type of the enum, and the inner match statement
diff --git a/strum_macros/src/macros/strings/display.rs b/strum_macros/src/macros/strings/display.rs
index a2f6f24..650e5b2 100644
--- a/strum_macros/src/macros/strings/display.rs
+++ b/strum_macros/src/macros/strings/display.rs
@@ -29,7 +29,20 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
 
         let params = match variant.fields {
             Fields::Unit => quote! {},
-            Fields::Unnamed(..) => quote! { (..) },
+            Fields::Unnamed(ref unnamed_fields) => {
+                // Transform unnamed params '(String, u8)' to '(ref field0, ref field1)'
+                let names: Punctuated<_, Token!(,)> = unnamed_fields
+                    .unnamed
+                    .iter()
+                    .enumerate()
+                    .map(|(index, field)| {
+                        assert!(field.ident.is_none());
+                        let ident = syn::parse_str::<Ident>(format!("field{}", index).as_str()).unwrap();
+                        quote! { ref #ident }
+                    })
+                    .collect();
+                quote! { (#names) }
+            }
             Fields::Named(ref field_names) => {
                 // Transform named params '{ name: String, age: u8 }' to '{ ref name, ref age }'
                 let names: Punctuated<TokenStream, Token!(,)> = field_names
@@ -58,33 +71,60 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
                 }
             }
         } else {
-            let arm = if let Fields::Named(ref field_names) = variant.fields {
-                let used_vars = capture_format_string_idents(&output)?;
-                if used_vars.is_empty() {
-                    quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) }
-                } else {
-                    // Create args like 'name = name, age = age' for format macro
-                    let args: Punctuated<_, Token!(,)> = field_names
-                        .named
-                        .iter()
-                        .filter_map(|field| {
-                            let ident = field.ident.as_ref().unwrap();
-                            // Only contain variables that are used in format string
-                            if !used_vars.contains(ident) {
-                                None
-                            } else {
-                                Some(quote! { #ident = #ident })
-                            }
-                        })
-                        .collect();
-
-                    quote! {
-                        #[allow(unused_variables)]
-                        #name::#ident #params => ::core::fmt::Display::fmt(&format!(#output, #args), f)
+            let arm = match variant.fields {
+                Fields::Named(ref field_names) => {
+                    let used_vars = capture_format_string_idents(&output)?;
+                    if used_vars.is_empty() {
+                        quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) }
+                    } else {
+                        // Create args like 'name = name, age = age' for format macro
+                        let args: Punctuated<_, Token!(,)> = field_names
+                            .named
+                            .iter()
+                            .filter_map(|field| {
+                                let ident = field.ident.as_ref().unwrap();
+                                // Only contain variables that are used in format string
+                                if !used_vars.contains(ident) {
+                                    None
+                                } else {
+                                    Some(quote! { #ident = #ident })
+                                }
+                            })
+                            .collect();
+
+                        quote! {
+                            #[allow(unused_variables)]
+                            #name::#ident #params => ::core::fmt::Display::fmt(&format!(#output, #args), f)
+                        }
+                    }
+                },
+                Fields::Unnamed(ref unnamed_fields) => {
+                    let used_vars = capture_format_strings(&output)?;
+                    if used_vars.iter().any(String::is_empty) {
+                        return Err(syn::Error::new_spanned(
+                            &output,
+                            "Empty {} is not allowed; Use manual numbering ({0})",
+                        ))
+                    }
+                    if used_vars.is_empty() {
+                        quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) }
+                    } else {
+                        let args: Punctuated<_, Token!(,)> = unnamed_fields
+                            .unnamed
+                            .iter()
+                            .enumerate()
+                            .map(|(index, field)| {
+                                assert!(field.ident.is_none());
+                                syn::parse_str::<Ident>(format!("field{}", index).as_str()).unwrap()
+                            })
+                            .collect();
+                        quote! {
+                            #[allow(unused_variables)]
+                            #name::#ident #params => ::core::fmt::Display::fmt(&format!(#output, #args), f)
+                        }
                     }
                 }
-            } else {
-                quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) }
+                Fields::Unit => quote! { #name::#ident #params => ::core::fmt::Display::fmt(&format!(#output), f) }
             };
 
             arms.push(arm);
@@ -107,11 +147,22 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result<TokenStream> {
 }
 
 fn capture_format_string_idents(string_literal: &LitStr) -> syn::Result<Vec<Ident>> {
+    capture_format_strings(string_literal)?.into_iter().map(|ident| {
+        syn::parse_str::<Ident>(ident.as_str()).map_err(|_| {
+            syn::Error::new_spanned(
+                string_literal,
+                "Invalid identifier inside format string bracket",
+            )
+        })
+    }).collect()
+}
+
+fn capture_format_strings(string_literal: &LitStr) -> syn::Result<Vec<String>> {
     // Remove escaped brackets
     let format_str = string_literal.value().replace("{{", "").replace("}}", "");
 
     let mut new_var_start_index: Option<usize> = None;
-    let mut var_used: Vec<Ident> = Vec::new();
+    let mut var_used = Vec::new();
 
     for (i, chr) in format_str.bytes().enumerate() {
         if chr == b'{' {
@@ -132,14 +183,8 @@ fn capture_format_string_idents(string_literal: &LitStr) -> syn::Result<Vec<Iden
             ))?;
 
             let inside_brackets = &format_str[start_index + 1..i];
-            let ident_str = inside_brackets.split(":").next().unwrap();
-            let ident = syn::parse_str::<Ident>(ident_str).map_err(|_| {
-                syn::Error::new_spanned(
-                    string_literal,
-                    "Invalid identifier inside format string bracket",
-                )
-            })?;
-            var_used.push(ident);
+            let ident_str = inside_brackets.split(":").next().unwrap().trim_end();
+            var_used.push(ident_str.to_owned());
         }
     }
 


Test patch:
diff --git a/strum_tests/tests/display.rs b/strum_tests/tests/display.rs
index c3b5041..4c38d78 100644
--- a/strum_tests/tests/display.rs
+++ b/strum_tests/tests/display.rs
@@ -14,6 +14,8 @@ enum Color {
     Purple { sat: usize },
     #[strum(default)]
     Green(String),
+    #[strum(to_string = "Orange({0})")]
+    Orange(usize),
 }
 
 #[test]
@@ -64,6 +66,14 @@ fn to_green_string() {
     );
 }
 
+#[test]
+fn to_orange_string() {
+    assert_eq!(
+        String::from("Orange(10)"),
+        Color::Orange(10).to_string().as_ref()
+    );
+}
+
 #[derive(Debug, Eq, PartialEq, EnumString, strum::Display)]
 enum ColorWithDefaultAndToString {
     #[strum(default, to_string = "GreenGreen")]