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