From 186d29f6a1691b6356a2d85d66f8904b9a5d3140 Mon Sep 17 00:00:00 2001 From: gin-ahirsch <38940744+gin-ahirsch@users.noreply.github.com> Date: Sun, 5 May 2024 03:02:46 +0200 Subject: [PATCH] Interpolate unnamed enum variant fields in to_string attribute (#345) * Trim spaces at the end of format capture identifiers format! allows trailing spaces. * Interpolate unnamed enum variant fields in to_string attribute * Reject "{}" in unit-variants in enums Strings for unit-variants in enums were taken verbatim and not passed to format!() since there's no associated values that *could* be formatted. For consistency with non-unit variants these now have to use the escaped form "{{}}", otherwise an error is produced for the format string. --- strum_macros/src/lib.rs | 4 +- strum_macros/src/macros/strings/display.rs | 115 ++++++++++++++------- strum_tests/tests/display.rs | 10 ++ 3 files changed, 93 insertions(+), 36 deletions(-) diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs index 089dfc12..f9282b24 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), /// } /// ``` /// diff --git a/strum_macros/src/macros/strings/display.rs b/strum_macros/src/macros/strings/display.rs index a2f6f24a..650e5b2b 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 { 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::(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 = field_names @@ -58,33 +71,60 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result { } } } 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::(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 { } fn capture_format_string_idents(string_literal: &LitStr) -> syn::Result> { + capture_format_strings(string_literal)?.into_iter().map(|ident| { + syn::parse_str::(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> { // Remove escaped brackets let format_str = string_literal.value().replace("{{", "").replace("}}", ""); let mut new_var_start_index: Option = None; - let mut var_used: Vec = 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(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()); } } diff --git a/strum_tests/tests/display.rs b/strum_tests/tests/display.rs index c3b5041d..4c38d784 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")]