env_settings_derive/
lib.rs

1#![deny(missing_docs)]
2#![doc(
3    html_logo_url = "https://raw.githubusercontent.com/dariocurr/env-settings/main/docs/logo.svg",
4    html_favicon_url = "https://raw.githubusercontent.com/dariocurr/env-settings/main/docs/logo.ico"
5)]
6
7//! # Env Settings Derive
8
9use proc_macro::TokenStream;
10use quote::quote;
11use std::collections::HashMap;
12use syn::parse;
13
14mod utils;
15
16/// The macro to add the `Derive` functionality
17#[proc_macro_derive(EnvSettings, attributes(env_settings))]
18pub fn env_settings_derive(input: TokenStream) -> TokenStream {
19    // Construct a representation of Rust code as a syntax tree that we can manipulate
20    let input = parse(input).unwrap();
21
22    // Build the trait implementation
23    implement(&input)
24}
25
26/// Implement the logic of the derive macro
27fn implement(input: &utils::input::EnvSettingsInput) -> TokenStream {
28    let struct_name = &input.name;
29
30    let mut new_args = Vec::new();
31    let mut new_impls = Vec::new();
32    let mut from_env_impls = Vec::new();
33    let mut from_env_args = Vec::new();
34
35    let mut env_variables_impls = quote! {};
36    let mut file_path_impls = quote! {};
37
38    if let Some(file_path) = &input.params.file_path {
39        if input.params.delay {
40            file_path_impls = quote! { env_settings_utils::load_env_file_path(#file_path)?; }
41        } else {
42            env_settings_utils::load_env_file_path(file_path).unwrap();
43        }
44    }
45
46    let case_insensitive = input.params.case_insensitive;
47
48    let env_variables = if input.params.delay {
49        env_variables_impls = quote! {
50            let env_variables = env_settings_utils::get_env_variables(#case_insensitive);
51        };
52        HashMap::new()
53    } else {
54        env_settings_utils::get_env_variables(case_insensitive)
55    };
56
57    let prefix = input.params.prefix.clone().unwrap_or_default();
58
59    for field in &input.fields {
60        match field {
61            utils::field::EnvSettingsField::NonParsable(non_parsable_field) => {
62                let name = &non_parsable_field.name;
63                let type_ = &non_parsable_field.type_;
64                let argument = quote! { #name: #type_ };
65                new_args.push(argument.clone());
66                from_env_args.push(argument);
67                let value = quote! {#name};
68                new_impls.push(value.clone());
69                from_env_impls.push(value);
70            }
71            utils::field::EnvSettingsField::Parsable(parsable_field) => {
72                let name = &parsable_field.name;
73                let name_label = &parsable_field.name_label;
74                let type_ = &parsable_field.type_;
75                let type_label = &parsable_field.type_label;
76                let optional_type = &parsable_field.optional_type;
77
78                let mut env_variable = parsable_field
79                    .variable
80                    .to_owned()
81                    .unwrap_or(format!("{prefix}{name}"));
82                if case_insensitive {
83                    env_variable = env_variable.to_lowercase();
84                }
85
86                // the variable involved must be named `value`
87                let (optional_value_impl, default_value_impl, new_arg_impl, parse_type) =
88                    match optional_type {
89                        Some(optional_type) => (
90                            quote! { Some(value) },
91                            quote! { None },
92                            quote! { #name: #type_ },
93                            optional_type,
94                        ),
95                        None => (
96                            quote! { value },
97                            quote! { return Err(env_settings_utils::EnvSettingsError::NotExists(#env_variable)) },
98                            quote! { #name: Option<#type_> },
99                            type_,
100                        ),
101                    };
102
103                // the variable involved must be named `value_to_parse`
104                let convert_err_impl = quote! {
105                    return Err(env_settings_utils::EnvSettingsError::Convert(
106                        #name_label,
107                        value_to_parse.to_owned(),
108                        #type_label,
109                    ))
110                };
111                let default_impl = match &parsable_field.default {
112                    Some(value_to_parse) => {
113                        quote! {
114                            match #value_to_parse.parse::<#parse_type>() {
115                                Ok(value) => #optional_value_impl,
116                                Err(_) => {
117                                    let value_to_parse = #value_to_parse;
118                                    #convert_err_impl
119                                }
120                            }
121                        }
122                    }
123                    None => default_value_impl,
124                };
125
126                // the variable involved must be named `value_to_parse`
127                let parse_impl = quote! {
128                    match value_to_parse.parse::<#parse_type>() {
129                        Ok(value) => #optional_value_impl,
130                        Err(_) => #convert_err_impl
131                    }
132                };
133
134                // the variable involved must be named `env_variables`
135                let env_value_impl = if input.params.delay {
136                    quote! {
137                        match env_variables.get(#env_variable) {
138                            Some(value_to_parse) => {#parse_impl},
139                            None => #default_impl,
140                        }
141                    }
142                } else {
143                    match env_variables.get(&env_variable) {
144                        Some(value_to_parse) => quote! {
145                            let value_to_parse = #value_to_parse;
146                            #parse_impl
147                        },
148                        None => default_impl,
149                    }
150                };
151
152                new_impls.push(quote! {
153                    #name: match #name {
154                        Some(value) => #optional_value_impl,
155                        None => #env_value_impl
156                    }
157                });
158                new_args.push(new_arg_impl);
159                from_env_impls.push(quote! { #name: #env_value_impl });
160            }
161        }
162    }
163
164    let pre_impls = quote! {
165        #file_path_impls
166        #env_variables_impls
167    };
168
169    let generated_impl = quote! {
170
171        impl #struct_name {
172
173            /// Create a new instance using just the environment variables. Skipped fields must be passed.
174            /// If something fails, it returns an `env_settings_utils::EnvSettingsError` error
175            #[allow(clippy::too_many_arguments)]
176            pub fn new(#(#new_args),*) -> env_settings_utils::EnvSettingsResult<Self> {
177                #pre_impls
178                let instance = Self {
179                    #(#new_impls),*
180                };
181                Ok(instance)
182            }
183
184            /// Create a new instance using a combination of environment variables and parameters.
185            /// More in detail, every field that can be initialized by the environment variables can be passed
186            /// as parameter wrapped in an `Option` object. Then if the parameter is `Some`, it is used,
187            /// otherwise the value is recoved from the environment variables. Skipped fields must be passed.
188            /// If something fails, it returns an `env_settings_utils::EnvSettingsError` error
189            #[allow(clippy::too_many_arguments)]
190            pub fn from_env(#(#from_env_args),*) -> env_settings_utils::EnvSettingsResult<Self> {
191                #pre_impls
192                let instance = Self {
193                    #(#from_env_impls),*
194                };
195                Ok(instance)
196            }
197
198        }
199
200    };
201
202    generated_impl.into()
203}