1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
use proc_macro::TokenStream as TokenStream1;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use syn::{Data, DataStruct, Error, Fields, Generics, Ident};
mod proc_macro_options;
use proc_macro_options::{collect_args_fields, FieldItem, StructItem};
pub(crate) mod util;
/// Derive a `Conf` implementation for an item with `#[conf(...)]` attributes
#[proc_macro_derive(Conf, attributes(conf, arg))]
pub fn conf(input: TokenStream1) -> TokenStream1 {
let input: DeriveInput = parse_macro_input!(input);
derive_conf(&input)
.unwrap_or_else(|error| error.to_compile_error())
.into()
}
fn derive_conf(input: &DeriveInput) -> Result<TokenStream, syn::Error> {
let ident = &input.ident;
match &input.data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => {
let struct_item = StructItem::new(ident, &input.attrs)?;
let fields = collect_args_fields(&struct_item, fields)?;
gen_conf_impl_for_struct(&struct_item, ident, &input.generics, &fields)
}
_ => Err(Error::new(
ident.span(),
"#[derive(Conf)] is only supported on structs with named fields",
)),
}
}
fn gen_conf_impl_for_struct(
struct_item: &StructItem,
item_name: &Ident,
generics: &Generics,
fields: &[FieldItem],
) -> Result<TokenStream, syn::Error> {
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let get_parser_config_impl = gen_conf_get_parser_config_impl_for_struct(struct_item, fields)?;
let get_program_options_impl =
gen_conf_get_program_options_impl_for_struct(struct_item, fields)?;
let from_conf_context_impl = gen_conf_from_conf_context_impl_for_struct(struct_item, fields)?;
let get_name_impl = gen_conf_get_name_impl_for_struct(struct_item)?;
Ok(quote! {
#[automatically_derived]
#[allow(
unused_qualifications,
)]
impl #impl_generics conf::Conf for #item_name #ty_generics #where_clause {
#get_parser_config_impl
#get_program_options_impl
#from_conf_context_impl
#get_name_impl
}
})
}
fn gen_conf_get_name_impl_for_struct(struct_item: &StructItem) -> Result<TokenStream, syn::Error> {
let struct_name = struct_item.get_ident().to_string();
Ok(quote! {
fn get_name() -> &'static str {
#struct_name
}
})
}
fn gen_conf_get_parser_config_impl_for_struct(
struct_item: &StructItem,
_fields: &[FieldItem],
) -> Result<TokenStream, syn::Error> {
// To implement Conf::get_parser_config, we need to get parser_config
// for this struct, (top-level config essentially).
let parser_config = struct_item.gen_parser_config()?;
Ok(quote! {
fn get_parser_config() -> Result<conf::ParserConfig, conf::Error> {
let parser_config = #parser_config;
Ok(parser_config)
}
})
}
fn gen_conf_get_program_options_impl_for_struct(
struct_item: &StructItem,
fields: &[FieldItem],
) -> Result<TokenStream, syn::Error> {
// To implement Conf::get_program_options, we need to
// get all the program options for our constituents. To do this, we create
// an ident for the list of program options, which is going to be Vec<ProgramOption>.
// Then we pass that ident to every constitutent field, and aggregate all their code gen.
let program_options_ident = Ident::new("program_options", Span::call_site());
let fields_push_program_options: Vec<TokenStream> = fields
.iter()
.map(|field| field.gen_push_program_options(&program_options_ident))
.collect::<Result<Vec<_>, syn::Error>>()?;
// To implement #[conf(env_prefix="ACME_")] on a struct (rather than on a flattened field),
// the code gen associated to the struct needs to be able to add its own prefixing during get_program_options
// and during from_conf_context.
// To do this, we allow the struct_item to "post-process" the Vec<ProgramOption>, (to add a prefix to them all)
// and to "pre-process" the ConfContext (to add a matching prefix to that before it is used)
// Note: The preprocessing no longer does anything since we switched to using id's like clap does.
let struct_post_process_program_options =
struct_item.gen_post_process_program_options(&program_options_ident)?;
// Note: fields_push_program_options is allowed to early return with ? on an error
Ok(quote! {
fn get_program_options() -> Result<&'static [conf::ProgramOption], conf::Error> {
static CACHED: ::std::sync::OnceLock<Vec<::conf::ProgramOption>> = ::std::sync::OnceLock::new();
if CACHED.get().is_none() {
let mut #program_options_ident = vec![];
#(#fields_push_program_options)*
#struct_post_process_program_options
let _ = CACHED.set(#program_options_ident);
}
let cached = CACHED.get().unwrap();
Ok(cached.as_ref())
}
})
}
fn gen_conf_from_conf_context_impl_for_struct(
struct_item: &StructItem,
fields: &[FieldItem],
) -> Result<TokenStream, syn::Error> {
// To implement Conf::from_conf_context, we need to take a conf context,
// and then return Ok(Self { ... }). For each constituent field, we need it
// to generate code to initialize itself properly. We pass the ConfContext ident
// to each consittuent field, and then aggregate all their code gen.
// Their code-gen is allowed to use `?` or `return Err(...)` to early return,
// but we still need to aggregate all the errors. Sample code gen is like.
//
// struct Sample {
// a: i32,
// b: i64,
// }
//
// from_conf_context(conf_context: &conf::ConfContext) -> Result<Self, Vec<conf::InnerError>> {
// let conf_context = #preprocess_conf_context;
// let mut errors = Vec::<conf::InnerError>::new();
//
// let a = match || -> Result<i32, conf::InnerError> { ... }() {
// Ok(val) => Some(val),
// Err(err) => {
// errors.push(err);
// None
// }
// };
//
// let b = match || -> Result<i64, conf::InnerError> { ... }() {
// Ok(val) => Some(val),
// Err(err) => {
// errors.push(err);
// None
// }
// };
//
// let return_value = match (a, b) {
// (Some(a), Some(b)) => Ok(Self {
// a,
// b,
// }),
// _ => Err(errors),
// }?;
//
// validation_predicate(&return_value).map_err(|err| vec![conf::InnerError::validation(&conf_context.id, err)])?;
//
// Ok(return_value)
// }
//
// The list of let a, let b... is called #initializations
// The match (a,b, ...) { ... } is called #return_value
// The validation_predicate(...) part is called #apply_validation_predicate
let conf_context_ident = Ident::new("__conf_context__", Span::call_site());
let errors_ident = Ident::new("__errors__", Span::call_site());
// For each field, intialize a local variable with Option<T> which is some if it worked and None if there were errors.
// Push all errors into #errors_ident.
let initializations: Vec<TokenStream> = fields
.iter()
.map(|field| -> Result<TokenStream, syn::Error> {
let field_name = field.get_field_name();
let field_type = field.get_field_type();
let (initializer, returns_multiple_errors) =
field.gen_initializer(&conf_context_ident)?;
// The initializer is the portion e.g.
// || -> Result<T, conf::InnerError> { ... }
//
// It returns `Result<T, Vec<conf::InnerError>>` if returns_multiple_errors is true, otherwise it's Result<T, conf::InnerError>
// It is allowed to read #conf_context_ident but not modify it
// We have to put it inside a locally defined fn so that it cannot modify the errors buffer etc.
Ok(if returns_multiple_errors {
quote! {
fn #field_name(#conf_context_ident: &::conf::ConfContext<'_>) -> Result<#field_type, Vec<::conf::InnerError>> {
#initializer
}
let #field_name = match #field_name(&#conf_context_ident) {
Ok(val) => Some(val),
Err(errs) => {
#errors_ident.extend(errs);
None
}
};
}
} else {
quote! {
fn #field_name(#conf_context_ident: &::conf::ConfContext<'_>) -> Result<#field_type, ::conf::InnerError> {
#initializer
}
let #field_name = match #field_name(&#conf_context_ident) {
Ok(val) => Some(val),
Err(err) => {
#errors_ident.push(err);
None
},
};
}
})
})
.collect::<Result<Vec<_>, syn::Error>>()?;
let field_names = fields
.iter()
.map(|field| field.get_field_name())
.collect::<Vec<_>>();
let return_value: TokenStream = quote! {
match (#(#field_names),*) {
(#(Some(#field_names)),*) => Ok( Self { #(#field_names),* } ),
_ => Err(#errors_ident)
}
};
let instance_ident = Ident::new("__instance__", Span::call_site());
let validation_routine =
struct_item.gen_validation_routine(&instance_ident, &conf_context_ident, fields)?;
let struct_ident = struct_item.get_ident();
let struct_pre_process_conf_context =
struct_item.gen_pre_process_conf_context(&conf_context_ident)?;
Ok(quote! {
fn from_conf_context<'a>(#conf_context_ident: ::conf::ConfContext<'a>) -> Result<Self, Vec<::conf::InnerError>> {
#struct_pre_process_conf_context
let mut #errors_ident = Vec::<::conf::InnerError>::new();
#(#initializations)*
let return_value = #return_value?;
fn validation<'a>(#instance_ident: & #struct_ident, #conf_context_ident: ::conf::ConfContext<'a>) -> Result<(), Vec<::conf::InnerError>> {
#validation_routine
}
validation(&return_value, #conf_context_ident)?;
Ok(return_value)
}
})
}