1use darling::util::Flag;
7use darling::{FromDeriveInput, FromField, ast};
8use proc_macro::TokenStream;
9use quote::quote;
10use syn::{DeriveInput, parse_macro_input};
11
12#[derive(Clone)]
18struct MetaExpr(#[allow(dead_code)] syn::Expr);
19
20impl darling::FromMeta for MetaExpr {
21 fn from_expr(expr: &syn::Expr) -> darling::Result<Self> {
22 Ok(MetaExpr(expr.clone()))
23 }
24
25 fn from_value(value: &syn::Lit) -> darling::Result<Self> {
26 Ok(MetaExpr(lit_to_expr(value)))
27 }
28}
29
30#[derive(Clone)]
37enum DefaultKind {
38 Inherit,
39 Explicit(syn::Expr),
40}
41
42impl darling::FromMeta for DefaultKind {
43 fn from_word() -> darling::Result<Self> {
44 Ok(DefaultKind::Inherit)
45 }
46
47 fn from_expr(expr: &syn::Expr) -> darling::Result<Self> {
48 Ok(DefaultKind::Explicit(expr.clone()))
49 }
50
51 fn from_value(value: &syn::Lit) -> darling::Result<Self> {
52 Ok(DefaultKind::Explicit(lit_to_expr(value)))
53 }
54}
55
56fn lit_to_expr(lit: &syn::Lit) -> syn::Expr {
57 syn::Expr::Lit(syn::ExprLit {
58 attrs: Vec::new(),
59 lit: lit.clone(),
60 })
61}
62
63#[derive(FromField)]
64#[darling(attributes(confroid), forward_attrs(doc))]
65#[allow(dead_code)]
66struct FieldOpts {
67 ident: Option<syn::Ident>,
68 ty: syn::Type,
69 name: Option<String>,
71 default: Option<DefaultKind>,
73 example: Option<MetaExpr>,
75 auto_vec: Flag,
77 auto_vec_delimiter: Option<String>,
79 attrs: Vec<syn::Attribute>,
81}
82
83#[derive(FromDeriveInput)]
84#[darling(attributes(confroid), supports(struct_named))]
85struct InputOpts {
86 ident: syn::Ident,
87 generics: syn::Generics,
88 data: ast::Data<darling::util::Ignored, FieldOpts>,
89 prefix: Option<String>,
91}
92
93#[proc_macro_derive(Config, attributes(confroid))]
171pub fn derive_config(input: TokenStream) -> TokenStream {
172 let di = parse_macro_input!(input as DeriveInput);
173 let opts = match InputOpts::from_derive_input(&di) {
174 Ok(opts) => opts,
175 Err(err) => return err.write_errors().into(),
176 };
177 match expand(&opts) {
178 Ok(tokens) => tokens.into(),
179 Err(err) => err.write_errors().into(),
180 }
181}
182
183fn expand(opts: &InputOpts) -> darling::Result<proc_macro2::TokenStream> {
184 let ident = &opts.ident;
185 let prefix = opts.prefix.as_deref().unwrap_or("");
186 let (impl_generics, ty_generics, where_clause) = opts.generics.split_for_impl();
187
188 let fields = opts
189 .data
190 .as_ref()
191 .take_struct()
192 .expect("supports(struct_named) guarantees a struct")
193 .fields;
194
195 let mut errors = darling::Error::accumulator();
196 let mut field_inits = Vec::with_capacity(fields.len());
197 let mut field_docs = Vec::with_capacity(fields.len());
198
199 for field in fields {
200 let fname = field
201 .ident
202 .as_ref()
203 .expect("named struct fields always have an ident");
204 let fname_str = fname.to_string();
205 let var_seg = field
206 .name
207 .clone()
208 .unwrap_or_else(|| screaming_snake(&fname_str));
209 let fty = &field.ty;
210
211 let read_expr = if field.auto_vec.is_present() {
213 match vec_inner(fty) {
214 Some(elem_ty) => {
215 let delim = field
216 .auto_vec_delimiter
217 .clone()
218 .unwrap_or_else(|| ",".to_string());
219 auto_vec_read(&var_seg, &fname_str, elem_ty, &delim)
220 }
221 None => {
222 errors.push(
223 darling::Error::custom("`auto_vec` requires a `Vec<T>` field")
224 .with_span(fty),
225 );
226 continue;
227 }
228 }
229 } else {
230 quote! {
231 {
232 let __ctx = ctx.field(#var_seg, #fname_str);
233 <#fty as ::confroid::FromEnv>::from_env(&__ctx, env)
234 }
235 }
236 };
237
238 let init = match &field.default {
239 None => quote! { #fname: (#read_expr)? },
240 Some(DefaultKind::Inherit) => quote! {
241 #fname: match #read_expr {
242 ::core::result::Result::Ok(__v) => __v,
243 ::core::result::Result::Err(::confroid::ConfroidError::EnvVarNotFound { .. }) =>
244 <#fty as ::core::default::Default>::default(),
245 ::core::result::Result::Err(__e) => return ::core::result::Result::Err(__e),
246 }
247 },
248 Some(DefaultKind::Explicit(expr)) => {
249 let default_val = default_value(expr);
250 quote! {
251 #fname: match #read_expr {
252 ::core::result::Result::Ok(__v) => __v,
253 ::core::result::Result::Err(::confroid::ConfroidError::EnvVarNotFound { .. }) =>
254 #default_val,
255 ::core::result::Result::Err(__e) => return ::core::result::Result::Err(__e),
256 }
257 }
258 }
259 };
260
261 field_inits.push(init);
262
263 let doc_tokens = match extract_doc(&field.attrs) {
265 Some(doc) => quote! { ::core::option::Option::Some(#doc) },
266 None => quote! { ::core::option::Option::None },
267 };
268 let is_collection = type_is_named(fty, "Vec") || type_is_named(fty, "HashMap");
272 let is_option = type_is_named(fty, "Option");
273 let none = quote! { ::core::option::Option::None };
274 let default_doc = match &field.default {
275 Some(DefaultKind::Inherit) if !is_collection && !is_option => quote! {
276 ::core::option::Option::Some(
277 ::std::format!("{}", <#fty as ::core::default::Default>::default())
278 )
279 },
280 Some(DefaultKind::Explicit(expr)) if !is_collection => quote! {
281 ::core::option::Option::Some(::std::format!("{}", #expr))
282 },
283 _ => none.clone(),
284 };
285 let example_doc = match &field.example {
286 Some(MetaExpr(expr)) if !is_collection => quote! {
287 ::core::option::Option::Some(::std::format!("{}", #expr))
288 },
289 _ => none.clone(),
290 };
291 field_docs.push(quote! {
292 ::confroid::FieldDoc {
293 var_seg: #var_seg,
294 name: #fname_str,
295 doc: #doc_tokens,
296 default: #default_doc,
297 example: #example_doc,
298 children: <#fty as ::confroid::Documented>::doc_fields(),
299 }
300 });
301 }
302
303 errors.finish()?;
304
305 let documented_fields = if prefix.is_empty() {
306 quote! { ::std::vec![#(#field_docs),*] }
307 } else {
308 quote! {
309 ::std::vec![::confroid::FieldDoc {
310 var_seg: #prefix,
311 name: "",
312 doc: ::core::option::Option::None,
313 default: ::core::option::Option::None,
314 example: ::core::option::Option::None,
315 children: ::core::option::Option::Some(::std::vec![#(#field_docs),*]),
316 }]
317 }
318 };
319
320 let documented_impl = if cfg!(feature = "docs") {
321 let ident_str = ident.to_string();
322 quote! {
323 #[automatically_derived]
324 impl #impl_generics ::confroid::Documented for #ident #ty_generics #where_clause {
325 fn doc_name() -> &'static str {
326 #ident_str
327 }
328
329 fn doc_fields() -> ::core::option::Option<::std::vec::Vec<::confroid::FieldDoc>> {
330 ::core::option::Option::Some(#documented_fields)
331 }
332 }
333 }
334 } else {
335 quote! {}
336 };
337
338 Ok(quote! {
339 #[automatically_derived]
340 impl #impl_generics ::confroid::FromEnv for #ident #ty_generics #where_clause {
341 fn from_env(
342 ctx: &::confroid::Ctx,
343 env: &::confroid::Env,
344 ) -> ::confroid::Result<Self> {
345 let __confroid_ctx = ctx.prefix(#prefix);
346 let ctx = &__confroid_ctx;
347 ::core::result::Result::Ok(Self {
348 #(#field_inits),*
349 })
350 }
351
352 fn is_present(ctx: &::confroid::Ctx, env: &::confroid::Env) -> bool {
353 let __confroid_ctx = ctx.prefix(#prefix);
354 !env.immediate_children(&__confroid_ctx.var).is_empty()
355 }
356 }
357
358 #documented_impl
359 })
360}
361
362fn auto_vec_read(
364 var_seg: &str,
365 fname_str: &str,
366 elem_ty: &syn::Type,
367 delim: &str,
368) -> proc_macro2::TokenStream {
369 quote! {
370 {
371 let __ctx = ctx.field(#var_seg, #fname_str);
372 match env.get(&__ctx.var) {
373 ::core::option::Option::None => ::core::result::Result::Err(
374 ::confroid::ConfroidError::EnvVarNotFound {
375 var_name: __ctx.var.clone(),
376 field: __ctx.path.clone(),
377 }
378 ),
379 ::core::option::Option::Some(__raw) if __raw.is_empty() =>
380 ::core::result::Result::Ok(::std::vec::Vec::new()),
381 ::core::option::Option::Some(__raw) => {
382 let mut __out: ::std::vec::Vec<#elem_ty> = ::std::vec::Vec::new();
383 let mut __result = ::core::result::Result::Ok(());
384 for __part in __raw.split(#delim) {
385 match __part.parse::<#elem_ty>() {
386 ::core::result::Result::Ok(__v) => __out.push(__v),
387 ::core::result::Result::Err(__e) => {
388 __result = ::core::result::Result::Err(
389 ::confroid::ConfroidError::EnvVarInvalid {
390 var_name: __ctx.var.clone(),
391 field: __ctx.path.clone(),
392 value: __part.to_string(),
393 parser_error: __e.to_string(),
394 }
395 );
396 break;
397 }
398 }
399 }
400 __result.map(|()| __out)
401 }
402 }
403 }
404 }
405}
406
407fn default_value(expr: &syn::Expr) -> proc_macro2::TokenStream {
413 if is_numeric_literal(expr) {
414 quote! { #expr }
415 } else {
416 quote! { ::core::convert::Into::into(#expr) }
417 }
418}
419
420fn is_numeric_literal(expr: &syn::Expr) -> bool {
421 match expr {
422 syn::Expr::Lit(syn::ExprLit { lit, .. }) => {
423 matches!(lit, syn::Lit::Int(_) | syn::Lit::Float(_))
424 }
425 syn::Expr::Unary(syn::ExprUnary {
426 op: syn::UnOp::Neg(_),
427 expr,
428 ..
429 }) => is_numeric_literal(expr),
430 syn::Expr::Group(g) => is_numeric_literal(&g.expr),
431 syn::Expr::Paren(p) => is_numeric_literal(&p.expr),
432 _ => false,
433 }
434}
435
436fn type_is_named(ty: &syn::Type, name: &str) -> bool {
438 matches!(ty, syn::Type::Path(tp) if tp.path.segments.last().is_some_and(|s| s.ident == name))
439}
440
441fn vec_inner(ty: &syn::Type) -> Option<&syn::Type> {
443 let syn::Type::Path(tp) = ty else {
444 return None;
445 };
446 let seg = tp.path.segments.last()?;
447 if seg.ident != "Vec" {
448 return None;
449 }
450 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
451 return None;
452 };
453 args.args.iter().find_map(|arg| match arg {
454 syn::GenericArgument::Type(inner) => Some(inner),
455 _ => None,
456 })
457}
458
459fn extract_doc(attrs: &[syn::Attribute]) -> Option<String> {
461 let mut lines = Vec::new();
462 for attr in attrs {
463 if !attr.path().is_ident("doc") {
464 continue;
465 }
466 if let syn::Meta::NameValue(nv) = &attr.meta
467 && let syn::Expr::Lit(syn::ExprLit {
468 lit: syn::Lit::Str(s),
469 ..
470 }) = &nv.value
471 {
472 lines.push(s.value().trim().to_string());
473 }
474 }
475 if lines.is_empty() {
476 None
477 } else {
478 Some(lines.join(" "))
479 }
480}
481
482fn screaming_snake(s: &str) -> String {
484 let mut out = String::new();
485 let mut prev_is_word = false;
486 for ch in s.chars() {
487 if ch == '_' {
488 out.push('_');
489 prev_is_word = false;
490 continue;
491 }
492 if ch.is_uppercase() && prev_is_word {
493 out.push('_');
494 }
495 out.extend(ch.to_uppercase());
496 prev_is_word = ch.is_lowercase() || ch.is_ascii_digit();
497 }
498 out
499}