diff --git a/strum_macros/src/lib.rs b/strum_macros/src/lib.rs index 2e33fde9..dd166d0d 100644 --- a/strum_macros/src/lib.rs +++ b/strum_macros/src/lib.rs @@ -317,6 +317,10 @@ pub fn to_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// }, /// Blue(usize), /// Yellow, +/// #[strum(to_string = "purple with {sat} saturation")] +/// Purple { +/// sat: usize, +/// }, /// } /// /// // uses the serialize string for Display @@ -331,6 +335,9 @@ pub fn to_string(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// Color::Blue(10), /// Color::Green { range: 42 } /// ); +/// // you can also use named fields in message +/// let purple = Color::Purple { sat: 10 }; +/// assert_eq!(String::from("purple with 10 saturation"), purple.to_string()); /// ``` #[proc_macro_derive(Display, attributes(strum))] pub fn display(input: proc_macro::TokenStream) -> proc_macro::TokenStream { diff --git a/strum_macros/src/macros/strings/display.rs b/strum_macros/src/macros/strings/display.rs index 2fd0eef7..34cdcddf 100644 --- a/strum_macros/src/macros/strings/display.rs +++ b/strum_macros/src/macros/strings/display.rs @@ -1,6 +1,6 @@ -use proc_macro2::TokenStream; +use proc_macro2::{Ident, TokenStream}; use quote::quote; -use syn::{Data, DeriveInput, Fields, LitStr}; +use syn::{punctuated::Punctuated, Data, DeriveInput, Fields, LitStr, Token}; use crate::helpers::{non_enum_error, HasStrumVariantProperties, HasTypeProperties}; @@ -32,7 +32,19 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result { let params = match variant.fields { Fields::Unit => quote! {}, Fields::Unnamed(..) => quote! { (..) }, - Fields::Named(..) => quote! { {..} }, + Fields::Named(ref field_names) => { + // Transform named params '{ name: String, age: u8 }' to '{ ref name, ref age }' + let names: Punctuated = field_names + .named + .iter() + .map(|field| { + let ident = field.ident.as_ref().unwrap(); + quote! { ref #ident } + }) + .collect(); + + quote! { {#names} } + } }; if variant_properties.to_string.is_none() && variant_properties.default.is_some() { @@ -48,7 +60,36 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result { } } } else { - arms.push(quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) } ); + 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 => f.pad(&format!(#output, #args)) + } + } + } else { + quote! { #name::#ident #params => ::core::fmt::Display::fmt(#output, f) } + }; + + arms.push(arm); } } @@ -66,3 +107,43 @@ pub fn display_inner(ast: &DeriveInput) -> syn::Result { } }) } + +fn capture_format_string_idents(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(); + + for (i, chr) in format_str.bytes().enumerate() { + if chr == b'{' { + if new_var_start_index.is_some() { + return Err(syn::Error::new_spanned( + string_literal, + "Bracket opened without closing previous bracket", + )); + } + new_var_start_index = Some(i); + continue; + } + + if chr == b'}' { + let start_index = new_var_start_index.take().ok_or(syn::Error::new_spanned( + string_literal, + "Bracket closed without previous opened bracket", + ))?; + + let inside_brackets = &format_str[start_index + 1..i]; + let ident_str = inside_brackets.split(":").next().unwrap(); + let ident = syn::parse_str::(ident_str).map_err(|_| { + syn::Error::new_spanned( + string_literal, + "Invalid identifier inside format string bracket", + ) + })?; + var_used.push(ident); + } + } + + Ok(var_used) +} diff --git a/strum_tests/tests/display.rs b/strum_tests/tests/display.rs index 77ea25c2..c3b5041d 100644 --- a/strum_tests/tests/display.rs +++ b/strum_tests/tests/display.rs @@ -10,6 +10,8 @@ enum Color { Blue { hue: usize }, #[strum(serialize = "y", serialize = "yellow")] Yellow, + #[strum(to_string = "saturation is {sat}")] + Purple { sat: usize }, #[strum(default)] Green(String), } @@ -41,6 +43,14 @@ fn to_yellow_string() { assert_eq!(String::from("yellow"), format!("{}", Color::Yellow)); } +#[test] +fn to_purple_string() { + assert_eq!( + String::from("saturation is 10"), + (Color::Purple { sat: 10 }).to_string().as_ref() + ); +} + #[test] fn to_red_string() { assert_eq!(String::from("RedRed"), format!("{}", Color::Red));