1use std::borrow::Cow;
2
3use proc_macro::TokenStream as LangTokenStream;
4use proc_macro2::{Span, TokenStream};
5use proc_macro_error::{emit_error, proc_macro_error};
6use quote::{quote, quote_spanned};
7use syn::{
8 punctuated::Punctuated, spanned::Spanned, Attribute, Expr, ExprLit, Ident, Lit, LitStr, Meta,
9 Token,
10};
11
12#[proc_macro_error]
68#[proc_macro_derive(Template, attributes(config_it, config, non_config_default_expr))]
69pub fn derive_collect_fn(item: LangTokenStream) -> LangTokenStream {
70 let tokens = TokenStream::from(item);
71 let Ok(syn::ItemStruct { attrs: _, ident, fields, .. }) =
72 syn::parse2::<syn::ItemStruct>(tokens)
73 else {
74 proc_macro_error::abort_call_site!("expected struct")
75 };
76 let syn::Fields::Named(fields) = fields else {
77 proc_macro_error::abort_call_site!("Non-named fields are not allowed")
78 };
79
80 let mut gen = GenContext::default();
81 let this_crate = this_crate_name();
82
83 visit_fields(
84 &mut gen,
85 GenInputCommon { this_crate: &this_crate, struct_ident: &ident },
86 fields,
87 );
88
89 let GenContext {
90 fn_props,
92 fn_prop_at_offset,
93 fn_default_config,
94 fn_elem_at_mut,
95 fn_global_constants,
96 ..
97 } = gen;
98 let n_props = fn_props.len();
99
100 quote!(
101 #[allow(unused_parens)]
102 #[allow(unused_imports)]
103 #[allow(unused_braces)]
104 #[allow(clippy::useless_conversion)]
105 #[allow(clippy::redundant_closure)]
106 #[allow(clippy::clone_on_copy)]
107 const _: () = {
108 #( #fn_global_constants )*
109
110 impl #this_crate::Template for #ident {
111 type LocalPropContextArray = #this_crate::config::group::LocalPropContextArrayImpl<#n_props>;
112
113 fn props__() -> &'static [#this_crate::config::entity::PropertyInfo] {
114 static PROPS: ::std::sync::OnceLock<[#this_crate::config::entity::PropertyInfo; #n_props]> = ::std::sync::OnceLock::new();
115 PROPS.get_or_init(|| [#(#fn_props)*] )
116 }
117
118 fn prop_at_offset__(offset: usize) -> Option<&'static #this_crate::config::entity::PropertyInfo> {
119 let index = match offset { #(#fn_prop_at_offset)* _ => None::<usize> };
120 index.map(|x| &Self::props__()[x])
121 }
122
123 fn template_name() -> (&'static str, &'static str) {
124 (module_path!(), stringify!(#ident))
125 }
126
127 fn default_config() -> Self {
128 Self {
129 #(#fn_default_config)*
130 }
131 }
132
133 fn elem_at_mut__(&mut self, index: usize) -> &mut dyn std::any::Any {
134 use ::std::any::Any;
135
136 match index {
137 #(#fn_elem_at_mut)*
138 _ => panic!("Invalid index {}", index),
139 }
140 }
141 }
142 };
143 )
144 .into()
145}
146
147fn visit_fields(
148 GenContext {
149 fn_props,
150 fn_prop_at_offset,
151 fn_default_config,
152 fn_elem_at_mut,
153 fn_global_constants,
154 }: &mut GenContext,
155 GenInputCommon { this_crate, struct_ident }: GenInputCommon,
156 syn::FieldsNamed { named: fields, .. }: syn::FieldsNamed,
157) {
158 let n_field = fields.len();
159 fn_prop_at_offset.reserve(n_field);
160 fn_default_config.reserve(n_field);
161 fn_props.reserve(n_field);
162 fn_elem_at_mut.reserve(n_field);
163
164 let mut doc_string = Vec::new();
165 let mut field_index = 0usize;
166
167 for field in fields.into_iter() {
168 let field_span = field.ident.span();
169 let field_ty = field.ty;
170 let field_ident = field.ident.expect("This is struct with named fields");
171
172 let mut field_type = FieldType::Plain;
177 doc_string.clear();
178
179 for Attribute { meta, .. } in field.attrs {
180 if meta.path().is_ident("doc") {
181 let Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. }) =
183 &meta.require_name_value().unwrap().value
184 else {
185 proc_macro_error::abort!(meta, "Expected string literal")
186 };
187
188 doc_string.push(lit_str.value());
189 } else if ["config", "config_it"].into_iter().any(|x| meta.path().is_ident(x)) {
190 if !matches!(&field_type, FieldType::Plain) {
192 emit_error!(meta, "Duplicate config attribute");
193 continue;
194 }
195
196 field_type = FieldType::Property(match meta {
197 Meta::Path(_) => Default::default(),
198 Meta::List(list) => {
199 if let Some(x) = from_meta_list(list) {
200 x
201 } else {
202 continue;
203 }
204 }
205 Meta::NameValue(_) => {
206 emit_error!(meta, "Unexpected value. Expected `#[config(...)]`");
207 continue;
208 }
209 });
210 } else if meta.path().is_ident("non_config_default_expr") {
211 let span = meta.path().span();
213 let Meta::NameValue(expr) = meta else {
214 emit_error!(meta, "Expected expression");
215 continue;
216 };
217
218 let Some(expr) = expr_take_lit_str(expr.value) else {
219 emit_error!(span, "Expected string literal");
220 continue;
221 };
222
223 let Ok(expr) = expr.parse::<Expr>() else {
224 emit_error!(span, "Expected valid expression");
225 continue;
226 };
227
228 field_type = FieldType::PlainWithDefaultExpr(expr);
229 } else {
230 }
232 }
233
234 let prop = match field_type {
238 FieldType::Plain => {
239 fn_default_config
240 .push(quote_spanned!(field_span => #field_ident: Default::default(),));
241 continue;
242 }
243 FieldType::PlainWithDefaultExpr(expr) => {
244 fn_default_config.push(quote!(#field_ident: #expr,));
245 continue;
246 }
247 FieldType::Property(x) => x,
248 };
249
250 let default_expr = match prop.default {
252 Some(FieldPropertyDefault::Expr(expr)) => {
253 quote!(<#field_ty>::try_from(#expr).unwrap())
254 }
255
256 Some(FieldPropertyDefault::ExprStr(lit)) => {
257 let Ok(expr) = lit.parse::<Expr>() else {
258 emit_error!(lit.span(), "Expected valid expression");
259 continue;
260 };
261
262 quote!(#expr)
263 }
264
265 None => {
266 quote_spanned!(field_span => Default::default())
267 }
268 };
269
270 let default_expr = if let Some((once, env)) = prop.env.clone() {
271 let env_var = env.value();
272 if once {
273 quote_spanned!(env.span() => {
274 static ENV: ::std::sync::OnceLock<Option<#field_ty>> = ::std::sync::OnceLock::new();
275 ENV .get_or_init(|| std::env::var(#env_var).ok().and_then(|x| x.parse().ok()))
276 .clone().unwrap_or_else(|| #default_expr)
277 })
278 } else {
279 quote_spanned!(env.span() =>
280 std::env::var(#env_var).ok().and_then(|x| x.parse().ok()).unwrap_or_else(|| #default_expr)
281 )
282 }
283 } else {
284 default_expr
285 };
286
287 let default_fn_ident = format!("__fn_default_{}", field_ident);
288 let default_fn_ident = Ident::new(&default_fn_ident, field_ident.span());
289 let field_ident_upper = field_ident.to_string().to_uppercase();
290 let const_offset_ident =
291 Ident::new(&format!("__COFST_{field_ident_upper}"), Span::call_site());
292
293 fn_global_constants.push(quote_spanned!(field_span =>
294 fn #default_fn_ident() -> #field_ty {
295 #default_expr
296 }
297
298 const #const_offset_ident: usize = #this_crate::offset_of!(#struct_ident, #field_ident);
299 ));
300
301 fn_default_config.push(quote_spanned!(field_span => #field_ident: #default_fn_ident(),));
302
303 {
305 let FieldProperty {
306 rename,
307 admin,
308 admin_write,
309 admin_read,
310 min,
311 max,
312 one_of,
313 transient,
314 no_export,
315 no_import,
316 editor,
317 hidden,
318 secret,
319 readonly,
320 writeonly,
321 env,
322 validate_with,
323 ..
324 } = prop;
325
326 let flags = [
327 readonly.then(|| quote!(MetaFlag::READONLY)),
328 writeonly.then(|| quote!(MetaFlag::WRITEONLY)),
329 hidden.then(|| quote!(MetaFlag::HIDDEN)),
330 secret.then(|| quote!(MetaFlag::SECRET)),
331 admin.then(|| quote!(MetaFlag::ADMIN)),
332 admin_write.then(|| quote!(MetaFlag::ADMIN_WRITE)),
333 admin_read.then(|| quote!(MetaFlag::ADMIN_READ)),
334 transient.then(|| quote!(MetaFlag::TRANSIENT)),
335 no_export.then(|| quote!(MetaFlag::NO_EXPORT)),
336 no_import.then(|| quote!(MetaFlag::NO_IMPORT)),
337 ]
338 .into_iter()
339 .flatten();
340
341 let varname = field_ident.to_string();
342 let name = rename
343 .map(|x| Cow::Owned(x.value()))
344 .unwrap_or_else(|| Cow::Borrowed(varname.as_str()));
345 let doc_string = doc_string.join("\n");
346 let none = quote!(None);
347 let env = env
348 .as_ref()
349 .map(|x| x.1.value())
350 .map(|env| quote!(Some(#env)))
351 .unwrap_or_else(|| none.clone());
352 let editor_hint = editor
353 .map(|x| {
354 let x = quote_spanned!(x.span() =>
355 MetadataEditorHint::#x
356 );
357 quote!(Some(#this_crate::shared::meta::#x))
358 })
359 .unwrap_or_else(|| none.clone());
360
361 let schema = cfg!(feature = "jsonschema").then(|| {
362 quote! {
363 __default_ref_ptr::<#field_ty>().get_schema()
364 }
365 });
366 let validation_function = {
367 let fn_min = min.map(|x| {
368 quote!(
369 if *mref < #x {
370 editted = true;
371 *mref = #x;
372 }
373 )
374 });
375 let fn_max = max.map(|x| {
376 quote!(
377 if *mref > #x {
378 editted = true;
379 *mref = #x;
380 }
381 )
382 });
383 let fn_one_of = one_of.map(|x| {
384 quote!(
385 if #x.into_iter().all(|x| x != *mref) {
386 return Err("Value is not one of the allowed values".into());
387 }
388 )
389 });
390 let fn_user = validate_with.map(|x| {
391 let Ok(ident) = x.parse::<syn::ExprPath>() else {
392 emit_error!(x, "Expected valid identifier");
393 return none.clone();
394 };
395 quote!(
396 match #ident(mref) {
397 Ok(__entity::Validation::Valid) => {}
398 Ok(__entity::Validation::Modified) => { editted = true }
399 Err(e) => return Err(e),
400 }
401 )
402 });
403
404 quote! {
405 let mut editted = false;
406
407 #fn_min
408 #fn_max
409 #fn_one_of
410 #fn_user
411
412 if !editted {
413 Ok(__entity::Validation::Valid)
414 } else {
415 Ok(__entity::Validation::Modified)
416 }
417 }
418 };
419
420 fn_props.push(quote_spanned! { field_span =>
421 {
422 use #this_crate::config as __config;
423 use #this_crate::shared as __shared;
424 use __config::entity as __entity;
425 use __config::__lookup::*;
426 use __shared::meta as __meta;
427
428 __entity::PropertyInfo::new(
429 std::any::TypeId::of::<#field_ty>(),
430 #field_index,
431 __meta::Metadata::__macro_new(
432 #name,
433 #varname,
434 stringify!(#field_ty),
435 {
436 use __meta::MetaFlag;
437 #(#flags |)* MetaFlag::empty()
438 },
439 #editor_hint,
440 #doc_string,
441 #env,
442 #schema ),
444 Box::leak(Box::new(__entity::MetadataVTableImpl {
445 impl_copy: #this_crate::impls!(#field_ty: Copy),
446 fn_default: #default_fn_ident,
447 fn_validate: {
448 fn __validate(mref: &mut #field_ty) -> __entity::ValidationResult {
449 let _ = mref; #validation_function
451 }
452
453 __validate
454 },
455 }))
456 )
457 },
458 });
459 }
460
461 fn_prop_at_offset.push(quote!(#const_offset_ident => Some(#field_index),));
463 fn_elem_at_mut.push(quote!(#field_index => &mut self.#field_ident as &mut dyn Any,));
464
465 field_index += 1;
467 }
468}
469
470fn from_meta_list(meta_list: syn::MetaList) -> Option<FieldProperty> {
471 let mut r = FieldProperty::default();
472 let span = meta_list.span();
473 let Ok(parsed) =
474 meta_list.parse_args_with(<Punctuated<syn::Meta, Token![,]>>::parse_terminated)
475 else {
476 emit_error!(span, "Expected valid list of arguments");
477 return None;
478 };
479
480 for arg in parsed {
481 let is_ = |x: &str| arg.path().is_ident(x);
482 match arg {
483 Meta::Path(_) => {
484 if is_("admin") {
485 r.admin = true
486 } else if is_("admin_write") {
487 r.admin_write = true
488 } else if is_("admin_read") {
489 r.admin_read = true
490 } else if is_("transient") {
491 r.transient = true
492 } else if is_("no_export") {
493 r.no_export = true
494 } else if is_("no_import") {
495 r.no_import = true
496 } else if is_("secret") {
497 r.secret = true
498 } else if is_("readonly") {
499 r.readonly = true
500 } else if is_("writeonly") {
501 r.writeonly = true
502 } else if is_("hidden") {
503 r.hidden = true
504 } else {
505 emit_error!(arg, "Unknown attribute")
506 }
507 }
508 Meta::List(_) => {
509 emit_error!(arg, "Unexpected list")
510 }
511 Meta::NameValue(syn::MetaNameValue { value, path, .. }) => {
512 let is_ = |x: &str| path.is_ident(x);
513 if is_("default") {
514 r.default = Some(FieldPropertyDefault::Expr(value));
515 } else if is_("default_expr") {
516 r.default = expr_take_lit_str(value).map(FieldPropertyDefault::ExprStr);
517 } else if is_("alias") || is_("rename") {
518 r.rename = expr_take_lit_str(value);
519 } else if is_("min") {
520 r.min = Some(value);
521 } else if is_("max") {
522 r.max = Some(value);
523 } else if is_("one_of") {
524 let Expr::Array(one_of) = value else {
525 emit_error!(value, "Expected array literal");
526 continue;
527 };
528
529 r.one_of = Some(one_of);
530 } else if is_("validate_with") {
531 r.validate_with = expr_take_lit_str(value);
532 } else if is_("env_once") {
533 r.env = expr_take_lit_str(value).map(|x| (true, x));
534 } else if is_("env") {
535 r.env = expr_take_lit_str(value).map(|x| (false, x));
536 } else if is_("editor") {
537 r.editor = Some(value);
538 } else {
539 emit_error!(path.span(), "Unknown attribute")
540 }
541 }
542 }
543 }
544
545 Some(r)
546}
547
548enum FieldType {
549 Plain,
550 PlainWithDefaultExpr(Expr),
551 Property(FieldProperty),
552}
553
554fn expr_take_lit_str(expr: Expr) -> Option<LitStr> {
555 if let Expr::Lit(ExprLit { lit: Lit::Str(lit), .. }) = expr {
556 Some(lit)
557 } else {
558 emit_error!(expr, "Expected string literal");
559 None
560 }
561}
562
563enum FieldPropertyDefault {
564 Expr(Expr),
565 ExprStr(LitStr),
566}
567
568#[derive(Default)]
569struct FieldProperty {
570 rename: Option<syn::LitStr>,
571 default: Option<FieldPropertyDefault>,
572 admin: bool,
573 admin_write: bool,
574 admin_read: bool,
575 secret: bool,
576 readonly: bool,
577 writeonly: bool,
578 min: Option<syn::Expr>,
579 max: Option<syn::Expr>,
580 one_of: Option<syn::ExprArray>,
581 env: Option<(bool, syn::LitStr)>, validate_with: Option<syn::LitStr>,
583 transient: bool,
584 no_export: bool,
585 no_import: bool,
586 editor: Option<syn::Expr>,
587 hidden: bool,
588}
589
590fn this_crate_name() -> TokenStream {
591 use proc_macro_crate::*;
592
593 match crate_name("config-it") {
594 Ok(FoundCrate::Itself) => quote!(::config_it),
595 Ok(FoundCrate::Name(name)) => {
596 let ident = Ident::new(&name, Span::call_site());
597 quote!(::#ident)
598 }
599
600 Err(_) => {
601 quote!(config_it)
603 }
604 }
605}
606
607struct GenInputCommon<'a> {
608 this_crate: &'a TokenStream,
609 struct_ident: &'a Ident,
610}
611
612#[derive(Default)]
613struct GenContext {
614 fn_props: Vec<TokenStream>,
615 fn_prop_at_offset: Vec<TokenStream>,
616 fn_global_constants: Vec<TokenStream>,
617 fn_default_config: Vec<TokenStream>,
618 fn_elem_at_mut: Vec<TokenStream>,
619}