askit_macros/
lib.rs

1//! Procedural macros for agent-stream-kit.
2//!
3//! Provides an attribute to declare agent metadata alongside the agent type and
4//! generate the registration boilerplate.
5
6use 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/// Declare agent metadata and generate `agent_definition` / `register` helpers.
20///
21/// Example:
22/// ```rust,ignore
23/// #[askit_agent(
24///     title = "Add Int",
25///     category = "Utils",
26///     inputs = ["int"],
27///     outputs = ["int"],
28///     integer_config(
29///         name = "n",
30///         default = 1,
31///     )
32/// )]
33/// struct AdderAgent { /* ... */ }
34/// ```
35#[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}