setty_derive/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::quote;
4use syn::spanned::Spanned;
5
6/////////////////////////////////////////////////////////////////////////////////////////
7
8#[proc_macro_derive(Config, attributes(config))]
9pub fn derive_config(input: TokenStream) -> TokenStream {
10    let input = syn::parse_macro_input!(input as syn::DeriveInput);
11
12    match config_impl(input) {
13        Ok(output) => output,
14        Err(err) => err.to_compile_error().into(),
15    }
16}
17
18/////////////////////////////////////////////////////////////////////////////////////////
19
20#[proc_macro_attribute]
21pub fn __erase(_attr: TokenStream, _item: TokenStream) -> TokenStream {
22    TokenStream::new()
23}
24
25/////////////////////////////////////////////////////////////////////////////////////////
26
27fn config_impl(mut input: syn::DeriveInput) -> syn::Result<TokenStream> {
28    let mut default_functions = Vec::new();
29
30    #[cfg(feature = "derive-debug")]
31    input.attrs.push(syn::parse_quote! {
32        #[derive(Debug)]
33    });
34
35    #[cfg(feature = "derive-eq")]
36    input.attrs.push(syn::parse_quote! {
37        #[derive(PartialEq, Eq)]
38    });
39
40    #[cfg(feature = "derive-deserialize")]
41    {
42        input.attrs.push(syn::parse_quote! {
43            #[derive(serde::Deserialize)]
44        });
45        input.attrs.push(syn::parse_quote! {
46            #[serde(deny_unknown_fields, rename_all = "camelCase")]
47        });
48    }
49
50    #[cfg(feature = "derive-serialize")]
51    input.attrs.push(syn::parse_quote! {
52        #[derive(serde::Serialize)]
53    });
54
55    #[cfg(feature = "derive-jsonschema")]
56    input.attrs.push(syn::parse_quote! {
57        #[derive(schemars::JsonSchema)]
58    });
59
60    match &mut input.data {
61        syn::Data::Struct(item) => {
62            for field in &mut item.fields {
63                let opts = ConfigFieldOpts::extract_from(&mut field.attrs)?;
64
65                if !opts.required.unwrap_or_default() {
66                    let new_default_attr = if let Some(expr) = opts.default {
67                        let fname =
68                            quote::format_ident!("__default_{}", field.ident.as_ref().unwrap());
69                        let path_str = syn::Lit::Str(syn::LitStr::new(
70                            &format!("{}::{}", input.ident, fname),
71                            opts.span,
72                        ));
73
74                        default_functions.push(quote! {
75                            fn #fname() -> String {
76                                #expr.into()
77                            }
78                        });
79
80                        syn::parse_quote! {
81                            #[serde(default = #path_str)]
82                        }
83                    } else {
84                        syn::parse_quote!(#[serde(default)])
85                    };
86
87                    field.attrs.push(new_default_attr);
88                }
89            }
90        }
91
92        syn::Data::Enum(item) => {
93            let unit_enum = item
94                .variants
95                .iter()
96                .all(|v| matches!(v.fields, syn::Fields::Unit));
97
98            if !unit_enum {
99                input.attrs.push(syn::parse_quote! {
100                    #[serde(tag = "kind")]
101                });
102            }
103        }
104
105        _ => {
106            return Err(syn::Error::new_spanned(
107                input,
108                "#[derive(Config)] can only be applied to structs and enums",
109            ));
110        }
111    }
112
113    // NOTE: Since derive macros are additive (only emit new code) we add a special
114    // `__erase` proc macro call to erase the emitted type and avoid having duplicate types.
115    //  i.e. the emitted type will exist only long enough for newly emitted derive macros
116    // to do their work.
117    input.attrs.push(syn::parse_quote! { #[::setty::__erase] });
118
119    let item_name = &input.ident;
120
121    Ok(TokenStream::from(quote! {
122        #input
123
124        impl #item_name {
125            #(#default_functions)*
126        }
127    }))
128}
129
130/////////////////////////////////////////////////////////////////////////////////////////
131
132struct ConfigFieldOpts {
133    required: Option<bool>,
134    default: Option<syn::Expr>,
135    span: Span,
136}
137
138impl ConfigFieldOpts {
139    fn new(span: Span) -> Self {
140        Self {
141            required: None,
142            default: None,
143            span,
144        }
145    }
146
147    fn merge(&mut self, other: Self) -> syn::Result<()> {
148        self.span = other.span;
149
150        if other.required.is_some() {
151            if self.required.is_some() {
152                return Err(syn::Error::new(
153                    other.span,
154                    "`required` specified more than once",
155                ));
156            }
157            self.required = other.required;
158        }
159
160        if other.default.is_some() {
161            if self.default.is_some() {
162                return Err(syn::Error::new(
163                    other.span,
164                    "`default` specified more than once",
165                ));
166            }
167            self.default = other.default;
168        }
169
170        if self.required == Some(true) && self.default.is_some() {
171            return Err(syn::Error::new(
172                other.span,
173                "can't be both `required` and specify a `default`",
174            ));
175        }
176
177        Ok(())
178    }
179
180    fn extract_from(attrs: &mut Vec<syn::Attribute>) -> syn::Result<ConfigFieldOpts> {
181        let mut opts = ConfigFieldOpts::new(proc_macro2::Span::call_site());
182
183        for attr in attrs.iter() {
184            if attr.path().is_ident("config") {
185                let more_opts = Self::parse_from(attr)?;
186                opts.merge(more_opts)?;
187            }
188        }
189
190        attrs.retain(|attr| !attr.path().is_ident("config"));
191
192        Ok(opts)
193    }
194
195    fn parse_from(attr: &syn::Attribute) -> syn::Result<ConfigFieldOpts> {
196        let mut opts = ConfigFieldOpts::new(attr.span());
197
198        attr.parse_args_with(|input: syn::parse::ParseStream| {
199            while !input.is_empty() {
200                let ident: syn::Ident = input.parse()?;
201
202                match ident.to_string().as_str() {
203                    "required" => {
204                        if opts.required.is_some() {
205                            return Err(syn::Error::new(
206                                attr.span(),
207                                "`required` specified more than once",
208                            ));
209                        }
210                        opts.required = Some(true);
211                    }
212
213                    "default" => {
214                        if opts.required.is_some() {
215                            return Err(syn::Error::new(
216                                attr.span(),
217                                "`default` specified more than once",
218                            ));
219                        }
220                        input.parse::<syn::Token![=]>()?;
221                        let expr: syn::Expr = input.parse()?;
222                        opts.default = Some(expr);
223                    }
224
225                    _ => {
226                        return Err(syn::Error::new(
227                            attr.span(),
228                            format!("unknown config option `{}`", ident),
229                        ));
230                    }
231                }
232
233                // Optional trailing comma
234                let _ = input.parse::<syn::Token![,]>();
235            }
236
237            Ok(())
238        })?;
239
240        Ok(opts)
241    }
242}
243
244/////////////////////////////////////////////////////////////////////////////////////////