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