1#![cfg_attr(docsrs, feature(doc_cfg))]
3#![warn(
4 anonymous_parameters,
5 missing_copy_implementations,
6 missing_debug_implementations,
7 missing_docs,
8 nonstandard_style,
9 rust_2018_idioms,
10 single_use_lifetimes,
11 trivial_casts,
12 trivial_numeric_casts,
13 unreachable_pub,
14 unused_extern_crates,
15 unused_qualifications,
16 variant_size_differences
17)]
18use quote::{__private::TokenStream, quote};
19use std::collections::BTreeMap;
20use syn::spanned::Spanned;
21use syn::*;
22
23#[allow(missing_docs)]
24#[proc_macro_derive(FromConfig, attributes(config, validate))]
25pub fn derive_config(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
26 let input: DeriveInput = parse_macro_input!(input as DeriveInput);
27 let name = input.ident.clone();
28 let body = match input.data {
29 Data::Struct(data) => derive_config_struct(&name, input.attrs, data),
30 _ => Err(Error::new_spanned(name, "Only support struct")),
31 };
32 let body = match body {
33 Ok(tokens) => tokens,
34 Err(err) => err.to_compile_error(),
35 };
36 proc_macro::TokenStream::from(body)
37}
38
39fn derive_config_struct(
40 name: &Ident,
41 attrs: Vec<Attribute>,
42 data: DataStruct,
43) -> Result<TokenStream> {
44 let mut cfg_crate_path = quote!(::cfg_rs);
47
48 let prefix = match derive_config_prefix(attrs, &mut cfg_crate_path) {
49 Some(p) => quote! {
50 #[automatically_derived]
51 impl #cfg_crate_path::FromConfigWithPrefix for #name {
52 fn prefix() -> &'static str {
53 #p
54 }
55 }
56 },
57 _ => quote! {},
58 };
59
60 let fields = derive_config_fields(data)?;
61 let fs: Vec<Ident> = fields.iter().map(|f| f.name.clone()).collect();
62 #[cfg(feature = "regex")]
63 let regex_map = {
64 use quote::ToTokens;
65 let mut map: BTreeMap<String, Ident> = BTreeMap::new();
66 let mut idx = 0usize;
67 for field in &fields {
68 for rule in &field.validates {
69 if let ValidateRule::Regex { pattern, .. } = rule {
70 let key = pattern.to_token_stream().to_string();
71 if !map.contains_key(&key) {
72 let ident = quote::format_ident!("__CFG_REGEX_{}", idx);
73 idx += 1;
74 map.insert(key, ident);
75 }
76 }
77 }
78 }
79 map
80 };
81 #[cfg(not(feature = "regex"))]
82 let regex_map: BTreeMap<String, Ident> = BTreeMap::new();
83
84 let regex_cache_decl = if regex_map.is_empty() {
85 quote! {}
86 } else {
87 let decls = regex_map.values().map(|ident| {
88 quote! {
89 static #ident: ::std::sync::OnceLock<
90 ::core::result::Result<::regex::Regex, ::std::string::String>
91 > = ::std::sync::OnceLock::new();
92 }
93 });
94 quote! { #(#decls)* }
95 };
96
97 let parse_fields: Vec<TokenStream> = fields
98 .iter()
99 .map(|f| build_parse_and_validate(f, &cfg_crate_path, ®ex_map))
100 .collect();
101
102 Ok(quote! {
103 #[automatically_derived]
104 impl #cfg_crate_path::FromConfig for #name {
105 fn from_config(
106 context: &mut #cfg_crate_path::ConfigContext<'_>,
107 value: ::core::option::Option<#cfg_crate_path::ConfigValue<'_>>,
108 ) -> ::core::result::Result<Self, #cfg_crate_path::ConfigError> {
109 #regex_cache_decl
110 #(#parse_fields)*
111 ::core::result::Result::Ok(Self {
112 #(#fs,)*
113 })
114 }
115 }
116
117 #prefix
118 })
119}
120
121fn derive_config_prefix(attrs: Vec<Attribute>, crate_path: &mut TokenStream) -> Option<String> {
122 let mut prefix = None;
123 for attr in attrs {
124 if attr.path().is_ident("config") {
125 attr.parse_nested_meta(|meta| {
126 if meta.path.is_ident("prefix") {
127 let value = meta.value()?;
128 let s: LitStr = value.parse()?;
129 prefix = Some(s.value());
130 Ok(())
131 } else if meta.path.is_ident("crate") {
132 let value = meta.value()?;
133 let s: LitStr = value.parse()?;
134 let ident = Ident::new(&s.value(), s.span());
135 *crate_path = quote!(#ident);
136 Ok(())
137 } else {
138 Err(meta.error("Only support prefix"))
139 }
140 })
141 .unwrap();
142 }
143 if prefix.is_some() {
144 break;
145 }
146 }
147 prefix
148}
149
150struct FieldInfo {
151 name: Ident,
152 def: Option<String>,
153 ren: String,
154 desc: Option<String>,
155 ty: Type,
156 validates: Vec<ValidateRule>,
157}
158
159fn derive_config_fields(data: DataStruct) -> Result<Vec<FieldInfo>> {
160 if let Fields::Named(fields) = data.fields {
161 let mut fs = vec![];
162 for field in fields.named {
163 fs.push(derive_config_field(field)?);
164 }
165 return Ok(fs);
166 }
167 Err(Error::new_spanned(data.fields, "Only support named body"))
168}
169
170fn derive_config_field(field: Field) -> Result<FieldInfo> {
171 let name = field.ident.expect("Not possible");
172 let mut f = FieldInfo {
173 ren: name.to_string(),
174 name,
175 def: None,
176 desc: None,
177 ty: field.ty.clone(),
178 validates: vec![],
179 };
180 derive_config_field_attr(&mut f, field.attrs)?;
181 Ok(f)
182}
183
184fn derive_config_field_attr(f: &mut FieldInfo, attrs: Vec<Attribute>) -> Result<()> {
185 for attr in attrs {
186 if attr.path().is_ident("config") {
187 attr.parse_nested_meta(|meta| {
188 if meta.path.is_ident("default") {
189 f.def = Some(parse_lit(meta.value()?.parse::<Lit>()?));
190 } else if meta.path.is_ident("name") {
191 f.ren = parse_lit(meta.value()?.parse::<Lit>()?);
192 } else if meta.path.is_ident("desc") {
193 f.desc = Some(parse_lit(meta.value()?.parse::<Lit>()?));
194 } else {
195 return Err(meta.error("Only support default/name/desc"));
196 }
197 Ok(())
198 })?;
199 } else if attr.path().is_ident("validate") {
200 parse_validate_attr(f, attr)?;
201 }
202 }
203 Ok(())
204}
205
206enum ValidateRule {
207 Range {
208 min: Option<Expr>,
209 max: Option<Expr>,
210 message: Option<LitStr>,
211 },
212 NotEmpty {
213 message: Option<LitStr>,
214 },
215 Length {
216 min: Option<Expr>,
217 max: Option<Expr>,
218 message: Option<LitStr>,
219 },
220 #[cfg(feature = "regex")]
221 Regex {
222 pattern: Expr,
223 message: Option<LitStr>,
224 },
225 Custom {
226 path: Path,
227 message: Option<LitStr>,
228 },
229}
230
231fn parse_validate_attr(f: &mut FieldInfo, attr: Attribute) -> Result<()> {
232 let mut rules: Vec<ValidateRule> = Vec::new();
233 let mut message_seen = false;
234 attr.parse_nested_meta(|meta| {
235 let item = meta.path.get_ident().map(|i| i.to_string());
236 let ret = match item.as_deref() {
237 Some("range") => {
238 let mut min: Option<Expr> = None;
239 let mut max: Option<Expr> = None;
240 meta.parse_nested_meta(|inner| {
241 if inner.path.is_ident("min") {
242 let value = inner.value()?;
243 min = Some(value.parse::<Expr>()?);
244 Ok(())
245 } else if inner.path.is_ident("max") {
246 let value = inner.value()?;
247 max = Some(value.parse::<Expr>()?);
248 Ok(())
249 } else {
250 Err(inner.error("Only support min/max"))
251 }
252 })?;
253 rules.push(ValidateRule::Range {
254 min,
255 max,
256 message: None,
257 });
258 Ok(())
259 }
260 Some("length") => {
261 let mut min: Option<Expr> = None;
262 let mut max: Option<Expr> = None;
263 meta.parse_nested_meta(|inner| {
264 if inner.path.is_ident("min") {
265 let value = inner.value()?;
266 min = Some(value.parse::<Expr>()?);
267 Ok(())
268 } else if inner.path.is_ident("max") {
269 let value = inner.value()?;
270 max = Some(value.parse::<Expr>()?);
271 Ok(())
272 } else {
273 Err(inner.error("Only support min/max"))
274 }
275 })?;
276 rules.push(ValidateRule::Length {
277 min,
278 max,
279 message: None,
280 });
281 Ok(())
282 }
283 Some("not_empty") => {
284 rules.push(ValidateRule::NotEmpty { message: None });
285 Ok(())
286 }
287 #[cfg(feature = "regex")]
288 Some("regex") => {
289 let value = meta.value()?;
290 let s: Expr = value.parse()?;
291 rules.push(ValidateRule::Regex {
292 pattern: s,
293 message: None,
294 });
295 Ok(())
296 }
297 Some("custom") => {
298 let value = meta.value()?;
299 let path = if let Ok(p) = value.parse::<Path>() {
300 p
301 } else {
302 let s: LitStr = value.parse()?;
303 parse_str::<Path>(&s.value()).map_err(|err| {
304 Error::new(
305 s.span(),
306 format!("custom validator must be a valid path: {}", err),
307 )
308 })?
309 };
310 rules.push(ValidateRule::Custom {
311 path,
312 message: None,
313 });
314 Ok(())
315 }
316 Some("message") => {
317 let message = if let Ok(value) = meta.value() {
318 value.parse::<LitStr>()?
319 } else {
320 meta.input.parse::<LitStr>()?
321 };
322 if rules.is_empty() {
323 return Err(meta.error("validate message must follow a rule"));
324 }
325 if message_seen {
326 return Err(meta.error("Only one message allowed per validate attribute"));
327 }
328 message_seen = true;
329 if let Some(last) = rules.pop() {
330 if validate_rule_has_message(&last) {
331 return Err(meta.error("validate message already set for this rule"));
332 }
333 rules.push(apply_validate_message(last, Some(message)));
334 }
335 Ok(())
336 }
337 _ => Err(meta.error("Only support range/length/not_empty/regex/custom/message")),
338 };
339
340 ret
341 })?;
342
343 if rules.is_empty() {
344 return Err(Error::new(
345 attr.span(),
346 "validate attribute must contain a rule",
347 ));
348 }
349 f.validates.extend(rules);
350 Ok(())
351}
352
353fn validate_rule_has_message(rule: &ValidateRule) -> bool {
354 match rule {
355 ValidateRule::Range { message, .. }
356 | ValidateRule::NotEmpty { message }
357 | ValidateRule::Length { message, .. }
358 | ValidateRule::Custom { message, .. } => message.is_some(),
359 #[cfg(feature = "regex")]
360 ValidateRule::Regex { message, .. } => message.is_some(),
361 }
362}
363
364fn apply_validate_message(rule: ValidateRule, message: Option<LitStr>) -> ValidateRule {
365 match rule {
366 ValidateRule::Range { min, max, .. } => ValidateRule::Range { min, max, message },
367 ValidateRule::NotEmpty { .. } => ValidateRule::NotEmpty { message },
368 ValidateRule::Length { min, max, .. } => ValidateRule::Length { min, max, message },
369 #[cfg(feature = "regex")]
370 ValidateRule::Regex { pattern, .. } => ValidateRule::Regex { pattern, message },
371 ValidateRule::Custom { path, .. } => ValidateRule::Custom { path, message },
372 }
373}
374
375fn build_parse_and_validate(
376 field: &FieldInfo,
377 crate_path: &TokenStream,
378 regex_map: &BTreeMap<String, Ident>,
379) -> TokenStream {
380 let name = &field.name;
381 let ty = &field.ty;
382 let key = field.ren.as_str();
383 let def = match &field.def {
384 Some(d) => quote! {,Some(#d.into())},
385 None => quote! {,None},
386 };
387 let validate = build_validate_block(field, crate_path, regex_map);
388 if field.validates.is_empty() {
389 quote! {
390 let #name: #ty = context.parse_config(#key #def)?;
391 }
392 } else {
393 quote! {
394 let #name: #ty = context.parse_config(#key #def)?;
395 #validate
396 }
397 }
398}
399
400fn build_validate_block(
401 field: &FieldInfo,
402 crate_path: &TokenStream,
403 regex_map: &BTreeMap<String, Ident>,
404) -> TokenStream {
405 if field.validates.is_empty() {
406 return quote! {};
407 }
408
409 let name = &field.name;
410 let key = field.ren.as_str();
411 let is_option = option_inner(&field.ty).is_some();
412
413 let field_key_init = quote! {
414 let field_key = || {
415 let current_key = context.current_key();
416 if current_key.is_empty() {
417 #key.to_string()
418 } else {
419 format!("{}.{}", current_key, #key)
420 }
421 };
422 };
423 let field_key_expr = quote! { &field_key };
424
425 if is_option {
426 let value_expr = quote! { value };
427 let checks: Vec<TokenStream> = field
428 .validates
429 .iter()
430 .map(|rule| {
431 build_validate_rule(rule, crate_path, &field_key_expr, &value_expr, regex_map)
432 })
433 .collect();
434 quote! {
435 #field_key_init
436 if let ::core::option::Option::Some(value) = #name.as_ref() {
437 #(#checks)*
438 }
439 }
440 } else {
441 let value_expr = quote! { &#name };
442 let checks: Vec<TokenStream> = field
443 .validates
444 .iter()
445 .map(|rule| {
446 build_validate_rule(rule, crate_path, &field_key_expr, &value_expr, regex_map)
447 })
448 .collect();
449 quote! {
450 #field_key_init
451 #(#checks)*
452 }
453 }
454}
455
456fn build_validate_rule(
457 rule: &ValidateRule,
458 crate_path: &TokenStream,
459 field_key: &TokenStream,
460 value: &TokenStream,
461 _regex_map: &BTreeMap<String, Ident>,
462) -> TokenStream {
463 match rule {
464 ValidateRule::Range { min, max, message } => {
465 let min_expr = min
466 .as_ref()
467 .map(|v| quote! { ::core::option::Option::Some(&#v) });
468 let max_expr = max
469 .as_ref()
470 .map(|v| quote! { ::core::option::Option::Some(&#v) });
471 let min_ref = min_expr.unwrap_or_else(|| quote! { ::core::option::Option::None });
472 let max_ref = max_expr.unwrap_or_else(|| quote! { ::core::option::Option::None });
473 let call = quote! {
474 #crate_path::validate::validate_range(
475 #field_key,
476 #value,
477 #min_ref,
478 #max_ref,
479 )
480 };
481 wrap_validate_call(call, crate_path, field_key, message)
482 }
483 ValidateRule::Length { min, max, message } => {
484 let min_expr = min
485 .as_ref()
486 .map(|v| quote! { ::core::option::Option::Some(#v) });
487 let max_expr = max
488 .as_ref()
489 .map(|v| quote! { ::core::option::Option::Some(#v) });
490 let min_ref = min_expr.unwrap_or_else(|| quote! { ::core::option::Option::None });
491 let max_ref = max_expr.unwrap_or_else(|| quote! { ::core::option::Option::None });
492 let call = quote! {
493 #crate_path::validate::validate_length(
494 #field_key,
495 #value,
496 #min_ref,
497 #max_ref,
498 )
499 };
500 wrap_validate_call(call, crate_path, field_key, message)
501 }
502 ValidateRule::NotEmpty { message } => {
503 let call = quote! {
504 #crate_path::validate::validate_not_empty(
505 #field_key,
506 #value,
507 )
508 };
509 wrap_validate_call(call, crate_path, field_key, message)
510 }
511 #[cfg(feature = "regex")]
512 ValidateRule::Regex { pattern, message } => {
513 use quote::ToTokens;
514 let key = pattern.to_token_stream().to_string();
515 let regex_ident = _regex_map.get(&key).expect("missing regex cache entry");
516 let call = quote! {
517 {
518 let regex_result = #regex_ident.get_or_init(|| {
519 ::regex::Regex::new(#pattern)
520 .map_err(|err| format!("invalid regex: {}", err))
521 });
522
523 let regex = match regex_result {
524 ::core::result::Result::Ok(re) => re,
525 ::core::result::Result::Err(message) => {
526 return ::core::result::Result::Err(
527 #crate_path::ConfigError::ConfigParseError(
528 (#field_key)(),
529 message.clone(),
530 ),
531 );
532 }
533 };
534
535 #crate_path::validate::validate_regex(
536 #field_key,
537 regex,
538 #value.as_ref(),
539 )
540 }
541 };
542 wrap_validate_call(call, crate_path, field_key, message)
543 }
544 ValidateRule::Custom { path, message } => {
545 let call = quote! { #crate_path::validate::validate_custom(#field_key, #value, #path) };
546 wrap_validate_call(call, crate_path, field_key, message)
547 }
548 }
549}
550
551fn wrap_validate_call(
552 call: TokenStream,
553 crate_path: &TokenStream,
554 field_key: &TokenStream,
555 message: &Option<LitStr>,
556) -> TokenStream {
557 if let Some(message) = message {
558 quote! {
559 match #call {
560 ::core::result::Result::Ok(()) => (),
561 ::core::result::Result::Err(_) => {
562 return ::core::result::Result::Err(
563 #crate_path::ConfigError::ConfigParseError(
564 (#field_key)(),
565 #message.to_string(),
566 ),
567 );
568 }
569 }
570 }
571 } else {
572 quote! {
573 #call?;
574 }
575 }
576}
577
578fn option_inner(ty: &Type) -> Option<&Type> {
579 let Type::Path(type_path) = ty else {
580 return None;
581 };
582 let segment = type_path.path.segments.last()?;
583 if segment.ident != "Option" {
584 return None;
585 }
586 let PathArguments::AngleBracketed(args) = &segment.arguments else {
587 return None;
588 };
589 for arg in &args.args {
590 if let GenericArgument::Type(inner) = arg {
591 return Some(inner);
592 }
593 }
594 None
595}
596
597fn parse_lit(lit: Lit) -> String {
598 match lit {
599 Lit::Str(s) => s.value(),
600 Lit::ByteStr(s) => match String::from_utf8(s.value()) {
601 Ok(v) => v,
602 Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
603 },
604 Lit::Byte(b) => (b.value() as char).to_string(),
605 Lit::Int(i) => i.base10_digits().to_owned(),
606 Lit::Float(f) => f.base10_digits().to_owned(),
607 Lit::Bool(b) => b.value.to_string(),
608 Lit::Char(c) => c.value().to_string(),
609 Lit::Verbatim(_) => panic!("cfg-rs not support Verbatim"),
610 _ => panic!("cfg-rs not support new types"),
611 }
612}