1use proc_macro::TokenStream;
7use proc_macro2::Span;
8use quote::{format_ident, quote};
9use syn::{
10 Expr, ItemStruct, Meta, MetaList, Type, parse_macro_input, parse_quote, punctuated::Punctuated,
11 spanned::Spanned, token::Comma,
12};
13
14#[proc_macro_attribute]
15pub fn askit(attr: TokenStream, item: TokenStream) -> TokenStream {
16 askit_agent(attr, item)
17}
18
19#[proc_macro_attribute]
36pub fn askit_agent(attr: TokenStream, item: TokenStream) -> TokenStream {
37 let args = parse_macro_input!(attr with Punctuated<Meta, Comma>::parse_terminated);
38 let item_struct = parse_macro_input!(item as ItemStruct);
39
40 match expand_askit_agent(args, item_struct) {
41 Ok(tokens) => tokens.into(),
42 Err(err) => err.into_compile_error().into(),
43 }
44}
45
46struct AgentArgs {
47 kind: Option<Expr>,
48 name: Option<Expr>,
49 title: Option<Expr>,
50 description: Option<Expr>,
51 category: Option<Expr>,
52 inputs: Vec<Expr>,
53 outputs: Vec<Expr>,
54 configs: Vec<ConfigSpec>,
55 global_configs: Vec<ConfigSpec>,
56}
57
58#[derive(Default)]
59struct CommonConfig {
60 name: Option<Expr>,
61 default: Option<Expr>,
62 title: Option<Expr>,
63 description: Option<Expr>,
64 hide_title: bool,
65 readonly: bool,
66}
67
68struct CustomConfig {
69 name: Expr,
70 default: Expr,
71 type_: Expr,
72 title: Option<Expr>,
73 description: Option<Expr>,
74 hide_title: bool,
75 readonly: bool,
76}
77
78enum ConfigSpec {
79 Unit(CommonConfig),
80 Boolean(CommonConfig),
81 Integer(CommonConfig),
82 Number(CommonConfig),
83 String(CommonConfig),
84 Text(CommonConfig),
85 Object(CommonConfig),
86 Custom(CustomConfig),
87}
88
89fn expand_askit_agent(
90 args: Punctuated<Meta, Comma>,
91 item: ItemStruct,
92) -> syn::Result<proc_macro2::TokenStream> {
93 let has_data_field = item.fields.iter().any(|f| match (&f.ident, &f.ty) {
94 (Some(ident), Type::Path(tp)) if ident == "data" => tp
95 .path
96 .segments
97 .last()
98 .map(|seg| seg.ident == "AgentData")
99 .unwrap_or(false),
100 _ => false,
101 });
102
103 if !has_data_field {
104 return Err(syn::Error::new(
105 item.span(),
106 "#[askit_agent] expects the struct to have a `data: AgentData` field",
107 ));
108 }
109
110 let mut parsed = AgentArgs {
111 kind: None,
112 name: None,
113 title: None,
114 description: None,
115 category: None,
116 inputs: Vec::new(),
117 outputs: Vec::new(),
118 configs: Vec::new(),
119 global_configs: Vec::new(),
120 };
121
122 for meta in args {
123 match meta {
124 Meta::NameValue(nv) if nv.path.is_ident("kind") => {
125 parsed.kind = Some(nv.value);
126 }
127 Meta::NameValue(nv) if nv.path.is_ident("name") => {
128 parsed.name = Some(nv.value);
129 }
130 Meta::NameValue(nv) if nv.path.is_ident("title") => {
131 parsed.title = Some(nv.value);
132 }
133 Meta::NameValue(nv) if nv.path.is_ident("description") => {
134 parsed.description = Some(nv.value);
135 }
136 Meta::NameValue(nv) if nv.path.is_ident("category") => {
137 parsed.category = Some(nv.value);
138 }
139 Meta::NameValue(nv) if nv.path.is_ident("inputs") => {
140 parsed.inputs = parse_expr_array(nv.value)?;
141 }
142 Meta::NameValue(nv) if nv.path.is_ident("outputs") => {
143 parsed.outputs = parse_expr_array(nv.value)?;
144 }
145 Meta::List(ml) if ml.path.is_ident("inputs") => {
146 parsed.inputs = collect_exprs(ml)?;
147 }
148 Meta::List(ml) if ml.path.is_ident("outputs") => {
149 parsed.outputs = collect_exprs(ml)?;
150 }
151 Meta::List(ml) if ml.path.is_ident("string_config") => {
152 parsed
153 .configs
154 .push(ConfigSpec::String(parse_common_config(ml)?));
155 }
156 Meta::List(ml) if ml.path.is_ident("text_config") => {
157 parsed
158 .configs
159 .push(ConfigSpec::Text(parse_common_config(ml)?));
160 }
161 Meta::List(ml) if ml.path.is_ident("boolean_config") => {
162 parsed
163 .configs
164 .push(ConfigSpec::Boolean(parse_common_config(ml)?));
165 }
166 Meta::List(ml) if ml.path.is_ident("integer_config") => {
167 parsed
168 .configs
169 .push(ConfigSpec::Integer(parse_common_config(ml)?));
170 }
171 Meta::List(ml) if ml.path.is_ident("number_config") => {
172 parsed
173 .configs
174 .push(ConfigSpec::Number(parse_common_config(ml)?));
175 }
176 Meta::List(ml) if ml.path.is_ident("object_config") => {
177 parsed
178 .configs
179 .push(ConfigSpec::Object(parse_common_config(ml)?));
180 }
181 Meta::List(ml) if ml.path.is_ident("custom_config") => {
182 parsed
183 .configs
184 .push(ConfigSpec::Custom(parse_custom_config(ml)?));
185 }
186 Meta::List(ml) if ml.path.is_ident("unit_config") => {
187 parsed
188 .configs
189 .push(ConfigSpec::Unit(parse_common_config(ml)?));
190 }
191 Meta::List(ml) if ml.path.is_ident("string_global_config") => {
192 parsed
193 .global_configs
194 .push(ConfigSpec::String(parse_common_config(ml)?));
195 }
196 Meta::List(ml) if ml.path.is_ident("text_global_config") => {
197 parsed
198 .global_configs
199 .push(ConfigSpec::Text(parse_common_config(ml)?));
200 }
201 Meta::List(ml) if ml.path.is_ident("boolean_global_config") => {
202 parsed
203 .global_configs
204 .push(ConfigSpec::Boolean(parse_common_config(ml)?));
205 }
206 Meta::List(ml) if ml.path.is_ident("integer_global_config") => {
207 parsed
208 .global_configs
209 .push(ConfigSpec::Integer(parse_common_config(ml)?));
210 }
211 Meta::List(ml) if ml.path.is_ident("number_global_config") => {
212 parsed
213 .global_configs
214 .push(ConfigSpec::Number(parse_common_config(ml)?));
215 }
216 Meta::List(ml) if ml.path.is_ident("object_global_config") => {
217 parsed
218 .global_configs
219 .push(ConfigSpec::Object(parse_common_config(ml)?));
220 }
221 Meta::List(ml) if ml.path.is_ident("custom_global_config") => {
222 parsed
223 .global_configs
224 .push(ConfigSpec::Custom(parse_custom_config(ml)?));
225 }
226 Meta::List(ml) if ml.path.is_ident("unit_global_config") => {
227 parsed
228 .global_configs
229 .push(ConfigSpec::Unit(parse_common_config(ml)?));
230 }
231 other => {
232 return Err(syn::Error::new_spanned(
233 other,
234 "unsupported askit_agent argument",
235 ));
236 }
237 }
238 }
239
240 let ident = &item.ident;
241 let generics = item.generics.clone();
242 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
243 let data_impl = quote! {
244 impl #impl_generics ::agent_stream_kit::HasAgentData for #ident #ty_generics #where_clause {
245 fn data(&self) -> &::agent_stream_kit::AgentData {
246 &self.data
247 }
248
249 fn mut_data(&mut self) -> &mut ::agent_stream_kit::AgentData {
250 &mut self.data
251 }
252 }
253 };
254
255 let kind = parsed.kind.unwrap_or_else(|| parse_quote! { "Agent" });
256 let name_tokens = parsed.name.map(|n| quote! { #n }).unwrap_or_else(|| {
257 quote! { concat!(module_path!(), "::", stringify!(#ident)) }
258 });
259
260 let title = parsed
261 .title
262 .ok_or_else(|| syn::Error::new(Span::call_site(), "askit_agent: missing `title`"))?;
263 let category = parsed
264 .category
265 .ok_or_else(|| syn::Error::new(Span::call_site(), "askit_agent: missing `category`"))?;
266 let title = quote! { .title(#title) };
267 let description = parsed.description.map(|d| quote! { .description(#d) });
268 let category = quote! { .category(#category) };
269
270 let inputs = if parsed.inputs.is_empty() {
271 quote! {}
272 } else {
273 let values = parsed.inputs;
274 quote! { .inputs(vec![#(#values),*]) }
275 };
276
277 let outputs = if parsed.outputs.is_empty() {
278 quote! {}
279 } else {
280 let values = parsed.outputs;
281 quote! { .outputs(vec![#(#values),*]) }
282 };
283
284 let config_calls = parsed
285 .configs
286 .into_iter()
287 .map(|cfg| match cfg {
288 ConfigSpec::Unit(c) => {
289 let name = c.name.ok_or_else(|| {
290 syn::Error::new(Span::call_site(), "unit_config missing `name`")
291 })?;
292 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
293 let description = c
294 .description
295 .map(|d| quote! { let entry = entry.description(#d); });
296 let hide_title = if c.hide_title {
297 quote! { let entry = entry.hide_title(); }
298 } else {
299 quote! {}
300 };
301 let readonly = if c.readonly {
302 quote! { let entry = entry.readonly(); }
303 } else {
304 quote! {}
305 };
306 Ok(quote! {
307 .unit_config_with(#name, |entry| {
308 let entry = entry;
309 #title
310 #description
311 #hide_title
312 #readonly
313 entry
314 })
315 })
316 }
317 ConfigSpec::Boolean(c) => {
318 let name = c.name.ok_or_else(|| {
319 syn::Error::new(Span::call_site(), "boolean_config missing `name`")
320 })?;
321 let default = c.default.unwrap_or_else(|| parse_quote! { false });
322 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
323 let description = c
324 .description
325 .map(|d| quote! { let entry = entry.description(#d); });
326 let hide_title = if c.hide_title {
327 quote! { let entry = entry.hide_title(); }
328 } else {
329 quote! {}
330 };
331 let readonly = if c.readonly {
332 quote! { let entry = entry.readonly(); }
333 } else {
334 quote! {}
335 };
336 Ok(quote! {
337 .boolean_config_with(#name, #default, |entry| {
338 let entry = entry;
339 #title
340 #description
341 #hide_title
342 #readonly
343 entry
344 })
345 })
346 }
347 ConfigSpec::Integer(c) => {
348 let name = c.name.ok_or_else(|| {
349 syn::Error::new(Span::call_site(), "integer_config missing `name`")
350 })?;
351 let default = c.default.unwrap_or_else(|| parse_quote! { 0i64 });
352 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
353 let description = c
354 .description
355 .map(|d| quote! { let entry = entry.description(#d); });
356 let hide_title = if c.hide_title {
357 quote! { let entry = entry.hide_title(); }
358 } else {
359 quote! {}
360 };
361 let readonly = if c.readonly {
362 quote! { let entry = entry.readonly(); }
363 } else {
364 quote! {}
365 };
366 Ok(quote! {
367 .integer_config_with(#name, #default, |entry| {
368 let entry = entry;
369 #title
370 #description
371 #hide_title
372 #readonly
373 entry
374 })
375 })
376 }
377 ConfigSpec::Number(c) => {
378 let name = c.name.ok_or_else(|| {
379 syn::Error::new(Span::call_site(), "number_config missing `name`")
380 })?;
381 let default = c.default.unwrap_or_else(|| parse_quote! { 0.0f64 });
382 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
383 let description = c
384 .description
385 .map(|d| quote! { let entry = entry.description(#d); });
386 let hide_title = if c.hide_title {
387 quote! { let entry = entry.hide_title(); }
388 } else {
389 quote! {}
390 };
391 let readonly = if c.readonly {
392 quote! { let entry = entry.readonly(); }
393 } else {
394 quote! {}
395 };
396 Ok(quote! {
397 .number_config_with(#name, #default, |entry| {
398 let entry = entry;
399 #title
400 #description
401 #hide_title
402 #readonly
403 entry
404 })
405 })
406 }
407 ConfigSpec::String(c) => {
408 let name = c.name.ok_or_else(|| {
409 syn::Error::new(Span::call_site(), "string_config missing `name`")
410 })?;
411 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
412 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
413 let description = c
414 .description
415 .map(|d| quote! { let entry = entry.description(#d); });
416 let hide_title = if c.hide_title {
417 quote! { let entry = entry.hide_title(); }
418 } else {
419 quote! {}
420 };
421 let readonly = if c.readonly {
422 quote! { let entry = entry.readonly(); }
423 } else {
424 quote! {}
425 };
426 Ok(quote! {
427 .string_config_with(#name, #default, |entry| {
428 let entry = entry;
429 #title
430 #description
431 #hide_title
432 #readonly
433 entry
434 })
435 })
436 }
437 ConfigSpec::Text(c) => {
438 let name = c.name.ok_or_else(|| {
439 syn::Error::new(Span::call_site(), "text_config missing `name`")
440 })?;
441 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
442 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
443 let description = c
444 .description
445 .map(|d| quote! { let entry = entry.description(#d); });
446 let hide_title = if c.hide_title {
447 quote! { let entry = entry.hide_title(); }
448 } else {
449 quote! {}
450 };
451 let readonly = if c.readonly {
452 quote! { let entry = entry.readonly(); }
453 } else {
454 quote! {}
455 };
456 Ok(quote! {
457 .text_config_with(#name, #default, |entry| {
458 let entry = entry;
459 #title
460 #description
461 #hide_title
462 #readonly
463 entry
464 })
465 })
466 }
467 ConfigSpec::Object(c) => {
468 let name = c.name.ok_or_else(|| {
469 syn::Error::new(Span::call_site(), "object_config missing `name`")
470 })?;
471 let default = c.default.unwrap_or_else(|| {
472 parse_quote! { ::agent_stream_kit::AgentValue::object_default() }
473 });
474 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
475 let description = c
476 .description
477 .map(|d| quote! { let entry = entry.description(#d); });
478 let hide_title = if c.hide_title {
479 quote! { let entry = entry.hide_title(); }
480 } else {
481 quote! {}
482 };
483 let readonly = if c.readonly {
484 quote! { let entry = entry.readonly(); }
485 } else {
486 quote! {}
487 };
488 Ok(quote! {
489 .object_config_with(#name, #default, |entry| {
490 let entry = entry;
491 #title
492 #description
493 #hide_title
494 #readonly
495 entry
496 })
497 })
498 }
499 ConfigSpec::Custom(c) => custom_config_call("custom_config_with", c),
500 })
501 .collect::<syn::Result<Vec<_>>>()?;
502
503 let global_config_calls = parsed
504 .global_configs
505 .into_iter()
506 .map(|cfg| match cfg {
507 ConfigSpec::Unit(c) => {
508 let name = c.name.ok_or_else(|| {
509 syn::Error::new(Span::call_site(), "unit_global_config missing `name`")
510 })?;
511 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
512 let description = c
513 .description
514 .map(|d| quote! { let entry = entry.description(#d); });
515 let hide_title = if c.hide_title {
516 quote! { let entry = entry.hide_title(); }
517 } else {
518 quote! {}
519 };
520 let readonly = if c.readonly {
521 quote! { let entry = entry.readonly(); }
522 } else {
523 quote! {}
524 };
525 Ok(quote! {
526 .unit_global_config_with(#name, |entry| {
527 let entry = entry;
528 #title
529 #description
530 #hide_title
531 #readonly
532 entry
533 })
534 })
535 }
536 ConfigSpec::Boolean(c) => {
537 let name = c.name.ok_or_else(|| {
538 syn::Error::new(Span::call_site(), "boolean_global_config missing `name`")
539 })?;
540 let default = c.default.unwrap_or_else(|| parse_quote! { false });
541 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
542 let description = c
543 .description
544 .map(|d| quote! { let entry = entry.description(#d); });
545 let hide_title = if c.hide_title {
546 quote! { let entry = entry.hide_title(); }
547 } else {
548 quote! {}
549 };
550 let readonly = if c.readonly {
551 quote! { let entry = entry.readonly(); }
552 } else {
553 quote! {}
554 };
555 Ok(quote! {
556 .boolean_global_config_with(#name, #default, |entry| {
557 let entry = entry;
558 #title
559 #description
560 #hide_title
561 #readonly
562 entry
563 })
564 })
565 }
566 ConfigSpec::Integer(c) => {
567 let name = c.name.ok_or_else(|| {
568 syn::Error::new(Span::call_site(), "integer_global_config missing `name`")
569 })?;
570 let default = c.default.unwrap_or_else(|| parse_quote! { 0i64 });
571 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
572 let description = c
573 .description
574 .map(|d| quote! { let entry = entry.description(#d); });
575 let hide_title = if c.hide_title {
576 quote! { let entry = entry.hide_title(); }
577 } else {
578 quote! {}
579 };
580 let readonly = if c.readonly {
581 quote! { let entry = entry.readonly(); }
582 } else {
583 quote! {}
584 };
585 Ok(quote! {
586 .integer_global_config_with(#name, #default, |entry| {
587 let entry = entry;
588 #title
589 #description
590 #hide_title
591 #readonly
592 entry
593 })
594 })
595 }
596 ConfigSpec::Number(c) => {
597 let name = c.name.ok_or_else(|| {
598 syn::Error::new(Span::call_site(), "number_global_config missing `name`")
599 })?;
600 let default = c.default.unwrap_or_else(|| parse_quote! { 0.0f64 });
601 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
602 let description = c
603 .description
604 .map(|d| quote! { let entry = entry.description(#d); });
605 let hide_title = if c.hide_title {
606 quote! { let entry = entry.hide_title(); }
607 } else {
608 quote! {}
609 };
610 let readonly = if c.readonly {
611 quote! { let entry = entry.readonly(); }
612 } else {
613 quote! {}
614 };
615 Ok(quote! {
616 .number_global_config_with(#name, #default, |entry| {
617 let entry = entry;
618 #title
619 #description
620 #hide_title
621 #readonly
622 entry
623 })
624 })
625 }
626 ConfigSpec::String(c) => {
627 let name = c.name.ok_or_else(|| {
628 syn::Error::new(Span::call_site(), "string_global_config missing `name`")
629 })?;
630 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
631 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
632 let description = c
633 .description
634 .map(|d| quote! { let entry = entry.description(#d); });
635 let hide_title = if c.hide_title {
636 quote! { let entry = entry.hide_title(); }
637 } else {
638 quote! {}
639 };
640 let readonly = if c.readonly {
641 quote! { let entry = entry.readonly(); }
642 } else {
643 quote! {}
644 };
645 Ok(quote! {
646 .string_global_config_with(#name, #default, |entry| {
647 let entry = entry;
648 #title
649 #description
650 #hide_title
651 #readonly
652 entry
653 })
654 })
655 }
656 ConfigSpec::Text(c) => {
657 let name = c.name.ok_or_else(|| {
658 syn::Error::new(Span::call_site(), "text_global_config missing `name`")
659 })?;
660 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
661 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
662 let description = c
663 .description
664 .map(|d| quote! { let entry = entry.description(#d); });
665 let hide_title = if c.hide_title {
666 quote! { let entry = entry.hide_title(); }
667 } else {
668 quote! {}
669 };
670 let readonly = if c.readonly {
671 quote! { let entry = entry.readonly(); }
672 } else {
673 quote! {}
674 };
675 Ok(quote! {
676 .text_global_config_with(#name, #default, |entry| {
677 let entry = entry;
678 #title
679 #description
680 #hide_title
681 #readonly
682 entry
683 })
684 })
685 }
686 ConfigSpec::Object(c) => {
687 let name = c.name.ok_or_else(|| {
688 syn::Error::new(Span::call_site(), "object_global_config missing `name`")
689 })?;
690 let default = c.default.unwrap_or_else(|| {
691 parse_quote! { ::agent_stream_kit::AgentValue::object_default() }
692 });
693 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
694 let description = c
695 .description
696 .map(|d| quote! { let entry = entry.description(#d); });
697 let hide_title = if c.hide_title {
698 quote! { let entry = entry.hide_title(); }
699 } else {
700 quote! {}
701 };
702 let readonly = if c.readonly {
703 quote! { let entry = entry.readonly(); }
704 } else {
705 quote! {}
706 };
707 Ok(quote! {
708 .object_global_config_with(#name, #default, |entry| {
709 let entry = entry;
710 #title
711 #description
712 #hide_title
713 #readonly
714 entry
715 })
716 })
717 }
718 ConfigSpec::Custom(c) => custom_config_call("custom_global_config_with", c),
719 })
720 .collect::<syn::Result<Vec<_>>>()?;
721
722 let definition_builder = quote! {
723 ::agent_stream_kit::AgentDefinition::new(
724 #kind,
725 #name_tokens,
726 Some(::agent_stream_kit::new_agent_boxed::<#ident>),
727 )
728 #title
729 #description
730 #category
731 #inputs
732 #outputs
733 #(#config_calls)*
734 #(#global_config_calls)*
735 };
736
737 let expanded = quote! {
738 #item
739
740 #data_impl
741
742 impl #impl_generics #ident #ty_generics #where_clause {
743 pub const DEF_NAME: &'static str = #name_tokens;
744
745 pub fn def_name() -> &'static str { Self::DEF_NAME }
746
747 pub fn agent_definition() -> ::agent_stream_kit::AgentDefinition {
748 #definition_builder
749 }
750
751 pub fn register(askit: &::agent_stream_kit::ASKit) {
752 askit.register_agent_definiton(Self::agent_definition());
753 }
754 }
755
756 ::agent_stream_kit::inventory::submit! {
757 ::agent_stream_kit::AgentRegistration {
758 build: || #definition_builder,
759 }
760 }
761 };
762
763 Ok(expanded)
764}
765
766fn parse_name_type_title_description(
767 meta: &Meta,
768 name: &mut Option<Expr>,
769 type_: &mut Option<Expr>,
770 title: &mut Option<Expr>,
771 description: &mut Option<Expr>,
772) -> bool {
773 match meta {
774 Meta::NameValue(nv) if nv.path.is_ident("name") => {
775 *name = Some(nv.value.clone());
776 true
777 }
778 Meta::NameValue(nv) if nv.path.is_ident("type") => {
779 *type_ = Some(nv.value.clone());
780 true
781 }
782 Meta::NameValue(nv) if nv.path.is_ident("type_") => {
783 *type_ = Some(nv.value.clone());
784 true
785 }
786 Meta::NameValue(nv) if nv.path.is_ident("title") => {
787 *title = Some(nv.value.clone());
788 true
789 }
790 Meta::NameValue(nv) if nv.path.is_ident("description") => {
791 *description = Some(nv.value.clone());
792 true
793 }
794 _ => false,
795 }
796}
797
798fn parse_custom_config(list: MetaList) -> syn::Result<CustomConfig> {
799 let mut name = None;
800 let mut default = None;
801 let mut type_ = None;
802 let mut title = None;
803 let mut description = None;
804 let mut hide_title = false;
805 let mut readonly = false;
806 let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
807
808 for meta in nested {
809 if parse_name_type_title_description(
810 &meta,
811 &mut name,
812 &mut type_,
813 &mut title,
814 &mut description,
815 ) {
816 continue;
817 }
818
819 match meta {
820 Meta::NameValue(nv) if nv.path.is_ident("default") => {
821 default = Some(nv.value.clone());
822 }
823 Meta::Path(p) if p.is_ident("hide_title") => {
824 hide_title = true;
825 }
826 Meta::Path(p) if p.is_ident("readonly") => {
827 readonly = true;
828 }
829 other => {
830 return Err(syn::Error::new_spanned(
831 other,
832 "custom_config supports name, default, type/type_, title, description, hide_title, readonly",
833 ));
834 }
835 }
836 }
837
838 let name = name.ok_or_else(|| syn::Error::new(list.span(), "config missing `name`"))?;
839 let default =
840 default.ok_or_else(|| syn::Error::new(list.span(), "config missing `default`"))?;
841 let type_ = type_.ok_or_else(|| syn::Error::new(list.span(), "config missing `type`"))?;
842
843 Ok(CustomConfig {
844 name,
845 default,
846 type_,
847 title,
848 description,
849 hide_title,
850 readonly,
851 })
852}
853
854fn collect_exprs(list: MetaList) -> syn::Result<Vec<Expr>> {
855 let values = list.parse_args_with(Punctuated::<Expr, Comma>::parse_terminated)?;
856 Ok(values.into_iter().collect())
857}
858
859fn parse_expr_array(expr: Expr) -> syn::Result<Vec<Expr>> {
860 if let Expr::Array(arr) = expr {
861 Ok(arr.elems.into_iter().collect())
862 } else {
863 Err(syn::Error::new_spanned(
864 expr,
865 "inputs/outputs expect array expressions",
866 ))
867 }
868}
869
870fn parse_common_config(list: MetaList) -> syn::Result<CommonConfig> {
871 let mut cfg = CommonConfig::default();
872 let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
873
874 for meta in nested {
875 match meta {
876 Meta::NameValue(nv) if nv.path.is_ident("name") => {
877 cfg.name = Some(nv.value.clone());
878 }
879 Meta::NameValue(nv) if nv.path.is_ident("default") => {
880 cfg.default = Some(nv.value.clone());
881 }
882 Meta::NameValue(nv) if nv.path.is_ident("title") => {
883 cfg.title = Some(nv.value.clone());
884 }
885 Meta::NameValue(nv) if nv.path.is_ident("description") => {
886 cfg.description = Some(nv.value.clone());
887 }
888 Meta::Path(p) if p.is_ident("hide_title") => {
889 cfg.hide_title = true;
890 }
891 Meta::Path(p) if p.is_ident("readonly") => {
892 cfg.readonly = true;
893 }
894 other => {
895 return Err(syn::Error::new_spanned(
896 other,
897 "config supports name, default, title, description, hide_title, readonly",
898 ));
899 }
900 }
901 }
902
903 if cfg.name.is_none() {
904 return Err(syn::Error::new(list.span(), "config missing `name`"));
905 }
906 Ok(cfg)
907}
908
909fn custom_config_call(method: &str, cfg: CustomConfig) -> syn::Result<proc_macro2::TokenStream> {
910 let CustomConfig {
911 name,
912 default,
913 type_,
914 title,
915 description,
916 hide_title,
917 readonly,
918 } = cfg;
919 let title = title.map(|t| quote! { let entry = entry.title(#t); });
920 let description = description.map(|d| quote! { let entry = entry.description(#d); });
921 let hide_title = if hide_title {
922 quote! { let entry = entry.hide_title(); }
923 } else {
924 quote! {}
925 };
926 let readonly = if readonly {
927 quote! { let entry = entry.readonly(); }
928 } else {
929 quote! {}
930 };
931 let method_ident = format_ident!("{}", method);
932
933 Ok(quote! {
934 .#method_ident(#name, #default, #type_, |entry| {
935 let entry = entry;
936 #title
937 #description
938 #hide_title
939 #readonly
940 entry
941 })
942 })
943}