Skip to main content

macron_impl_display/
lib.rs

1use 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}