config_gen_macro_impl/
lib.rs

1mod env_attrs;
2mod utils;
3
4use proc_macro2::{Ident, TokenStream};
5use quote::{format_ident, quote};
6use syn::{parse_macro_input, punctuated::Punctuated, token::Comma, DeriveInput, Field};
7
8use crate::{
9  env_attrs::derive_env_loader,
10  utils::{get_fields, inner_type, type_is_vec},
11};
12
13#[proc_macro_derive(ConfigGenerator, attributes(env_key))]
14pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
15  let ast = parse_macro_input!(input as DeriveInput);
16
17  let old_struct_impls = derive_old_struct_impls(&ast);
18  let new_struct_ident = derive_optional_struct_name(&ast);
19  let fields = derive_optional_fields(&ast);
20  let new_struct_impls = derive_new_struct_impls(&ast);
21
22  let macro_output = quote! {
23    #old_struct_impls
24
25    #[derive(::serde::Deserialize)]
26    struct #new_struct_ident {
27      #fields
28    }
29
30    #new_struct_impls
31  };
32
33  macro_output.into()
34}
35
36fn derive_optional_struct_name(ast: &DeriveInput) -> Ident {
37  let original_struct_name = ast.ident.to_string();
38  format_ident!("Optional{}", original_struct_name)
39}
40
41fn derive_optional_fields(ast: &DeriveInput) -> TokenStream {
42  let fields = get_fields(ast);
43
44  let optional_fields = fields.iter().map(|field| {
45    let name = &field.ident;
46    let ty = &field.ty;
47
48    if inner_type("Option", ty).is_some() {
49      quote! { #name: ::std::option::#ty }
50    } else if type_is_vec(ty) {
51      quote! { #name: ::std::vec::#ty }
52    } else {
53      quote! { #name: ::std::option::Option<#ty> }
54    }
55  });
56
57  // Output interior portion of new struct definition
58  quote! {
59    #(#optional_fields),*
60  }
61}
62
63fn derive_old_struct_impls(ast: &DeriveInput) -> TokenStream {
64  let old_struct_ident = &ast.ident;
65  let new_struct_ident = derive_optional_struct_name(ast);
66
67  // Write new struct implementations for the input struct. These will define the from_toml and
68  // with_env functions for the struct, which are the main functionality exposed by this crate.
69
70  // We start with the with_env function, which loads from the environment.
71  // First we generate a Vec<TokenStream> of loaders for each field.
72  let env_loaders: Vec<TokenStream> = get_fields(ast)
73    .iter()
74    .map(|field| derive_env_loader(field))
75    .collect();
76  // And write the actual fn implementation into a TokenStream
77  let with_env_fn = quote! {
78    #[doc="Load values from enviroment variables configured with the `env_key` attribute and overwrite current values with them."]
79    pub fn with_env(mut self) -> Self {
80      let mut env_configs = #new_struct_ident::new();
81
82      #(#env_loaders)*
83
84      // Apply optional_config from environment and return
85      env_configs.apply_to(self)
86    }
87  };
88
89  // Then we move onto the with_toml fn. This is optional depending on what features are enabled, so we
90  // switch on that here.
91  #[cfg(not(feature = "load_toml"))]
92  let with_toml_fn = quote! {};
93  #[cfg(feature = "load_toml")]
94  let with_toml_fn = quote! {
95    #[doc="Load the toml file at `path` and overwrite current values with them."]
96    pub fn with_toml<P: ::core::convert::AsRef<::std::path::Path>>(mut self, path: &P) -> Self {
97      let file_contents = ::std::fs::read_to_string(path).unwrap();
98      let optional_config = ::toml::from_str::<#new_struct_ident>(file_contents.as_str()).unwrap();
99
100      // Apply loaded optional_config and return
101      optional_config.apply_to(self)
102    }
103  };
104
105  quote! {
106    impl #old_struct_ident {
107      #with_env_fn
108      #with_toml_fn
109    }
110  }
111}
112
113fn derive_new_struct_impls(ast: &DeriveInput) -> TokenStream {
114  let old_struct_ident = &ast.ident;
115  let new_struct_ident = derive_optional_struct_name(ast);
116  let fields = get_fields(ast);
117
118  let new_settings = fields.iter().map(|field| {
119    let name = &field.ident;
120    let ty = &field.ty;
121
122    if type_is_vec(ty) {
123      quote! { #name: ::std::vec::Vec::new() }
124    } else {
125      quote! { #name: ::std::option::Option::None }
126    }
127  });
128
129  let apply_to_fn = gen_apply_to_fn(old_struct_ident, &fields);
130
131  quote! {
132    impl #new_struct_ident {
133      pub fn new() -> Self {
134        Self {
135          #(#new_settings),*
136        }
137      }
138
139      #apply_to_fn
140    }
141  }
142}
143
144fn gen_apply_to_fn(old_struct_ident: &Ident, fields: &Punctuated<Field, Comma>) -> TokenStream {
145  // Build up a vector of field appliction tokenstreams
146  let field_applyors: Vec<TokenStream> = fields
147    .iter()
148    .map(|field| {
149      let name = &field.ident;
150      let ty = &field.ty;
151      if inner_type("Option", ty).is_some() {
152        quote! {
153          if self.#name.is_some() {
154            old.#name = self.#name.clone();
155          }
156        }
157      } else if type_is_vec(ty) {
158        quote! {
159          for item in &self.#name {
160            if !old.#name.contains(item) {
161              old.#name.push(item.clone());
162            }
163          }
164        }
165      } else {
166        quote! {
167          if let ::std::option::Option::Some(val) = &self.#name {
168            old.#name = val.clone();
169          }
170        }
171      }
172    })
173    .collect();
174
175  quote! {
176    pub fn apply_to(&self, mut old: #old_struct_ident) -> #old_struct_ident {
177      #(#field_applyors)*
178
179      old
180    }
181  }
182}