fromenv_derive/
lib.rs

1mod field;
2
3use darling::{
4    FromDeriveInput,
5    ast::{Data, Fields},
6};
7use proc_macro2::TokenStream;
8use quote::{ToTokens, format_ident, quote};
9use syn::{DeriveInput, ExprPath, Ident, Visibility, parse_macro_input};
10
11use crate::field::{EnvAttribute, FromEnvFieldReceiver};
12
13#[proc_macro_derive(FromEnv, attributes(env))]
14pub fn derive_from_env(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
15    let input = parse_macro_input!(input as DeriveInput);
16
17    match impl_derive(input) {
18        Ok(output) => output.into(),
19        Err(err) => err.write_errors().into(),
20    }
21}
22
23fn impl_derive(input: DeriveInput) -> darling::Result<TokenStream> {
24    let config_struct = match FromEnvReceiver::from_derive_input(&input) {
25        Ok(config_struct) => config_struct,
26        Err(e) => {
27            return Err(e);
28        }
29    };
30
31    config_struct.validate()?;
32
33    Ok(config_struct.to_token_stream())
34}
35
36#[derive(FromDeriveInput)]
37#[darling(supports(struct_named))]
38struct FromEnvReceiver {
39    pub ident: Ident,
40    pub vis: Visibility,
41    pub data: Data<(), FromEnvFieldReceiver>,
42}
43
44struct ConstTokens {
45    private_path: TokenStream,
46    errors_ident: TokenStream,
47    builder_name: Ident,
48}
49
50impl ToTokens for FromEnvReceiver {
51    fn to_tokens(&self, tokens: &mut TokenStream) {
52        let consts = ConstTokens {
53            builder_name: format_ident!("{}Builder", self.ident),
54            private_path: quote!(__fromenv::__private),
55            errors_ident: quote!(__fromenv_derive_builder_errors),
56        };
57        let private_path = &consts.private_path;
58
59        let impl_struct = self.impl_struct(&consts);
60        let builder_struct = self.builder_struct(&consts);
61        let impl_from_env = self.impl_from_env(&consts);
62        let impl_from_env_builder = self.impl_from_env_builder(&consts);
63        let impl_builder = self.impl_builder(&consts);
64
65        let derive = quote! {
66            const _: () = {
67                extern crate fromenv as __fromenv;
68                use #private_path::Parser as _;
69
70                #impl_struct
71
72                #builder_struct
73
74                #impl_from_env
75
76                #impl_from_env_builder
77
78                #impl_builder
79            };
80        };
81
82        tokens.extend(derive);
83    }
84}
85
86impl FromEnvReceiver {
87    fn validate(&self) -> darling::Result<()> {
88        if !matches!(&self.vis, Visibility::Public(_)) {
89            let err = darling::Error::custom("FromEnv derive requires a public struct")
90                .with_span(&self.ident.span());
91            Err(err)
92        } else {
93            Ok(())
94        }
95    }
96
97    fn builder_struct(&self, consts: &ConstTokens) -> TokenStream {
98        let private_path = &consts.private_path;
99        let builder_name = &consts.builder_name;
100        let fields = self.get_fields().iter().map(|field| {
101            let ident = &field.ident;
102            let ty = &field.option.as_ref().unwrap_or(&field.ty);
103
104            match &field.env_attr {
105                EnvAttribute::Nested => {
106                    quote! { #ident: Option<<#ty as #private_path::FromEnv>::FromEnvBuilder> }
107                }
108                EnvAttribute::Flat { .. } | EnvAttribute::None => {
109                    quote! { #ident: Option<#ty> }
110                }
111            }
112        });
113
114        quote! {
115            pub struct #builder_name {
116                #(#fields,)*
117            }
118        }
119    }
120
121    fn impl_struct(&self, consts: &ConstTokens) -> TokenStream {
122        let struct_name = &self.ident;
123        let private_path = &consts.private_path;
124        let builder_name = &consts.builder_name;
125
126        quote! {
127            impl #struct_name {
128                pub fn from_env() -> #builder_name {
129                    <Self as #private_path::FromEnv>::from_env()
130                }
131
132                pub fn requirements() -> String {
133                    let mut requirements = ::std::string::String::new();
134                    <Self as #private_path::FromEnv>::requirements(&mut requirements);
135                    requirements
136                }
137            }
138        }
139    }
140
141    fn impl_from_env(&self, consts: &ConstTokens) -> TokenStream {
142        let struct_name = &self.ident;
143        let builder_name = &consts.builder_name;
144        let private_path = &consts.private_path;
145
146        let fields = self.get_fields().iter().map(|field| {
147            let ident = &field.ident;
148            let ty = field.option.as_ref().unwrap_or(&field.ty);
149
150            match &field.env_attr {
151                EnvAttribute::Nested => {
152                    quote! { #ident: Some(<#ty as #private_path::FromEnv>::from_env()) }
153                }
154                EnvAttribute::Flat { .. } | EnvAttribute::None => {
155                    quote! { #ident: None }
156                }
157            }
158        });
159
160        let requirements = self.get_fields().iter().map(|field| {
161            let ty = field.option.as_ref().unwrap_or(&field.ty);
162
163            match &field.env_attr {
164                EnvAttribute::Nested => {
165                    quote! {
166                        <#ty as #private_path::FromEnv>::requirements(requirements);
167                    }
168                }
169                EnvAttribute::Flat {
170                    from,
171                    default,
172                    with: _,
173                } => {
174                    let from = from.value();
175                    let default = default
176                        .as_ref()
177                        .map(|default| default.value())
178                        .unwrap_or(String::new());
179                    let out = format!("{from}={default}\n");
180
181                    quote! {
182                        requirements.push_str(#out);
183                    }
184                }
185                _ => TokenStream::new(),
186            }
187        });
188
189        quote! {
190            impl #private_path::FromEnv for #struct_name {
191                type FromEnvBuilder = #builder_name;
192
193                fn from_env() -> Self::FromEnvBuilder {
194                    #builder_name {
195                        #(#fields,)*
196                    }
197                }
198
199                fn requirements(requirements: &mut ::std::string::String) {
200                    #(#requirements)*
201                }
202            }
203        }
204    }
205
206    fn impl_from_env_builder(&self, consts: &ConstTokens) -> TokenStream {
207        let struct_name = &self.ident;
208        let private_path = &consts.private_path;
209        let errors_ident = &consts.errors_ident;
210        let builder_name = &consts.builder_name;
211
212        // This is the secret sauce that lets us gather all errors before
213        // failing.
214        let assignments = self.get_fields().iter().map(|field| {
215            let ident = &field.ident;
216            let path = format!("{struct_name}.{ident}");
217
218            match (&field.env_attr, field.option.is_some()) {
219                // #[config(nested)] field: T,
220                (EnvAttribute::Nested, false) => {
221                    quote! {
222                        let #ident = match #private_path::FromEnvBuilder::finalize(self.#ident.take().unwrap()) {
223                            Ok(inner) => Ok(inner),
224                            Err(errors) => {
225                                #errors_ident.extend(errors);
226                                Err(())
227                            }
228                        };
229                    }
230                }
231                // #[config(nested)] field: Option<T>,
232                (EnvAttribute::Nested, true) => {
233                    quote! {
234                        let #ident = match #private_path::FromEnvBuilder::finalize(self.#ident.take().unwrap()) {
235                            Ok(inner) => Ok(Some(inner)),
236                            Err(errors) if errors.only_missing_errors() => Ok(None),
237                            Err(errors) => {
238                                #errors_ident.extend(errors);
239                                Err(())
240                            }
241                        };
242                    }
243                }
244                // #[config(env = "...", default = ...)] field: T
245                (
246                    EnvAttribute::Flat {
247                        from,
248                        with,
249                        default: Some(default),
250                    },
251                    false,
252                ) => {
253                    let with = parser_path(consts, with.as_ref());
254
255                    quote! {
256                        let #ident = if let Some(inner) = self.#ident {
257                            Ok(inner)
258                        } else {
259                            match #with.parse_from_env(#from) {
260                                Some((_, Ok(val))) => Ok(val),
261                                Some((value, Err(error))) => {
262                                    let err = #private_path::FromEnvError::ParseError {
263                                        path: #path.to_string(),
264                                        env_var: #from.to_string(),
265                                        value,
266                                        error,
267                                    };
268                                    #errors_ident.add(err);
269                                    Err(())
270                                }
271                                None => {
272                                    #with.parse(#default).map_err(|error| {
273                                        let err = #private_path::FromEnvError::ParseError {
274                                            path: #path.to_string(),
275                                            env_var: #from.to_string(),
276                                            value: #default.to_string(),
277                                            error: error.into(),
278                                        };
279                                        #errors_ident.add(err);
280                                    })
281                                },
282                            }
283                        };
284                    }
285                }
286                // #[env(from = "...")] field: T
287                (
288                    EnvAttribute::Flat {
289                        from,
290                        with,
291                        default: None,
292                    },
293                    false,
294                ) => {
295                    let with = parser_path(consts, with.as_ref());
296
297                    quote! {
298                    let #ident = if let Some(inner) = self.#ident {
299                        Ok(inner)
300                    } else {
301                        match #with.parse_from_env(#from) {
302                            Some((_, Ok(val))) => Ok(val),
303                            Some((value, Err(error))) => {
304                                let err = #private_path::FromEnvError::ParseError {
305                                    path: #path.to_string(),
306                                    env_var: #from.to_string(),
307                                    value,
308                                    error,
309                                };
310                                #errors_ident.add(err);
311                                Err(())
312                            }
313                            None => {
314                                let err = #private_path::FromEnvError::MissingEnv {
315                                    path: #path.to_string(),
316                                    env_var: #from.to_string(),
317                                };
318                                #errors_ident.add(err);
319                                Err(())
320                            }
321                        }
322                    };
323                    }
324                }
325                // #[env(from = "...")] field: Option<T>
326                (
327                    EnvAttribute::Flat {
328                        from,
329                        with,
330                        default: None,
331                    },
332                    true,
333                ) => {
334                    let with = parser_path(consts, with.as_ref());
335
336                    quote! {
337                        let #ident = if let Some(inner) = self.#ident {
338                            Ok(Some(inner))
339                        } else {
340                            match #with.parse_from_env(#from) {
341                                Some((_, Ok(val))) => Ok(Some(val)),
342                                Some((value, Err(error))) => {
343                                    let err = #private_path::FromEnvError::ParseError {
344                                        path: #path.to_string(),
345                                        env_var: #from.to_string(),
346                                        value,
347                                        error,
348                                    };
349                                    #errors_ident.add(err);
350                                    Err(())
351                                }
352                                None => {
353                                    Ok(None)
354                                }
355                            }
356                        };
357                    }
358                }
359                // #[env(default = "...")] field: Option<T>
360                (
361                    EnvAttribute::Flat {
362                        from: _,
363                        with: _,
364                        default: Some(_),
365                    },
366                    true,
367                ) => unreachable!("we've already checked that Optional fields can't have a default"),
368                (EnvAttribute::None, false) => {
369                    quote! {
370                        let #ident = match self.#ident {
371                            Some(inner) => Ok(inner),
372                            None => {
373                                let err = #private_path::FromEnvError::MissingValue {
374                                    path: #path.to_string(),
375                                };
376                                #errors_ident.add(err);
377                                Err(())
378                            }
379                        };
380                    }
381                }
382                (EnvAttribute::None, true) => {
383                    quote! {
384                        let #ident = self.#ident;
385                    }
386                }
387            }
388        });
389
390        let fields = self.get_fields().iter().map(|field| {
391            let ident = &field.ident;
392
393            quote! {
394                #ident: match #ident {
395                    Ok(val) => val,
396                    Err(_) => {
397                        return Err(#errors_ident);
398                    }
399                }
400            }
401        });
402
403        quote! {
404            impl #private_path::FromEnvBuilder for #builder_name {
405                type Target = #struct_name;
406
407                fn finalize(mut self) -> Result<Self::Target, #private_path::FromEnvErrors> {
408                    let mut #errors_ident = #private_path::FromEnvErrors::new();
409
410                    #(#assignments)*
411
412                    Ok(#struct_name {
413                        #(#fields,)*
414                    })
415                }
416            }
417        }
418    }
419
420    fn impl_builder(&self, consts: &ConstTokens) -> TokenStream {
421        let private_path = &consts.private_path;
422        let builder_name = &consts.builder_name;
423
424        let setters = self.get_fields().iter().map(|field| {
425            let ident = &field.ident;
426            let ty = &field.option.as_ref().unwrap_or(&field.ty);
427            let doc_attrs = &field.doc_attrs;
428
429            match &field.env_attr {
430                EnvAttribute::Nested => {
431                    quote! {
432                        #(#doc_attrs)*
433                        pub fn #ident<F>(mut self, f: F) -> Self
434                        where
435                            F: FnOnce(<#ty as #private_path::FromEnv>::FromEnvBuilder) -> <#ty as #private_path::FromEnv>::FromEnvBuilder,
436                        {
437                            let nested = self.#ident.take().unwrap();
438                            let nested = f(nested);
439                            self.#ident = Some(nested);
440                            self
441                        }
442                    }
443                }
444                EnvAttribute::Flat { .. } | EnvAttribute::None => {
445                    quote! {
446                        #(#doc_attrs)*
447                        pub fn #ident(mut self, #ident: #ty) -> Self {
448                            self.#ident = Some(#ident);
449                            self
450                        }
451                    }
452                }
453            }
454        });
455
456        quote! {
457            impl #builder_name {
458                #(#setters)*
459
460                pub fn finalize(self) -> Result<<Self as #private_path::FromEnvBuilder>::Target, #private_path::FromEnvErrors> {
461                    #private_path::FromEnvBuilder::finalize(self)
462                }
463            }
464        }
465    }
466
467    fn get_fields(&self) -> &Fields<FromEnvFieldReceiver> {
468        let Data::Struct(fields) = &self.data else {
469            panic!("we've asserted that it's a struct");
470        };
471
472        fields
473    }
474}
475
476fn parser_path(consts: &ConstTokens, path: Option<&ExprPath>) -> TokenStream {
477    let private_path = &consts.private_path;
478
479    if let Some(expr_path) = path {
480        if let Some(ident) = expr_path.path.get_ident() {
481            let ident_str = ident.to_string();
482
483            match ident_str.as_str() {
484                "from_str" => return quote!(#private_path::from_str),
485                "into" => return quote!(#private_path::into),
486                _ => {}
487            }
488        }
489
490        return quote!(#expr_path);
491    }
492
493    quote!(#private_path::from_str)
494}