1use proc_macro::TokenStream;
2use std::env;
3use std::fmt::{Display, Formatter};
4use darling::{FromDeriveInput, FromMeta};
5use proc_macro2::TokenStream as TokenStream2;
6use quote::quote;
7use syn::{parse_macro_input, DeriveInput};
8
9#[proc_macro_derive(Configuration, attributes(config))]
10pub fn derive_helper_attr(input: TokenStream) -> TokenStream {
11 let input: DeriveInput = parse_macro_input!(input);
12
13 let configuration = ConfigurationData::from_derive_input(&input).unwrap();
15 expand(configuration).into()
16}
17
18fn expand(configuration: ConfigurationData) -> TokenStream2 {
19 let ident = configuration.ident;
20 let prefix = configuration.prefix;
22 let files = configuration.file;
23 let envs = configuration.env;
24 let environment = configuration.environment;
25 let https = configuration.http;
26 let file_sources = file_source(envs, files);
28 let environment_source = environment_source(environment);
29 let http_sources = http_source(https);
30 let get_result = process_prefix(prefix, &ident);
31 quote! {
32 impl #ident {
34 pub fn config() -> &'static Self {
35 static CONFIG: ::std::sync::LazyLock<#ident> = ::std::sync::LazyLock::new(|| {
36 ::config_plus::Config::builder()
37 #(#file_sources)*
38 #environment_source
39 #(#http_sources)*
40 .build()
41 .unwrap()
42 #get_result
43 });
44 &CONFIG
45 }
46 }
47 }
48}
49
50fn file_source(envs:Vec<EnvData>, files: Vec<FileData>) -> Vec<TokenStream2> {
52 let mut ts = vec![];
53
54 for file in files {
55 let path = file.path;
56 let require = file.require;
57 let format = file.format;
58 match format {
59 Some(format) => {
60 let format = format.to_string();
61 ts.push(quote! {
62 .add_source(
63 ::config_plus::get_file(#path, #format).required(#require),
64 )
65 })
66 },
67 None => {
68 ts.push(quote! {
69 .add_source(
70 ::config_plus::File::with_name(#path).required(#require),
71 )
72 })
73 }
74 }
75 }
76
77 for env in envs {
78 let name = env.name;
79 let require = env.require;
80 let format = env.format;
81 match env::var(name) {
83 Ok(path) => {
84 let path = path.trim();
85 match format {
86 Some(format) => {
87 let format = format.to_string();
88 ts.push(quote! {
89 .add_source(
90 ::config_plus::get_file(#path, #format).required(#require),
91 )
92 })
93 },
94 None => {
95 ts.push(quote! {
96 .add_source(
97 ::config_plus::File::with_name(#path).required(#require),
98 )
99 })
100 }
101 }
102 },
103 Err(_) => {},
104 }
105 }
106 ts
107}
108
109fn environment_source(environment: bool) -> TokenStream2 {
111 let mut ts = quote! {};
112
113 if environment {
114 ts.extend(quote! {
115 .add_source(
116 ::config_plus::Environment::with_prefix("")
117 .separator(".")
118 .prefix_separator("")
119 .list_separator(",")
120 )
121 })
122 }
123 ts
124}
125
126fn http_source(https: Vec<HttpData>) -> Vec<TokenStream2> {
128 let mut ts = vec![];
129
130 for http in https {
131 let url = http.url;
132 let format = http.format.to_string();
133 let method = http.method.to_string();
134 ts.push(quote! {
135 .add_source(
136 ::config_plus::Http::with(#url, #format, #method)
137 )
138 })
139 }
140 ts
141}
142
143fn process_prefix(prefix: String, ident: &syn::Ident) -> TokenStream2 {
145 let mut ts = quote! {};
146
147 if prefix.is_empty() {
148 return quote! {
149 .try_deserialize()
150 .unwrap()
151 };
152 }
153 let split = prefix.split(".");
154
155 let count = split.clone().count();
157 for (index, str) in split.enumerate() {
158 if count == 1 {
159 ts.extend(quote! {
160 .get::<#ident>(#str)
161 .unwrap()
162 });
163 return ts;
164 }
165
166 if index == 0 {
167 ts.extend(quote! {
168 .get_table(#str)
169 .unwrap()
170 })
171 } else if count == index + 1 {
172 ts.extend(quote! {
173 .get(#str)
174 .ok_or_else(|| ::config_plus::ConfigError::NotFound(#str.into()))
175 .unwrap()
176 .clone()
177 .try_deserialize()
178 .unwrap()
179 })
180 } else {
181 ts.extend(quote! {
182 .get(#str)
183 .ok_or_else(|| ::config_plus::ConfigError::NotFound(#str.into()))
184 .unwrap()
185 .clone()
186 .into_table()
187 .unwrap()
188 })
189 }
190 }
191 ts
192}
193
194#[derive(Debug, FromDeriveInput)]
195#[darling(attributes(config), supports(struct_named))]
196struct ConfigurationData {
197 ident: syn::Ident,
199 #[darling(default)]
204 prefix: String,
205
206 #[darling(multiple, default)]
208 file: Vec<FileData>,
209
210 #[darling(multiple, default)]
212 env: Vec<EnvData>,
213
214 #[darling(multiple, default)]
216 http: Vec<HttpData>,
217
218 #[darling(default = env_default)]
220 environment: bool,
221}
222
223#[derive(Debug, FromMeta)]
225struct FileData{
226 path: String,
227 #[darling(default)]
228 require: bool,
229 format: Option<Format>,
230}
231
232#[derive(Debug, FromMeta)]
234struct EnvData{
235 name: String,
236 #[darling(default)]
237 require: bool,
238 format: Option<Format>,
239}
240
241#[derive(Debug, FromMeta)]
243struct HttpData{
244 url: String,
245 #[darling(default)]
246 method: Method,
247 format: Format,
248}
249
250#[derive(Debug, FromMeta)]
251enum Method {
252 Get,
253 Post,
254}
255
256impl Default for Method {
257 fn default() -> Self {
258 Method::Get
259 }
260}
261
262impl Display for Method{
263 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
264 match self {
265 Method::Get => write!(f, "get"),
266 Method::Post => write!(f, "post"),
267 }
268 }
269}
270
271#[derive(Debug, FromMeta)]
272enum Format {
273 Toml,
275
276 Json,
278
279 Yaml,
281
282 Yml,
284
285 Ini,
287
288 Ron,
290
291 Json5,
293}
294
295
296impl Display for Format{
297 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
298 match self {
299 Format::Toml => write!(f, "toml"),
300 Format::Json => write!(f, "json"),
301 Format::Yaml => write!(f, "yaml"),
302 Format::Yml => write!(f, "yaml"),
303 Format::Ini => write!(f, "ini"),
304 Format::Ron => write!(f, "ron"),
305 Format::Json5 => write!(f, "json5"),
306 }
307 }
308}
309
310fn env_default() -> bool {
311 true
312}