From 0113268c3f301b5e25da5b6833b0875e702b8410 Mon Sep 17 00:00:00 2001 From: jngls Date: Mon, 16 Jun 2025 21:16:51 +0100 Subject: [PATCH 1/6] Implement generics, lifetimes, bounds and where clause support --- enum-display-macro/src/lib.rs | 7 +++++-- src/lib.rs | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/enum-display-macro/src/lib.rs b/enum-display-macro/src/lib.rs index 5dc0c7b..e2803be 100644 --- a/enum-display-macro/src/lib.rs +++ b/enum-display-macro/src/lib.rs @@ -30,7 +30,7 @@ fn parse_case_name(case_name: &str) -> Case { pub fn derive(input: TokenStream) -> TokenStream { // Parse the input tokens into a syntax tree let DeriveInput { - ident, data, attrs, .. + ident, data, attrs, generics, .. } = parse_macro_input!(input); // Should we transform the case of the enum variants? @@ -82,13 +82,16 @@ pub fn derive(input: TokenStream) -> TokenStream { } }); + // Copy generics and bounds + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + // #[allow(unused_qualifications)] is needed // due to https://github.com/SeedyROM/enum-display/issues/1 // Possibly related to https://github.com/rust-lang/rust/issues/96698 let output = quote! { #[automatically_derived] #[allow(unused_qualifications)] - impl ::core::fmt::Display for #ident { + impl #impl_generics ::core::fmt::Display for #ident #ty_generics #where_clause { fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { ::core::fmt::Formatter::write_str( f, diff --git a/src/lib.rs b/src/lib.rs index 2014389..7ac4ebc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,19 @@ mod tests { DateOfBirth(u32, u32, u32), } + #[allow(dead_code)] + #[derive(EnumDisplay)] + enum TestEnumWithGenerics<'a, T: Clone> where T: std::fmt::Display { + Name, + Address { + street: &'a T, + city: &'a T, + state: &'a T, + zip: &'a T, + }, + DateOfBirth(u32, u32, u32), + } + #[test] fn test_unit_field_variant() { assert_eq!(TestEnum::Name.to_string(), "Name"); @@ -117,4 +130,28 @@ mod tests { "date-of-birth" ); } + + #[test] + fn test_unit_field_variant_with_generics() { + assert_eq!(TestEnumWithGenerics::<'_, String>::Name.to_string(), "Name"); + } + + #[test] + fn test_named_fields_variant_with_generics() { + assert_eq!( + TestEnumWithGenerics::Address { + street: &"123 Main St".to_string(), + city: &"Any Town".to_string(), + state: &"CA".to_string(), + zip: &"12345".to_string() + } + .to_string(), + "Address" + ); + } + + #[test] + fn test_unnamed_fields_variant_with_generics() { + assert_eq!(TestEnumWithGenerics::<'_, String>::DateOfBirth(1, 1, 2000).to_string(), "DateOfBirth"); + } } From 3be7bcfa3eb236e61bbe8304bc30682f7f1fb749 Mon Sep 17 00:00:00 2001 From: jngls Date: Thu, 19 Jun 2025 13:15:47 +0100 Subject: [PATCH 2/6] First pass formating --- enum-display-macro/Cargo.toml | 2 + enum-display-macro/src/lib.rs | 349 ++++++++++++++++++++++++++++------ src/lib.rs | 52 ++++- 3 files changed, 339 insertions(+), 64 deletions(-) diff --git a/enum-display-macro/Cargo.toml b/enum-display-macro/Cargo.toml index a2bf804..6e7fe89 100644 --- a/enum-display-macro/Cargo.toml +++ b/enum-display-macro/Cargo.toml @@ -12,9 +12,11 @@ repository = "https://github.com/SeedyROM/enum-display/tree/main/enum-display-de # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +proc-macro2 = "1.0.95" convert_case = "0.6.0" quote = "1.0.21" syn = { version = "1.0.101", features = ["full"] } +regex = "1.11.1" [lib] proc-macro = true diff --git a/enum-display-macro/src/lib.rs b/enum-display-macro/src/lib.rs index e2803be..1f47f60 100644 --- a/enum-display-macro/src/lib.rs +++ b/enum-display-macro/src/lib.rs @@ -1,90 +1,313 @@ +use std::{collections::HashSet, fmt::format}; + use convert_case::{Case, Casing}; use proc_macro::{self, TokenStream}; +use proc_macro2::Span; use quote::quote; -use syn::{parse_macro_input, DeriveInput}; - -fn parse_case_name(case_name: &str) -> Case { - match case_name { - "Upper" => Case::Upper, - "Lower" => Case::Lower, - "Title" => Case::Title, - "Toggle" => Case::Toggle, - "Camel" => Case::Camel, - "Pascal" => Case::Pascal, - "UpperCamel" => Case::UpperCamel, - "Snake" => Case::Snake, - "UpperSnake" => Case::UpperSnake, - "ScreamingSnake" => Case::ScreamingSnake, - "Kebab" => Case::Kebab, - "Cobol" => Case::Cobol, - "UpperKebab" => Case::UpperKebab, - "Train" => Case::Train, - "Flat" => Case::Flat, - "UpperFlat" => Case::UpperFlat, - "Alternating" => Case::Alternating, - _ => panic!("Unrecognized case name: {}", case_name), +use regex::Regex; +use syn::{parse_macro_input, Attribute, DeriveInput, FieldsNamed, FieldsUnnamed, Ident, Variant}; + +// Enum attributes +struct EnumAttrs { + case_transform: Option, +} + +impl EnumAttrs { + fn from_attrs(attrs: Vec) -> Self { + let mut case_transform: Option = None; + + for attr in attrs.into_iter() { + if attr.path.is_ident("enum_display") { + let meta = attr.parse_meta().unwrap(); + if let syn::Meta::List(list) = meta { + for nested in list.nested { + if let syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) = nested { + if name_value.path.is_ident("case") { + if let syn::Lit::Str(lit_str) = name_value.lit { + case_transform = + Some(Self::parse_case_name(lit_str.value().as_str())); + } + } + } + } + } + } + } + + Self { case_transform } + } + + fn parse_case_name(case_name: &str) -> Case { + match case_name { + "Upper" => Case::Upper, + "Lower" => Case::Lower, + "Title" => Case::Title, + "Toggle" => Case::Toggle, + "Camel" => Case::Camel, + "Pascal" => Case::Pascal, + "UpperCamel" => Case::UpperCamel, + "Snake" => Case::Snake, + "UpperSnake" => Case::UpperSnake, + "ScreamingSnake" => Case::ScreamingSnake, + "Kebab" => Case::Kebab, + "Cobol" => Case::Cobol, + "UpperKebab" => Case::UpperKebab, + "Train" => Case::Train, + "Flat" => Case::Flat, + "UpperFlat" => Case::UpperFlat, + "Alternating" => Case::Alternating, + _ => panic!("Unrecognized case name: {}", case_name), + } + } + + fn transform_case(&self, ident: String) -> String { + if let Some(case) = self.case_transform { + ident.to_case(case) + } else { + ident + } } } -#[proc_macro_derive(EnumDisplay, attributes(enum_display))] -pub fn derive(input: TokenStream) -> TokenStream { - // Parse the input tokens into a syntax tree - let DeriveInput { - ident, data, attrs, generics, .. - } = parse_macro_input!(input); +// Variant attributes +struct VariantAttrs { + format: Option, +} + +impl VariantAttrs { + fn from_attrs(attrs: Vec) -> Self { + let mut format = None; - // Should we transform the case of the enum variants? - let mut case_transform: Option = None; - - // Find the enum_display attribute - for attr in attrs.into_iter() { - if attr.path.is_ident("enum_display") { - let meta = attr.parse_meta().unwrap(); - if let syn::Meta::List(list) = meta { - for nested in list.nested { - if let syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) = nested { - if name_value.path.is_ident("case") { - if let syn::Lit::Str(lit_str) = name_value.lit { - // Set the case transform - case_transform = Some(parse_case_name(lit_str.value().as_str())); + // Find the variant_display attribute + for attr in attrs.into_iter() { + if attr.path.is_ident("variant_display") { + let meta = attr.parse_meta().unwrap(); + if let syn::Meta::List(list) = meta { + for nested in list.nested { + if let syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) = nested { + if name_value.path.is_ident("format") { + if let syn::Lit::Str(lit_str) = name_value.lit { + format = Some(Self::translate_numeric_placeholders(&lit_str.value())); + } } } } } } } + + Self { format } } - // Build the match arms - let variants = match data { - syn::Data::Enum(syn::DataEnum { variants, .. }) => variants, - _ => panic!("EnumDisplay can only be derived for enums"), + // Translates {123:?} to {_unnamed_123:?} for safer format arg usage + fn translate_numeric_placeholders(fmt: &str) -> String { + let re = Regex::new(r"\{\s*(\d+)\s*([^}]*)\}").unwrap(); + re.replace_all(fmt, |caps: ®ex::Captures| { + let idx = &caps[1]; + let fmt_spec = &caps[2]; + format!("{{_unnamed_{}{} }}", idx, fmt_spec) + }) + .to_string() } - .into_iter() - .map(|variant| { - let ident = variant.ident; - let ident_str = if case_transform.is_some() { - ident.to_string().to_case(case_transform.unwrap()) + + // Makes a hash set of user referenced format args so we can output minimal generated code + fn read_named_placeholders(&self) -> HashSet<&str> { + if let Some(fmt) = &self.format { + let re = Regex::new(r"\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*[^}]*\}").unwrap(); + re.captures_iter(&fmt) + .filter_map(|caps| caps.get(1).map(|m| m.as_str())) + .collect() } else { - ident.to_string() - }; + HashSet::new() + } + } +} + +// Shared intermediate variant info +struct VariantInfo { + ident: Ident, + ident_transformed: String, + attrs: VariantAttrs, +} + +// Intermediate Named variant info +struct NamedVariantIR { + info: VariantInfo, + fields: Vec, +} + +impl NamedVariantIR { + fn from_fields_named(fields_named: FieldsNamed, info: VariantInfo) -> Self { + let referenced_fields = info.attrs.read_named_placeholders(); + if referenced_fields.is_empty() { + Self { info, fields: Vec::new() } + } else { + let fields = fields_named + .named + .into_iter() + .filter_map(|field| { + let ident = field.ident.unwrap(); + referenced_fields + .contains(ident.to_string().as_str()) + .then_some(ident) + }) + .collect(); + Self { info, fields } + } + } + + fn gen(self, any_has_format: bool) -> proc_macro2::TokenStream { + let VariantInfo { ident, ident_transformed, attrs } = self.info; + let fields = self.fields; + match (any_has_format, attrs.format) { + (true, Some(fmt)) => quote! { #ident { #(#fields),*, .. } => { let variant = #ident_transformed; format!(#fmt) } }, + (true, None) => quote! { #ident { .. } => String::from(#ident_transformed), }, + (false, None) => quote! { #ident { .. } => #ident_transformed, }, + _ => unreachable!("`any_has_format` should never be false when a variant has format string"), + } + } +} + +// Intermediate Unnamed variant info +struct UnnamedVariantIR { + info: VariantInfo, + fields: Vec, +} + +impl UnnamedVariantIR { + fn from_fields_unnamed(fields_unnamed: FieldsUnnamed, info: VariantInfo) -> Self { + let referenced_fields = info.attrs.read_named_placeholders(); + if referenced_fields.is_empty() { + Self { info, fields: Vec::new() } + } else { + let skip_field = "_"; + let fields: Vec = fields_unnamed + .unnamed + .into_iter() + .enumerate() + .map(|(i, _)| { + Ident::new( + referenced_fields + .get(format!("_unnamed_{i}").as_str()) + .unwrap_or(&skip_field), + Span::call_site(), + ) + }) + .collect(); + Self { info, fields } + } + } + + fn gen(self, any_has_format: bool) -> proc_macro2::TokenStream { + let VariantInfo { ident, ident_transformed, attrs } = self.info; + let fields = self.fields; + match (any_has_format, attrs.format) { + (true, Some(fmt)) => quote! { #ident(#(#fields),*) => { let variant = #ident_transformed; format!(#fmt) } }, + (true, None) => quote! { #ident(..) => String::from(#ident_transformed), }, + (false, None) => quote! { #ident(..) => #ident_transformed, }, + _ => unreachable!("`any_has_format` should never be false when a variant has format string"), + } + } +} +// Intermediate Unit variant info +struct UnitVariantIR { + info: VariantInfo, +} + +impl UnitVariantIR { + fn new(info: VariantInfo) -> Self { + Self { info } + } + + fn gen(self, any_has_format: bool) -> proc_macro2::TokenStream { + let VariantInfo { ident, ident_transformed, attrs } = self.info; + match (any_has_format, attrs.format) { + (true, Some(fmt)) => quote! { #ident => { let variant = #ident_transformed; format!(#fmt) } }, + (true, None) => quote! { #ident => String::from(#ident_transformed), }, + (false, None) => quote! { #ident => #ident_transformed, }, + _ => unreachable!("`any_has_format` should never be false when a variant has format string"), + } + } +} + +// Intermediate version of Variant +enum VariantIR { + Named(NamedVariantIR), + Unnamed(UnnamedVariantIR), + Unit(UnitVariantIR), +} + +impl VariantIR { + fn from_variant(variant: Variant, enum_attrs: &EnumAttrs) -> Self { + let ident_str = variant.ident.to_string(); + let info = VariantInfo { + ident: variant.ident, + ident_transformed: enum_attrs.transform_case(ident_str), + attrs: VariantAttrs::from_attrs(variant.attrs), + }; match variant.fields { - syn::Fields::Named(_) => quote! { - #ident { .. } => #ident_str, + syn::Fields::Named(fields_named) => { + Self::Named(NamedVariantIR::from_fields_named(fields_named, info)) }, - syn::Fields::Unnamed(_) => quote! { - #ident(..) => #ident_str, - }, - syn::Fields::Unit => quote! { - #ident => #ident_str, + syn::Fields::Unnamed(fields_unnamed) => { + Self::Unnamed(UnnamedVariantIR::from_fields_unnamed(fields_unnamed, info)) }, + syn::Fields::Unit => Self::Unit(UnitVariantIR::new(info)), + } + } + + fn gen(self, any_has_format: bool) -> proc_macro2::TokenStream { + match self { + VariantIR::Named(named_variant) => named_variant.gen(any_has_format), + VariantIR::Unnamed(unnamed_variant) => unnamed_variant.gen(any_has_format), + VariantIR::Unit(unit_variant) => unit_variant.gen(any_has_format), } - }); + } + + fn has_format(&self) -> bool { + match self { + VariantIR::Named(named_variant) => &named_variant.info, + VariantIR::Unnamed(unnamed_variant) => &unnamed_variant.info, + VariantIR::Unit(unit_variant) => &unit_variant.info, + }.attrs.format.is_some() + } +} + +#[proc_macro_derive(EnumDisplay, attributes(enum_display, variant_display))] +pub fn derive(input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let DeriveInput { + ident, + data, + attrs, + generics, + .. + } = parse_macro_input!(input); // Copy generics and bounds let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + // Read enum attrs + let enum_attrs = EnumAttrs::from_attrs(attrs); + + // Read variants and variant attrs into an intermediate format + let intermediate_variants: Vec = match data { + syn::Data::Enum(syn::DataEnum { variants, .. }) => variants, + _ => panic!("EnumDisplay can only be derived for enums"), + } + .into_iter() + .map(|variant| VariantIR::from_variant(variant, &enum_attrs)) + .collect(); + + // If any variants have a format string, the output of all match arms must be String instead of &str + // This is because we can't return a reference to the temporary output of format!() + let any_has_format = intermediate_variants.iter().any(|v| v.has_format()); + let post_fix = if any_has_format { quote!{ .as_str() } } else { quote! { } }; + + // Build the match arms + let variants = intermediate_variants.into_iter().map(|v| v.gen(any_has_format)); + // #[allow(unused_qualifications)] is needed // due to https://github.com/SeedyROM/enum-display/issues/1 // Possibly related to https://github.com/rust-lang/rust/issues/96698 @@ -96,8 +319,8 @@ pub fn derive(input: TokenStream) -> TokenStream { ::core::fmt::Formatter::write_str( f, match self { - #(#ident::#variants)* - }, + #(Self::#variants)* + }#post_fix ) } } diff --git a/src/lib.rs b/src/lib.rs index 7ac4ebc..8340cdd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,13 +44,40 @@ mod tests { #[derive(EnumDisplay)] enum TestEnum { Name, + + #[variant_display(format = "Unit: {variant}")] + NameFullFormat, + Address { street: String, city: String, state: String, zip: String, }, + + #[variant_display(format = "Named: {variant} {{{street}, {zip}}}")] + AddressPartialFormat { + street: String, + city: String, + state: String, + zip: String, + }, + + #[variant_display(format = "Named: {variant} {{{street}, {city}, {state}, {zip}}}")] + AddressFullFormat { + street: String, + city: String, + state: String, + zip: String, + }, + DateOfBirth(u32, u32, u32), + + #[variant_display(format = "Unnamed: {variant}({2})")] + DateOfBirthPartialFormat(u32, u32, u32), + + #[variant_display(format = "Unnamed: {variant}({0}, {1}, {2})")] + DateOfBirthFullFormat(u32, u32, u32), } #[allow(dead_code)] @@ -83,6 +110,7 @@ mod tests { #[test] fn test_unit_field_variant() { assert_eq!(TestEnum::Name.to_string(), "Name"); + assert_eq!(TestEnum::NameFullFormat.to_string(), "Unit: NameFullFormat"); } #[test] @@ -97,11 +125,33 @@ mod tests { .to_string(), "Address" ); + assert_eq!( + TestEnum::AddressPartialFormat { + street: "123 Main St".to_string(), + city: "Any Town".to_string(), + state: "CA".to_string(), + zip: "12345".to_string() + } + .to_string(), + "Named: AddressPartialFormat {123 Main St, 12345}" + ); + assert_eq!( + TestEnum::AddressFullFormat { + street: "123 Main St".to_string(), + city: "Any Town".to_string(), + state: "CA".to_string(), + zip: "12345".to_string() + } + .to_string(), + "Named: AddressFullFormat {123 Main St, Any Town, CA, 12345}" + ); } #[test] fn test_unnamed_fields_variant() { - assert_eq!(TestEnum::DateOfBirth(1, 1, 2000).to_string(), "DateOfBirth"); + assert_eq!(TestEnum::DateOfBirth(1, 2, 1999).to_string(), "DateOfBirth"); + assert_eq!(TestEnum::DateOfBirthPartialFormat(1, 2, 1999).to_string(), "Unnamed: DateOfBirthPartialFormat(1999)"); + assert_eq!(TestEnum::DateOfBirthFullFormat(1, 2, 1999).to_string(), "Unnamed: DateOfBirthFullFormat(1, 2, 1999)"); } #[test] From 2730120aafa756e7073c832301f7be02e35f2ba0 Mon Sep 17 00:00:00 2001 From: jngls Date: Thu, 19 Jun 2025 14:02:12 +0100 Subject: [PATCH 3/6] Simplify --- enum-display-macro/src/lib.rs | 65 ++++++++--------------------------- 1 file changed, 14 insertions(+), 51 deletions(-) diff --git a/enum-display-macro/src/lib.rs b/enum-display-macro/src/lib.rs index 1f47f60..2c7b337 100644 --- a/enum-display-macro/src/lib.rs +++ b/enum-display-macro/src/lib.rs @@ -1,5 +1,3 @@ -use std::{collections::HashSet, fmt::format}; - use convert_case::{Case, Casing}; use proc_macro::{self, TokenStream}; use proc_macro2::Span; @@ -109,18 +107,6 @@ impl VariantAttrs { }) .to_string() } - - // Makes a hash set of user referenced format args so we can output minimal generated code - fn read_named_placeholders(&self) -> HashSet<&str> { - if let Some(fmt) = &self.format { - let re = Regex::new(r"\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*[^}]*\}").unwrap(); - re.captures_iter(&fmt) - .filter_map(|caps| caps.get(1).map(|m| m.as_str())) - .collect() - } else { - HashSet::new() - } - } } // Shared intermediate variant info @@ -138,29 +124,19 @@ struct NamedVariantIR { impl NamedVariantIR { fn from_fields_named(fields_named: FieldsNamed, info: VariantInfo) -> Self { - let referenced_fields = info.attrs.read_named_placeholders(); - if referenced_fields.is_empty() { - Self { info, fields: Vec::new() } - } else { - let fields = fields_named - .named - .into_iter() - .filter_map(|field| { - let ident = field.ident.unwrap(); - referenced_fields - .contains(ident.to_string().as_str()) - .then_some(ident) - }) - .collect(); - Self { info, fields } - } + let fields = fields_named + .named + .into_iter() + .filter_map(|field| field.ident) + .collect(); + Self { info, fields } } fn gen(self, any_has_format: bool) -> proc_macro2::TokenStream { let VariantInfo { ident, ident_transformed, attrs } = self.info; let fields = self.fields; match (any_has_format, attrs.format) { - (true, Some(fmt)) => quote! { #ident { #(#fields),*, .. } => { let variant = #ident_transformed; format!(#fmt) } }, + (true, Some(fmt)) => quote! { #ident { #(#fields),* } => { let variant = #ident_transformed; format!(#fmt) } }, (true, None) => quote! { #ident { .. } => String::from(#ident_transformed), }, (false, None) => quote! { #ident { .. } => #ident_transformed, }, _ => unreachable!("`any_has_format` should never be false when a variant has format string"), @@ -176,26 +152,13 @@ struct UnnamedVariantIR { impl UnnamedVariantIR { fn from_fields_unnamed(fields_unnamed: FieldsUnnamed, info: VariantInfo) -> Self { - let referenced_fields = info.attrs.read_named_placeholders(); - if referenced_fields.is_empty() { - Self { info, fields: Vec::new() } - } else { - let skip_field = "_"; - let fields: Vec = fields_unnamed - .unnamed - .into_iter() - .enumerate() - .map(|(i, _)| { - Ident::new( - referenced_fields - .get(format!("_unnamed_{i}").as_str()) - .unwrap_or(&skip_field), - Span::call_site(), - ) - }) - .collect(); - Self { info, fields } - } + let fields: Vec = fields_unnamed + .unnamed + .into_iter() + .enumerate() + .map(|(i, _)| Ident::new(format!("_unnamed_{i}").as_str(), Span::call_site())) + .collect(); + Self { info, fields } } fn gen(self, any_has_format: bool) -> proc_macro2::TokenStream { From c693e5e874753f4b2c17e73e1da60885ae7eec06 Mon Sep 17 00:00:00 2001 From: Zack Kollar Date: Mon, 14 Jul 2025 00:48:54 -0400 Subject: [PATCH 4/6] :hammer: attempt to fix issues --- Cargo.toml | 4 +- enum-display-macro/Cargo.toml | 2 +- enum-display-macro/src/lib.rs | 79 +++++++++++++++++++++++++---------- 3 files changed, 60 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3aaf02d..3c6cf8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "enum-display" description = "A macro to derive Display for enums" keywords = ["enum", "display", "derive", "macro"] -version = "0.1.4" +version = "0.1.5" edition = "2021" license = "MIT" documentation = "https://docs.rs/enum-display" @@ -12,4 +12,4 @@ repository = "https://github.com/SeedyROM/enum-display" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -enum-display-macro = { version = "0.1.4" } +enum-display-macro = { version = "0.1.5" } diff --git a/enum-display-macro/Cargo.toml b/enum-display-macro/Cargo.toml index 6e7fe89..b780b58 100644 --- a/enum-display-macro/Cargo.toml +++ b/enum-display-macro/Cargo.toml @@ -2,7 +2,7 @@ name = "enum-display-macro" description = "A macro to derive Display for enums" keywords = ["enum", "display", "derive", "macro"] -version = "0.1.4" +version = "0.1.5" edition = "2021" license = "MIT" documentation = "https://docs.rs/enum-display/tree/main/enum-display-derive" diff --git a/enum-display-macro/src/lib.rs b/enum-display-macro/src/lib.rs index 2c7b337..4a6a127 100644 --- a/enum-display-macro/src/lib.rs +++ b/enum-display-macro/src/lib.rs @@ -85,7 +85,9 @@ impl VariantAttrs { if let syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) = nested { if name_value.path.is_ident("format") { if let syn::Lit::Str(lit_str) = name_value.lit { - format = Some(Self::translate_numeric_placeholders(&lit_str.value())); + format = Some(Self::translate_numeric_placeholders( + &lit_str.value(), + )); } } } @@ -132,14 +134,22 @@ impl NamedVariantIR { Self { info, fields } } - fn gen(self, any_has_format: bool) -> proc_macro2::TokenStream { - let VariantInfo { ident, ident_transformed, attrs } = self.info; + fn genereate(self, any_has_format: bool) -> proc_macro2::TokenStream { + let VariantInfo { + ident, + ident_transformed, + attrs, + } = self.info; let fields = self.fields; match (any_has_format, attrs.format) { - (true, Some(fmt)) => quote! { #ident { #(#fields),* } => { let variant = #ident_transformed; format!(#fmt) } }, + (true, Some(fmt)) => { + quote! { #ident { #(#fields),* } => { let variant = #ident_transformed; format!(#fmt) } } + } (true, None) => quote! { #ident { .. } => String::from(#ident_transformed), }, (false, None) => quote! { #ident { .. } => #ident_transformed, }, - _ => unreachable!("`any_has_format` should never be false when a variant has format string"), + _ => unreachable!( + "`any_has_format` should never be false when a variant has format string" + ), } } } @@ -161,14 +171,22 @@ impl UnnamedVariantIR { Self { info, fields } } - fn gen(self, any_has_format: bool) -> proc_macro2::TokenStream { - let VariantInfo { ident, ident_transformed, attrs } = self.info; + fn generate(self, any_has_format: bool) -> proc_macro2::TokenStream { + let VariantInfo { + ident, + ident_transformed, + attrs, + } = self.info; let fields = self.fields; match (any_has_format, attrs.format) { - (true, Some(fmt)) => quote! { #ident(#(#fields),*) => { let variant = #ident_transformed; format!(#fmt) } }, + (true, Some(fmt)) => { + quote! { #ident(#(#fields),*) => { let variant = #ident_transformed; format!(#fmt) } } + } (true, None) => quote! { #ident(..) => String::from(#ident_transformed), }, (false, None) => quote! { #ident(..) => #ident_transformed, }, - _ => unreachable!("`any_has_format` should never be false when a variant has format string"), + _ => unreachable!( + "`any_has_format` should never be false when a variant has format string" + ), } } } @@ -183,13 +201,21 @@ impl UnitVariantIR { Self { info } } - fn gen(self, any_has_format: bool) -> proc_macro2::TokenStream { - let VariantInfo { ident, ident_transformed, attrs } = self.info; + fn generate(self, any_has_format: bool) -> proc_macro2::TokenStream { + let VariantInfo { + ident, + ident_transformed, + attrs, + } = self.info; match (any_has_format, attrs.format) { - (true, Some(fmt)) => quote! { #ident => { let variant = #ident_transformed; format!(#fmt) } }, + (true, Some(fmt)) => { + quote! { #ident => { let variant = #ident_transformed; format!(#fmt) } } + } (true, None) => quote! { #ident => String::from(#ident_transformed), }, (false, None) => quote! { #ident => #ident_transformed, }, - _ => unreachable!("`any_has_format` should never be false when a variant has format string"), + _ => unreachable!( + "`any_has_format` should never be false when a variant has format string" + ), } } } @@ -212,19 +238,19 @@ impl VariantIR { match variant.fields { syn::Fields::Named(fields_named) => { Self::Named(NamedVariantIR::from_fields_named(fields_named, info)) - }, + } syn::Fields::Unnamed(fields_unnamed) => { Self::Unnamed(UnnamedVariantIR::from_fields_unnamed(fields_unnamed, info)) - }, + } syn::Fields::Unit => Self::Unit(UnitVariantIR::new(info)), } } - fn gen(self, any_has_format: bool) -> proc_macro2::TokenStream { + fn generate(self, any_has_format: bool) -> proc_macro2::TokenStream { match self { - VariantIR::Named(named_variant) => named_variant.gen(any_has_format), - VariantIR::Unnamed(unnamed_variant) => unnamed_variant.gen(any_has_format), - VariantIR::Unit(unit_variant) => unit_variant.gen(any_has_format), + VariantIR::Named(named_variant) => named_variant.generate(any_has_format), + VariantIR::Unnamed(unnamed_variant) => unnamed_variant.generate(any_has_format), + VariantIR::Unit(unit_variant) => unit_variant.generate(any_has_format), } } @@ -233,7 +259,10 @@ impl VariantIR { VariantIR::Named(named_variant) => &named_variant.info, VariantIR::Unnamed(unnamed_variant) => &unnamed_variant.info, VariantIR::Unit(unit_variant) => &unit_variant.info, - }.attrs.format.is_some() + } + .attrs + .format + .is_some() } } @@ -266,10 +295,16 @@ pub fn derive(input: TokenStream) -> TokenStream { // If any variants have a format string, the output of all match arms must be String instead of &str // This is because we can't return a reference to the temporary output of format!() let any_has_format = intermediate_variants.iter().any(|v| v.has_format()); - let post_fix = if any_has_format { quote!{ .as_str() } } else { quote! { } }; + let post_fix = if any_has_format { + quote! { .as_str() } + } else { + quote! {} + }; // Build the match arms - let variants = intermediate_variants.into_iter().map(|v| v.gen(any_has_format)); + let variants = intermediate_variants + .into_iter() + .map(|v| v.generate(any_has_format)); // #[allow(unused_qualifications)] is needed // due to https://github.com/SeedyROM/enum-display/issues/1 From 9fde553e86606230209b1bbead177e2aa59f1d2d Mon Sep 17 00:00:00 2001 From: Zack Kollar Date: Mon, 14 Jul 2025 00:59:43 -0400 Subject: [PATCH 5/6] :rotating_light: Fix issues with current PR - Bump version number before publish - `gen` is a reserved keyword: https://doc.rust-lang.org/edition-guide/rust-2024/gen-keyword.html - Rename all functions named `gen` to `generate` - Allow for local testing via workspaces, however when publishing the local dep needs to be replaced with the published version - This allowed for the actual proc_macro code changes to propegate into the local build. --- Cargo.toml | 7 ++++--- enum-display-macro/src/lib.rs | 6 +++--- src/lib.rs | 20 ++++++++++++++++---- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3c6cf8a..a746047 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = [".", "enum-display-macro"] + [package] name = "enum-display" description = "A macro to derive Display for enums" @@ -9,7 +12,5 @@ documentation = "https://docs.rs/enum-display" homepage = "https://github.com/SeedyROM/enum-display" repository = "https://github.com/SeedyROM/enum-display" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -enum-display-macro = { version = "0.1.5" } +enum-display-macro = { path = "./enum-display-macro" } diff --git a/enum-display-macro/src/lib.rs b/enum-display-macro/src/lib.rs index 4a6a127..da9c79c 100644 --- a/enum-display-macro/src/lib.rs +++ b/enum-display-macro/src/lib.rs @@ -54,7 +54,7 @@ impl EnumAttrs { "Flat" => Case::Flat, "UpperFlat" => Case::UpperFlat, "Alternating" => Case::Alternating, - _ => panic!("Unrecognized case name: {}", case_name), + _ => panic!("Unrecognized case name: {case_name}"), } } @@ -105,7 +105,7 @@ impl VariantAttrs { re.replace_all(fmt, |caps: ®ex::Captures| { let idx = &caps[1]; let fmt_spec = &caps[2]; - format!("{{_unnamed_{}{} }}", idx, fmt_spec) + format!("{{_unnamed_{idx}{fmt_spec}}}") }) .to_string() } @@ -134,7 +134,7 @@ impl NamedVariantIR { Self { info, fields } } - fn genereate(self, any_has_format: bool) -> proc_macro2::TokenStream { + fn generate(self, any_has_format: bool) -> proc_macro2::TokenStream { let VariantInfo { ident, ident_transformed, diff --git a/src/lib.rs b/src/lib.rs index 8340cdd..7335558 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,7 +96,10 @@ mod tests { #[allow(dead_code)] #[derive(EnumDisplay)] - enum TestEnumWithGenerics<'a, T: Clone> where T: std::fmt::Display { + enum TestEnumWithGenerics<'a, T: Clone> + where + T: std::fmt::Display, + { Name, Address { street: &'a T, @@ -150,8 +153,14 @@ mod tests { #[test] fn test_unnamed_fields_variant() { assert_eq!(TestEnum::DateOfBirth(1, 2, 1999).to_string(), "DateOfBirth"); - assert_eq!(TestEnum::DateOfBirthPartialFormat(1, 2, 1999).to_string(), "Unnamed: DateOfBirthPartialFormat(1999)"); - assert_eq!(TestEnum::DateOfBirthFullFormat(1, 2, 1999).to_string(), "Unnamed: DateOfBirthFullFormat(1, 2, 1999)"); + assert_eq!( + TestEnum::DateOfBirthPartialFormat(1, 2, 1999).to_string(), + "Unnamed: DateOfBirthPartialFormat(1999)" + ); + assert_eq!( + TestEnum::DateOfBirthFullFormat(1, 2, 1999).to_string(), + "Unnamed: DateOfBirthFullFormat(1, 2, 1999)" + ); } #[test] @@ -202,6 +211,9 @@ mod tests { #[test] fn test_unnamed_fields_variant_with_generics() { - assert_eq!(TestEnumWithGenerics::<'_, String>::DateOfBirth(1, 1, 2000).to_string(), "DateOfBirth"); + assert_eq!( + TestEnumWithGenerics::<'_, String>::DateOfBirth(1, 1, 2000).to_string(), + "DateOfBirth" + ); } } From 1bfa469d206c1fa8577d203ad8ff6fd4a03ed967 Mon Sep 17 00:00:00 2001 From: Zack Kollar Date: Mon, 14 Jul 2025 01:09:50 -0400 Subject: [PATCH 6/6] :hammer: Use specific version locally, in production/package management use the published version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a746047..1bf8a55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,4 @@ homepage = "https://github.com/SeedyROM/enum-display" repository = "https://github.com/SeedyROM/enum-display" [dependencies] -enum-display-macro = { path = "./enum-display-macro" } +enum-display-macro = { version = "0.1.5", path = "./enum-display-macro" }