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}
90
91#[proc_macro_derive(Config, attributes(confroid))]
93pub fn derive_config(input: TokenStream) -> TokenStream {
94 let di = parse_macro_input!(input as DeriveInput);
95 let opts = match InputOpts::from_derive_input(&di) {
96 Ok(opts) => opts,
97 Err(err) => return err.write_errors().into(),
98 };
99 match expand(&opts) {
100 Ok(tokens) => tokens.into(),
101 Err(err) => err.write_errors().into(),
102 }
103}
104
105fn expand(opts: &InputOpts) -> darling::Result<proc_macro2::TokenStream> {
106 let ident = &opts.ident;
107 let (impl_generics, ty_generics, where_clause) = opts.generics.split_for_impl();
108
109 let fields = opts
110 .data
111 .as_ref()
112 .take_struct()
113 .expect("supports(struct_named) guarantees a struct")
114 .fields;
115
116 let mut errors = darling::Error::accumulator();
117 let mut field_inits = Vec::with_capacity(fields.len());
118 let mut field_docs = Vec::with_capacity(fields.len());
119
120 for field in fields {
121 let fname = field
122 .ident
123 .as_ref()
124 .expect("named struct fields always have an ident");
125 let fname_str = fname.to_string();
126 let var_seg = field
127 .name
128 .clone()
129 .unwrap_or_else(|| screaming_snake(&fname_str));
130 let fty = &field.ty;
131
132 let read_expr = if field.auto_vec.is_present() {
134 match vec_inner(fty) {
135 Some(elem_ty) => {
136 let delim = field
137 .auto_vec_delimiter
138 .clone()
139 .unwrap_or_else(|| ",".to_string());
140 auto_vec_read(&var_seg, &fname_str, elem_ty, &delim)
141 }
142 None => {
143 errors.push(
144 darling::Error::custom("`auto_vec` requires a `Vec<T>` field")
145 .with_span(fty),
146 );
147 continue;
148 }
149 }
150 } else {
151 quote! {
152 {
153 let __ctx = ctx.field(#var_seg, #fname_str);
154 <#fty as ::confroid::FromEnv>::from_env(&__ctx, env)
155 }
156 }
157 };
158
159 let init = match &field.default {
160 None => quote! { #fname: (#read_expr)? },
161 Some(DefaultKind::Inherit) => quote! {
162 #fname: match #read_expr {
163 ::core::result::Result::Ok(__v) => __v,
164 ::core::result::Result::Err(::confroid::ConfroidError::EnvVarNotFound { .. }) =>
165 <#fty as ::core::default::Default>::default(),
166 ::core::result::Result::Err(__e) => return ::core::result::Result::Err(__e),
167 }
168 },
169 Some(DefaultKind::Explicit(expr)) => {
170 let default_val = default_value(expr);
171 quote! {
172 #fname: match #read_expr {
173 ::core::result::Result::Ok(__v) => __v,
174 ::core::result::Result::Err(::confroid::ConfroidError::EnvVarNotFound { .. }) =>
175 #default_val,
176 ::core::result::Result::Err(__e) => return ::core::result::Result::Err(__e),
177 }
178 }
179 }
180 };
181
182 field_inits.push(init);
183
184 let doc_tokens = match extract_doc(&field.attrs) {
186 Some(doc) => quote! { ::core::option::Option::Some(#doc) },
187 None => quote! { ::core::option::Option::None },
188 };
189 let is_collection = type_is_named(fty, "Vec") || type_is_named(fty, "HashMap");
193 let is_option = type_is_named(fty, "Option");
194 let none = quote! { ::core::option::Option::None };
195 let default_doc = match &field.default {
196 Some(DefaultKind::Inherit) if !is_collection && !is_option => quote! {
197 ::core::option::Option::Some(
198 ::std::format!("{}", <#fty as ::core::default::Default>::default())
199 )
200 },
201 Some(DefaultKind::Explicit(expr)) if !is_collection => quote! {
202 ::core::option::Option::Some(::std::format!("{}", #expr))
203 },
204 _ => none.clone(),
205 };
206 let example_doc = match &field.example {
207 Some(MetaExpr(expr)) if !is_collection => quote! {
208 ::core::option::Option::Some(::std::format!("{}", #expr))
209 },
210 _ => none.clone(),
211 };
212 field_docs.push(quote! {
213 ::confroid::FieldDoc {
214 var_seg: #var_seg,
215 name: #fname_str,
216 doc: #doc_tokens,
217 default: #default_doc,
218 example: #example_doc,
219 children: <#fty as ::confroid::Documented>::doc_fields(),
220 }
221 });
222 }
223
224 errors.finish()?;
225
226 let documented_impl = if cfg!(feature = "docs") {
227 let ident_str = ident.to_string();
228 quote! {
229 #[automatically_derived]
230 impl #impl_generics ::confroid::Documented for #ident #ty_generics #where_clause {
231 fn doc_name() -> &'static str {
232 #ident_str
233 }
234
235 fn doc_fields() -> ::core::option::Option<::std::vec::Vec<::confroid::FieldDoc>> {
236 ::core::option::Option::Some(::std::vec![
237 #(#field_docs),*
238 ])
239 }
240 }
241 }
242 } else {
243 quote! {}
244 };
245
246 Ok(quote! {
247 #[automatically_derived]
248 impl #impl_generics ::confroid::FromEnv for #ident #ty_generics #where_clause {
249 fn from_env(
250 ctx: &::confroid::Ctx,
251 env: &::confroid::Env,
252 ) -> ::confroid::Result<Self> {
253 ::core::result::Result::Ok(Self {
254 #(#field_inits),*
255 })
256 }
257
258 fn is_present(ctx: &::confroid::Ctx, env: &::confroid::Env) -> bool {
259 !env.immediate_children(&ctx.var).is_empty()
260 }
261 }
262
263 #documented_impl
264 })
265}
266
267fn auto_vec_read(
269 var_seg: &str,
270 fname_str: &str,
271 elem_ty: &syn::Type,
272 delim: &str,
273) -> proc_macro2::TokenStream {
274 quote! {
275 {
276 let __ctx = ctx.field(#var_seg, #fname_str);
277 match env.get(&__ctx.var) {
278 ::core::option::Option::None => ::core::result::Result::Err(
279 ::confroid::ConfroidError::EnvVarNotFound {
280 var_name: __ctx.var.clone(),
281 field: __ctx.path.clone(),
282 }
283 ),
284 ::core::option::Option::Some(__raw) if __raw.is_empty() =>
285 ::core::result::Result::Ok(::std::vec::Vec::new()),
286 ::core::option::Option::Some(__raw) => {
287 let mut __out: ::std::vec::Vec<#elem_ty> = ::std::vec::Vec::new();
288 let mut __result = ::core::result::Result::Ok(());
289 for __part in __raw.split(#delim) {
290 match __part.parse::<#elem_ty>() {
291 ::core::result::Result::Ok(__v) => __out.push(__v),
292 ::core::result::Result::Err(__e) => {
293 __result = ::core::result::Result::Err(
294 ::confroid::ConfroidError::EnvVarInvalid {
295 var_name: __ctx.var.clone(),
296 field: __ctx.path.clone(),
297 value: __part.to_string(),
298 parser_error: __e.to_string(),
299 }
300 );
301 break;
302 }
303 }
304 }
305 __result.map(|()| __out)
306 }
307 }
308 }
309 }
310}
311
312fn default_value(expr: &syn::Expr) -> proc_macro2::TokenStream {
318 if is_numeric_literal(expr) {
319 quote! { #expr }
320 } else {
321 quote! { ::core::convert::Into::into(#expr) }
322 }
323}
324
325fn is_numeric_literal(expr: &syn::Expr) -> bool {
326 match expr {
327 syn::Expr::Lit(syn::ExprLit { lit, .. }) => {
328 matches!(lit, syn::Lit::Int(_) | syn::Lit::Float(_))
329 }
330 syn::Expr::Unary(syn::ExprUnary {
331 op: syn::UnOp::Neg(_),
332 expr,
333 ..
334 }) => is_numeric_literal(expr),
335 syn::Expr::Group(g) => is_numeric_literal(&g.expr),
336 syn::Expr::Paren(p) => is_numeric_literal(&p.expr),
337 _ => false,
338 }
339}
340
341fn type_is_named(ty: &syn::Type, name: &str) -> bool {
343 matches!(ty, syn::Type::Path(tp) if tp.path.segments.last().is_some_and(|s| s.ident == name))
344}
345
346fn vec_inner(ty: &syn::Type) -> Option<&syn::Type> {
348 let syn::Type::Path(tp) = ty else {
349 return None;
350 };
351 let seg = tp.path.segments.last()?;
352 if seg.ident != "Vec" {
353 return None;
354 }
355 let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
356 return None;
357 };
358 args.args.iter().find_map(|arg| match arg {
359 syn::GenericArgument::Type(inner) => Some(inner),
360 _ => None,
361 })
362}
363
364fn extract_doc(attrs: &[syn::Attribute]) -> Option<String> {
366 let mut lines = Vec::new();
367 for attr in attrs {
368 if !attr.path().is_ident("doc") {
369 continue;
370 }
371 if let syn::Meta::NameValue(nv) = &attr.meta
372 && let syn::Expr::Lit(syn::ExprLit {
373 lit: syn::Lit::Str(s),
374 ..
375 }) = &nv.value
376 {
377 lines.push(s.value().trim().to_string());
378 }
379 }
380 if lines.is_empty() {
381 None
382 } else {
383 Some(lines.join(" "))
384 }
385}
386
387fn screaming_snake(s: &str) -> String {
389 let mut out = String::new();
390 let mut prev_is_word = false;
391 for ch in s.chars() {
392 if ch == '_' {
393 out.push('_');
394 prev_is_word = false;
395 continue;
396 }
397 if ch.is_uppercase() && prev_is_word {
398 out.push('_');
399 }
400 out.extend(ch.to_uppercase());
401 prev_is_word = ch.is_lowercase() || ch.is_ascii_digit();
402 }
403 out
404}