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_all: 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_all: 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_all.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
126 .rename_all
127 .as_deref()
128 .or(container.rename_all.as_deref()),
129 );
130 quote! { Self::#var_ident => write!(f, #name) }
131 }
132 }
133
134 Fields::Named(fields) => {
135 let args = fields
136 .named
137 .iter()
138 .filter_map(|f| f.ident.as_ref())
139 .collect::<Vec<_>>();
140 if let Some(fmt) = var_fmt {
141 quote! {
142 Self::#var_ident { #(#args,)* .. } => {
143 #[allow(unused_variables)]
144 {
145 write!(f, #fmt)
146 }
147 }
148 }
149 } else {
150 let name = apply_rename(
151 &var_ident.to_string(),
152 var_ctx
153 .rename_all
154 .as_deref()
155 .or(container.rename_all.as_deref()),
156 );
157 quote! { Self::#var_ident { .. } => write!(f, #name) }
158 }
159 }
160
161 Fields::Unnamed(fields) => {
162 if let Some(fmt) = var_fmt {
163 let args = (0..fields.unnamed.len())
164 .map(|i| quote::format_ident!("_{}", i))
165 .collect::<Vec<_>>();
166
167 let used_args = (0..fields.unnamed.len())
168 .filter(|i| {
169 fmt.contains(&format!("{{{i}}}"))
170 || fmt.contains(&format!("{{{i}:"))
171 })
172 .map(|i| quote::format_ident!("_{}", i))
173 .collect::<Vec<_>>();
174
175 quote! {
176 Self::#var_ident(#(#args,)*) => {
177 #[allow(unused_variables)]
178 {
179 write!(f, #fmt, #(#used_args),*)
180 }
181 }
182 }
183 } else if fields.unnamed.len() == 1 {
184 quote! { Self::#var_ident(ref arg) => write!(f, "{}", arg) }
185 } else {
186 let name = apply_rename(
187 &var_ident.to_string(),
188 var_ctx
189 .rename_all
190 .as_deref()
191 .or(container.rename_all.as_deref()),
192 );
193 quote! { Self::#var_ident(..) => write!(f, #name) }
194 }
195 }
196 };
197 matches.push(match_arm);
198 }
199
200 if matches.is_empty() {
201 quote! { write!(f, "") }
202 } else {
203 quote! {
204 match self {
205 #(#matches,)*
206 }
207 }
208 }
209 }
210 _ => panic!("Only structs and enums are supported"),
211 };
212
213 quote! {
214 impl ::std::fmt::Display for #ident {
215 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
216 #body
217 }
218 }
219 }
220 .into()
221}