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: 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}