macron_impl_display/
lib.rs1use darling::{FromDeriveInput, FromVariant};
2use proc_macro::TokenStream;
3use quote::quote;
4use syn::{parse_macro_input, Data, DeriveInput, Fields};
5
6#[derive(FromDeriveInput)]
7#[darling(attributes(display), forward_attrs(allow, doc))]
8struct ContainerReceiver {
9 ident: syn::Ident,
10 #[darling(default)]
11 rename: Option<String>,
12 #[darling(default)]
13 fmt: Option<String>,
14}
15
16#[derive(FromVariant)]
17#[darling(attributes(display))]
18struct VariantReceiver {
19 ident: syn::Ident,
20 #[allow(dead_code)]
21 fields: darling::ast::Fields<darling::util::Ignored>,
22 #[darling(default)]
23 rename: Option<String>,
24 #[darling(default)]
25 fmt: Option<String>,
26}
27
28use heck::{
29 ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase,
30 ToUpperCamelCase,
31};
32
33fn apply_rename(text: &str, style: Option<&str>) -> String {
34 let Some(style) = style else {
35 return text.to_string();
36 };
37
38 match style {
39 "lowercase" => text.to_lowercase(),
40 "uppercase" | "UPPERCASE" => text.to_uppercase(),
41 "camelcase" | "camelCase" => text.to_lower_camel_case(),
42 "CamelCase" => text.to_upper_camel_case(),
43 "pascalcase" | "PascalCase" => text.to_pascal_case(),
44 "snakecase" | "snake_case" => text.to_snake_case(),
45 "SNAKE_CASE" | "SCREAMING_SNAKE_CASE" => text.to_shouty_snake_case(),
46 "kebabcase" | "kebab-case" => text.to_kebab_case(),
47 "KEBAB-CASE" | "SCREAMING-KEBAB-CASE" => text.to_shouty_kebab_case(),
48 _ => panic!("Unexpected text style '{style}'. Available variants: lowercase, uppercase, UPPERCASE, camelcase, CamelCase, pascalcase, PascalCase, snakecase, snake_case, SNAKE_CASE, SCREAMING_SNAKE_CASE, kebabcase, kebab-case, KEBAB-CASE, SCREAMING-KEBAB-CASE"),
49 }
50}
51
52fn read_legacy_attr(attrs: &[syn::Attribute]) -> Option<String> {
53 attrs
54 .iter()
55 .find(|attr| attr.path().is_ident("display"))
56 .and_then(|attr| {
57 if let syn::Meta::NameValue(meta) = &attr.meta {
58 if let syn::Expr::Lit(syn::ExprLit {
59 lit: syn::Lit::Str(s),
60 ..
61 }) = &meta.value
62 {
63 return Some(s.value());
64 }
65 }
66 None
67 })
68}
69
70#[proc_macro_derive(Display, attributes(display))]
71pub fn impl_display(input: TokenStream) -> TokenStream {
72 let input = parse_macro_input!(input as DeriveInput);
73
74 let container = match ContainerReceiver::from_derive_input(&input) {
75 Ok(val) => val,
76 Err(err) => return err.write_errors().into(),
77 };
78
79 let ident = &container.ident;
80 let legacy_fmt = read_legacy_attr(&input.attrs);
81 let container_fmt = container.fmt.or(legacy_fmt);
82
83 let body = match &input.data {
84 Data::Struct(st) => {
85 if let Some(fmt) = container_fmt {
86 let field_names = st
87 .fields
88 .iter()
89 .filter_map(|f| f.ident.as_ref())
90 .collect::<Vec<_>>();
91
92 quote! {
93 #[allow(unused_variables)]
94 {
95 #( let #field_names = &self.#field_names; )*
96 write!(f, #fmt)
97 }
98 }
99 } else {
100 let name = apply_rename(&ident.to_string(), container.rename.as_deref());
101 quote! { write!(f, #name) }
102 }
103 }
104
105 Data::Enum(en) => {
106 let mut matches = Vec::new();
107
108 for variant in &en.variants {
109 let var_ctx = match VariantReceiver::from_variant(variant) {
110 Ok(val) => val,
111 Err(err) => return err.write_errors().into(),
112 };
113
114 let var_ident = &var_ctx.ident;
115 let legacy_var_fmt = read_legacy_attr(&variant.attrs);
116 let var_fmt = var_ctx.fmt.or(legacy_var_fmt);
117
118 let match_arm = match &variant.fields {
119 Fields::Unit => {
120 if let Some(fmt) = var_fmt {
121 quote! { Self::#var_ident => write!(f, #fmt) }
122 } else {
123 let name = apply_rename(
124 &var_ident.to_string(),
125 var_ctx.rename.as_deref().or(container.rename.as_deref()),
126 );
127 quote! { Self::#var_ident => write!(f, #name) }
128 }
129 }
130
131 Fields::Named(fields) => {
132 let args = fields
133 .named
134 .iter()
135 .filter_map(|f| f.ident.as_ref())
136 .collect::<Vec<_>>();
137 if let Some(fmt) = var_fmt {
138 quote! {
139 Self::#var_ident { #(#args,)* .. } => {
140 #[allow(unused_variables)]
141 {
142 write!(f, #fmt)
143 }
144 }
145 }
146 } else {
147 let name = apply_rename(
148 &var_ident.to_string(),
149 var_ctx.rename.as_deref().or(container.rename.as_deref()),
150 );
151 quote! { Self::#var_ident { .. } => write!(f, #name) }
152 }
153 }
154
155 Fields::Unnamed(fields) => {
156 if let Some(fmt) = var_fmt {
157 let args = (0..fields.unnamed.len())
158 .map(|i| quote::format_ident!("_{}", i))
159 .collect::<Vec<_>>();
160
161 let used_args = (0..fields.unnamed.len())
162 .filter(|i| {
163 fmt.contains(&format!("{{{i}}}"))
164 || fmt.contains(&format!("{{{i}:"))
165 })
166 .map(|i| quote::format_ident!("_{}", i))
167 .collect::<Vec<_>>();
168
169 quote! {
170 Self::#var_ident(#(#args,)*) => {
171 #[allow(unused_variables)]
172 {
173 write!(f, #fmt, #(#used_args),*)
174 }
175 }
176 }
177 } else if fields.unnamed.len() == 1 {
178 quote! { Self::#var_ident(ref arg) => write!(f, "{}", arg) }
179 } else {
180 let name = apply_rename(
181 &var_ident.to_string(),
182 var_ctx.rename.as_deref().or(container.rename.as_deref()),
183 );
184 quote! { Self::#var_ident(..) => write!(f, #name) }
185 }
186 }
187 };
188 matches.push(match_arm);
189 }
190
191 if matches.is_empty() {
192 quote! { write!(f, "") }
193 } else {
194 quote! {
195 match self {
196 #(#matches,)*
197 }
198 }
199 }
200 }
201 _ => panic!("Only structs and enums are supported"),
202 };
203
204 quote! {
205 impl ::std::fmt::Display for #ident {
206 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
207 #body
208 }
209 }
210 }
211 .into()
212}