1use proc_macro::TokenStream;
6use proc_macro2::TokenStream as TokenStream2;
7use quote::quote;
8use syn::{
9 parse::{Parse, ParseStream},
10 parse_macro_input, Attribute, Data, DeriveInput, Fields, Ident, Lit, LitStr, Meta, Token, Type,
11};
12
13#[derive(Debug, Clone)]
16enum FieldKind {
17 Str,
18 Bool,
19 Num(proc_macro2::TokenStream), OptStr,
21 OptBool,
22 OptNum(proc_macro2::TokenStream),
23 Other,
24}
25
26fn classify(ty: &Type) -> FieldKind {
27 let Type::Path(tp) = ty else {
28 return FieldKind::Other;
29 };
30 let segs = &tp.path.segments;
31 if segs.is_empty() {
32 return FieldKind::Other;
33 }
34 let last = segs.last().unwrap();
35 let name = last.ident.to_string();
36
37 match name.as_str() {
38 "String" => FieldKind::Str,
39 "bool" => FieldKind::Bool,
40 n @ ("u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128"
41 | "f32" | "f64" | "usize" | "isize") => {
42 let ident = Ident::new(n, proc_macro2::Span::call_site());
43 FieldKind::Num(quote! { #ident })
44 }
45 "Option" => {
46 if let syn::PathArguments::AngleBracketed(ab) = &last.arguments {
47 if let Some(syn::GenericArgument::Type(inner)) = ab.args.first() {
48 return match classify(inner) {
49 FieldKind::Str => FieldKind::OptStr,
50 FieldKind::Bool => FieldKind::OptBool,
51 FieldKind::Num(t) => FieldKind::OptNum(t),
52 _ => FieldKind::Other,
53 };
54 }
55 }
56 FieldKind::Other
57 }
58 _ => FieldKind::Other,
59 }
60}
61
62struct EnvAttr {
65 var_name: LitStr,
66 default: Option<Lit>,
67}
68
69impl Parse for EnvAttr {
70 fn parse(input: ParseStream) -> syn::Result<Self> {
71 let var_name: LitStr = input.parse()?;
72 let default = if input.peek(Token![,]) {
73 input.parse::<Token![,]>()?;
74 let key: Ident = input.parse()?;
75 if key != "default" {
76 return Err(syn::Error::new(key.span(), "expected `default`"));
77 }
78 input.parse::<Token![=]>()?;
79 Some(input.parse::<Lit>()?)
80 } else {
81 None
82 };
83 Ok(EnvAttr { var_name, default })
84 }
85}
86
87#[proc_macro_derive(Config, attributes(env))]
115pub fn derive_config(input: TokenStream) -> TokenStream {
116 let input = parse_macro_input!(input as DeriveInput);
117 expand_config(input).unwrap_or_else(|e| e.to_compile_error().into())
118}
119
120fn expand_config(input: DeriveInput) -> syn::Result<TokenStream> {
121 let name = &input.ident;
122
123 let Data::Struct(data) = &input.data else {
124 return Err(syn::Error::new_spanned(
125 &input.ident,
126 "#[derive(Config)] only supports structs",
127 ));
128 };
129 let Fields::Named(fields) = &data.fields else {
130 return Err(syn::Error::new_spanned(
131 &input.ident,
132 "#[derive(Config)] only supports structs with named fields",
133 ));
134 };
135
136 let mut field_inits: Vec<TokenStream2> = Vec::new();
137
138 for field in &fields.named {
139 let field_ident = field.ident.as_ref().unwrap();
140 let kind = classify(&field.ty);
141
142 let env_attr = field
144 .attrs
145 .iter()
146 .find(|a| a.path().is_ident("env"))
147 .ok_or_else(|| {
148 syn::Error::new_spanned(
149 field_ident,
150 "each field must have an #[env(\"VAR_NAME\")] attribute",
151 )
152 })?;
153
154 let parsed: EnvAttr = env_attr.parse_args()?;
155 let var_name = &parsed.var_name;
156 let var_str = var_name.value();
157
158 let init = match kind {
159 FieldKind::Str => match &parsed.default {
160 Some(Lit::Str(default)) => quote! {
161 #field_ident: ::std::env::var(#var_name).unwrap_or_else(|_| #default.to_string()),
162 },
163 None => {
164 let msg = format!(
165 "required env var `{var_str}` is not set — add it to .env or the environment"
166 );
167 quote! {
168 #field_ident: ::std::env::var(#var_name).unwrap_or_else(|_| panic!(#msg)),
169 }
170 }
171 Some(other) => {
172 return Err(syn::Error::new_spanned(
173 other,
174 "default for a String field must be a string literal",
175 ))
176 }
177 },
178
179 FieldKind::Bool => {
180 let default_val = match &parsed.default {
181 Some(Lit::Bool(b)) => b.value,
182 None => false,
183 Some(other) => {
184 return Err(syn::Error::new_spanned(
185 other,
186 "default for a bool field must be `true` or `false`",
187 ))
188 }
189 };
190 quote! {
191 #field_ident: ::std::env::var(#var_name)
192 .ok()
193 .and_then(|v| match v.to_lowercase().as_str() {
194 "true" | "1" | "yes" | "on" => ::std::option::Option::Some(true),
195 "false" | "0" | "no" | "off" => ::std::option::Option::Some(false),
196 _ => ::std::option::Option::None,
197 })
198 .unwrap_or(#default_val),
199 }
200 }
201
202 FieldKind::Num(ref ty_tokens) => match &parsed.default {
203 Some(Lit::Int(n)) => quote! {
204 #field_ident: ::std::env::var(#var_name)
205 .ok()
206 .and_then(|v| v.parse::<#ty_tokens>().ok())
207 .unwrap_or(#n as #ty_tokens),
208 },
209 Some(Lit::Float(f)) => quote! {
210 #field_ident: ::std::env::var(#var_name)
211 .ok()
212 .and_then(|v| v.parse::<#ty_tokens>().ok())
213 .unwrap_or(#f as #ty_tokens),
214 },
215 None => {
216 let msg = format!(
217 "required env var `{var_str}` is not set — add it to .env or the environment"
218 );
219 let bad_msg = format!("env var `{var_str}` must be a valid number");
220 quote! {
221 #field_ident: {
222 let __raw = ::std::env::var(#var_name).unwrap_or_else(|_| panic!(#msg));
223 __raw.parse::<#ty_tokens>().unwrap_or_else(|_| panic!(#bad_msg))
224 },
225 }
226 }
227 Some(other) => {
228 return Err(syn::Error::new_spanned(
229 other,
230 "default for a numeric field must be a numeric literal",
231 ))
232 }
233 },
234
235 FieldKind::OptStr => quote! {
236 #field_ident: ::std::env::var(#var_name).ok(),
237 },
238
239 FieldKind::OptBool => quote! {
240 #field_ident: ::std::env::var(#var_name)
241 .ok()
242 .and_then(|v| match v.to_lowercase().as_str() {
243 "true" | "1" | "yes" | "on" => ::std::option::Option::Some(true),
244 "false" | "0" | "no" | "off" => ::std::option::Option::Some(false),
245 _ => ::std::option::Option::None,
246 }),
247 },
248
249 FieldKind::OptNum(ref ty_tokens) => quote! {
250 #field_ident: ::std::env::var(#var_name)
251 .ok()
252 .and_then(|v| v.parse::<#ty_tokens>().ok()),
253 },
254
255 FieldKind::Other => {
256 return Err(syn::Error::new_spanned(
257 &field.ty,
258 "#[derive(Config)] supports String, bool, numeric types, and their Option<T> wrappers",
259 ))
260 }
261 };
262
263 field_inits.push(init);
264 }
265
266 let expanded = quote! {
267 impl ::rok_core::config::FromEnv for #name {
268 fn from_env() -> Self {
269 Self {
270 #(#field_inits)*
271 }
272 }
273 }
274
275 impl #name {
276 pub fn load() -> Self {
278 ::rok_core::config::Config::load::<Self>()
279 }
280 }
281 };
282
283 Ok(expanded.into())
284}
285
286fn extract_prefix(attrs: &[Attribute]) -> syn::Result<String> {
290 for attr in attrs {
291 if attr.path().is_ident("config") {
292 let meta: Meta = attr.parse_args()?;
293 match &meta {
294 Meta::NameValue(nv) if nv.path.is_ident("prefix") => {
295 if let syn::Expr::Lit(expr_lit) = &nv.value {
296 if let Lit::Str(s) = &expr_lit.lit {
297 return Ok(s.value());
298 }
299 }
300 }
301 _ => {
302 return Err(syn::Error::new_spanned(
303 &meta,
304 "expected `#[config(prefix = \"...\")]`",
305 ))
306 }
307 }
308 }
309 }
310 Err(syn::Error::new(
311 proc_macro2::Span::call_site(),
312 "missing `#[config(prefix = \"app\")]` attribute",
313 ))
314}
315
316#[proc_macro_derive(RokConfig, attributes(config, env))]
349pub fn derive_rok_config(input: TokenStream) -> TokenStream {
350 let input = parse_macro_input!(input as DeriveInput);
351 expand_rok_config(input).unwrap_or_else(|e| e.to_compile_error().into())
352}
353
354fn expand_rok_config(input: DeriveInput) -> syn::Result<TokenStream> {
355 let prefix = extract_prefix(&input.attrs)?;
356 let name = &input.ident;
357 let prefix_str = prefix.clone();
358
359 let Data::Struct(data) = &input.data else {
361 return Err(syn::Error::new_spanned(
362 &input.ident,
363 "#[derive(RokConfig)] only supports structs",
364 ));
365 };
366 let Fields::Named(fields) = &data.fields else {
367 return Err(syn::Error::new_spanned(
368 &input.ident,
369 "#[derive(RokConfig)] only supports structs with named fields",
370 ));
371 };
372
373 let mut field_inits: Vec<TokenStream2> = Vec::new();
374
375 for field in &fields.named {
376 let field_ident = field.ident.as_ref().unwrap();
377 let kind = classify(&field.ty);
378
379 let env_attr = field
380 .attrs
381 .iter()
382 .find(|a| a.path().is_ident("env"))
383 .ok_or_else(|| {
384 syn::Error::new_spanned(
385 field_ident,
386 "each field must have an #[env(\"VAR_NAME\")] attribute",
387 )
388 })?;
389
390 let parsed: EnvAttr = env_attr.parse_args()?;
391 let var_name = &parsed.var_name;
392 let var_str = var_name.value();
393
394 let init = match kind {
395 FieldKind::Str => match &parsed.default {
396 Some(Lit::Str(default)) => quote! {
397 #field_ident: ::std::env::var(#var_name).unwrap_or_else(|_| #default.to_string()),
398 },
399 None => {
400 let msg = format!(
401 "required env var `{var_str}` is not set — add it to .env or the environment"
402 );
403 quote! {
404 #field_ident: ::std::env::var(#var_name).unwrap_or_else(|_| panic!(#msg)),
405 }
406 }
407 Some(other) => {
408 return Err(syn::Error::new_spanned(
409 other,
410 "default for a String field must be a string literal",
411 ))
412 }
413 },
414
415 FieldKind::Bool => {
416 let default_val = match &parsed.default {
417 Some(Lit::Bool(b)) => b.value,
418 None => false,
419 Some(other) => {
420 return Err(syn::Error::new_spanned(
421 other,
422 "default for a bool field must be `true` or `false`",
423 ))
424 }
425 };
426 quote! {
427 #field_ident: ::std::env::var(#var_name)
428 .ok()
429 .and_then(|v| match v.to_lowercase().as_str() {
430 "true" | "1" | "yes" | "on" => ::std::option::Option::Some(true),
431 "false" | "0" | "no" | "off" => ::std::option::Option::Some(false),
432 _ => ::std::option::Option::None,
433 })
434 .unwrap_or(#default_val),
435 }
436 }
437
438 FieldKind::Num(ref ty_tokens) => match &parsed.default {
439 Some(Lit::Int(n)) => quote! {
440 #field_ident: ::std::env::var(#var_name)
441 .ok()
442 .and_then(|v| v.parse::<#ty_tokens>().ok())
443 .unwrap_or(#n as #ty_tokens),
444 },
445 Some(Lit::Float(f)) => quote! {
446 #field_ident: ::std::env::var(#var_name)
447 .ok()
448 .and_then(|v| v.parse::<#ty_tokens>().ok())
449 .unwrap_or(#f as #ty_tokens),
450 },
451 None => {
452 let msg = format!(
453 "required env var `{var_str}` is not set — add it to .env or the environment"
454 );
455 let bad_msg = format!("env var `{var_str}` must be a valid number");
456 quote! {
457 #field_ident: {
458 let __raw = ::std::env::var(#var_name).unwrap_or_else(|_| panic!(#msg));
459 __raw.parse::<#ty_tokens>().unwrap_or_else(|_| panic!(#bad_msg))
460 },
461 }
462 }
463 Some(other) => {
464 return Err(syn::Error::new_spanned(
465 other,
466 "default for a numeric field must be a numeric literal",
467 ))
468 }
469 },
470
471 FieldKind::OptStr => quote! {
472 #field_ident: ::std::env::var(#var_name).ok(),
473 },
474
475 FieldKind::OptBool => quote! {
476 #field_ident: ::std::env::var(#var_name)
477 .ok()
478 .and_then(|v| match v.to_lowercase().as_str() {
479 "true" | "1" | "yes" | "on" => ::std::option::Option::Some(true),
480 "false" | "0" | "no" | "off" => ::std::option::Option::Some(false),
481 _ => ::std::option::Option::None,
482 }),
483 },
484
485 FieldKind::OptNum(ref ty_tokens) => quote! {
486 #field_ident: ::std::env::var(#var_name)
487 .ok()
488 .and_then(|v| v.parse::<#ty_tokens>().ok()),
489 },
490
491 FieldKind::Other => {
492 return Err(syn::Error::new_spanned(
493 &field.ty,
494 "#[derive(RokConfig)] supports String, bool, numeric types, and their Option<T> wrappers",
495 ))
496 }
497 };
498
499 field_inits.push(init);
500 }
501
502 let expanded = quote! {
503 impl ::rok_core::config::Configurable for #name {
504 fn key() -> &'static str {
505 #prefix_str
506 }
507 }
508
509 impl ::rok_core::config::FromEnv for #name {
510 fn from_env() -> Self {
511 Self {
512 #(#field_inits)*
513 }
514 }
515 }
516
517 impl #name {
518 pub fn load() -> Self {
521 ::rok_core::config::load_config::<Self>()
522 .unwrap_or_else(|| ::rok_core::config::Config::load::<Self>())
523 }
524 }
525 };
526
527 Ok(expanded.into())
528}