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::quote;
9use syn::{
10    spanned::Spanned,
11    parse_macro_input, parse_quote,
12    punctuated::Punctuated,
13    Expr, ItemStruct, Lit, Meta, MetaList, Type, token::Comma,
14};
15
16#[proc_macro_attribute]
17pub fn askit(attr: TokenStream, item: TokenStream) -> TokenStream {
18    askit_agent(attr, item)
19}
20
21/// Declare agent metadata and generate `agent_definition` / `register` helpers.
22///
23/// Example:
24/// ```rust,ignore
25/// #[askit_agent(
26///     kind = "Board",
27///     title = "Board In",
28///     category = "Core",
29///     inputs = ["*"],
30///     string_config(
31///         name = CONFIG_BOARD_NAME,
32///         default = "",
33///         title = "Board Name",
34///         description = "* = source kind"
35///     )
36/// )]
37/// struct BoardInAgent { /* ... */ }
38/// ```
39#[proc_macro_attribute]
40pub fn askit_agent(attr: TokenStream, item: TokenStream) -> TokenStream {
41    let args = parse_macro_input!(attr with Punctuated<Meta, Comma>::parse_terminated);
42    let item_struct = parse_macro_input!(item as ItemStruct);
43
44    match expand_askit_agent(args, item_struct) {
45        Ok(tokens) => tokens.into(),
46        Err(err) => err.into_compile_error().into(),
47    }
48}
49
50struct AgentArgs {
51    kind: Option<Expr>,
52    name: Option<Expr>,
53    title: Option<Expr>,
54    description: Option<Expr>,
55    category: Option<Expr>,
56    inputs: Vec<Expr>,
57    outputs: Vec<Expr>,
58    configs: Vec<ConfigSpec>,
59    global_configs: Vec<ConfigSpec>,
60    displays: Vec<DisplaySpec>,
61}
62
63#[derive(Default)]
64struct CommonConfig {
65    name: Option<Expr>,
66    default: Option<Expr>,
67    title: Option<Expr>,
68    description: Option<Expr>,
69}
70
71enum ConfigSpec {
72    Unit(CommonConfig),
73    Boolean(CommonConfig),
74    Integer(CommonConfig),
75    Number(CommonConfig),
76    String(CommonConfig),
77    Text(CommonConfig),
78    Object(CommonConfig),
79}
80
81enum DisplaySpec {
82    Unit(CommonDisplay),
83    Boolean(CommonDisplay),
84    Integer(CommonDisplay),
85    Number(CommonDisplay),
86    String(CommonDisplay),
87    Text(CommonDisplay),
88    Object(CommonDisplay),
89    Any(CommonDisplay),
90}
91
92#[derive(Default)]
93struct CommonDisplay {
94    name: Option<Expr>,
95    title: Option<Expr>,
96    description: Option<Expr>,
97    hide_title: bool,
98}
99
100fn expand_askit_agent(
101    args: Punctuated<Meta, Comma>,
102    item: ItemStruct,
103) -> syn::Result<proc_macro2::TokenStream> {
104    let has_data_field = item.fields.iter().any(|f| match (&f.ident, &f.ty) {
105        (Some(ident), Type::Path(tp)) if ident == "data" => {
106            tp.path
107                .segments
108                .last()
109                .map(|seg| seg.ident == "AsAgentData")
110                .unwrap_or(false)
111        }
112        _ => false,
113    });
114
115    if !has_data_field {
116        return Err(syn::Error::new(
117            item.span(),
118            "#[askit_agent] expects the struct to have a `data: AsAgentData` field",
119        ));
120    }
121
122    let mut parsed = AgentArgs {
123        kind: None,
124        name: None,
125        title: None,
126        description: None,
127        category: None,
128        inputs: Vec::new(),
129        outputs: Vec::new(),
130        configs: Vec::new(),
131        global_configs: Vec::new(),
132        displays: Vec::new(),
133    };
134
135    for meta in args {
136        match meta {
137            Meta::NameValue(nv) if nv.path.is_ident("kind") => {
138                parsed.kind = Some(nv.value);
139            }
140            Meta::NameValue(nv) if nv.path.is_ident("name") => {
141                parsed.name = Some(nv.value);
142            }
143            Meta::NameValue(nv) if nv.path.is_ident("title") => {
144                parsed.title = Some(nv.value);
145            }
146            Meta::NameValue(nv) if nv.path.is_ident("description") => {
147                parsed.description = Some(nv.value);
148            }
149            Meta::NameValue(nv) if nv.path.is_ident("category") => {
150                parsed.category = Some(nv.value);
151            }
152            Meta::NameValue(nv) if nv.path.is_ident("inputs") => {
153                parsed.inputs = parse_expr_array(nv.value)?;
154            }
155            Meta::NameValue(nv) if nv.path.is_ident("outputs") => {
156                parsed.outputs = parse_expr_array(nv.value)?;
157            }
158            Meta::List(ml) if ml.path.is_ident("inputs") => {
159                parsed.inputs = collect_exprs(ml)?;
160            }
161            Meta::List(ml) if ml.path.is_ident("outputs") => {
162                parsed.outputs = collect_exprs(ml)?;
163            }
164            Meta::List(ml) if ml.path.is_ident("string_config") => {
165                parsed.configs.push(ConfigSpec::String(parse_common_config(ml)?));
166            }
167            Meta::List(ml) if ml.path.is_ident("text_config") => {
168                parsed.configs.push(ConfigSpec::Text(parse_common_config(ml)?));
169            }
170            Meta::List(ml) if ml.path.is_ident("boolean_config") => {
171                parsed.configs.push(ConfigSpec::Boolean(parse_common_config(ml)?));
172            }
173            Meta::List(ml) if ml.path.is_ident("integer_config") => {
174                parsed.configs.push(ConfigSpec::Integer(parse_common_config(ml)?));
175            }
176            Meta::List(ml) if ml.path.is_ident("number_config") => {
177                parsed.configs.push(ConfigSpec::Number(parse_common_config(ml)?));
178            }
179            Meta::List(ml) if ml.path.is_ident("object_config") => {
180                parsed.configs.push(ConfigSpec::Object(parse_common_config(ml)?));
181            }
182            Meta::List(ml) if ml.path.is_ident("unit_config") => {
183                parsed.configs.push(ConfigSpec::Unit(parse_common_config(ml)?));
184            }
185            Meta::List(ml) if ml.path.is_ident("string_global_config") => {
186                parsed.global_configs.push(ConfigSpec::String(parse_common_config(ml)?));
187            }
188            Meta::List(ml) if ml.path.is_ident("text_global_config") => {
189                parsed.global_configs.push(ConfigSpec::Text(parse_common_config(ml)?));
190            }
191            Meta::List(ml) if ml.path.is_ident("boolean_global_config") => {
192                parsed.global_configs.push(ConfigSpec::Boolean(parse_common_config(ml)?));
193            }
194            Meta::List(ml) if ml.path.is_ident("integer_global_config") => {
195                parsed.global_configs.push(ConfigSpec::Integer(parse_common_config(ml)?));
196            }
197            Meta::List(ml) if ml.path.is_ident("number_global_config") => {
198                parsed.global_configs.push(ConfigSpec::Number(parse_common_config(ml)?));
199            }
200            Meta::List(ml) if ml.path.is_ident("object_global_config") => {
201                parsed.global_configs.push(ConfigSpec::Object(parse_common_config(ml)?));
202            }
203            Meta::List(ml) if ml.path.is_ident("unit_global_config") => {
204                parsed.global_configs.push(ConfigSpec::Unit(parse_common_config(ml)?));
205            }
206            Meta::List(ml) if ml.path.is_ident("unit_display") => {
207                parsed.displays.push(DisplaySpec::Unit(parse_common_display(ml)?));
208            }
209            Meta::List(ml) if ml.path.is_ident("boolean_display") => {
210                parsed.displays.push(DisplaySpec::Boolean(parse_common_display(ml)?));
211            }
212            Meta::List(ml) if ml.path.is_ident("integer_display") => {
213                parsed.displays.push(DisplaySpec::Integer(parse_common_display(ml)?));
214            }
215            Meta::List(ml) if ml.path.is_ident("number_display") => {
216                parsed.displays.push(DisplaySpec::Number(parse_common_display(ml)?));
217            }
218            Meta::List(ml) if ml.path.is_ident("string_display") => {
219                parsed.displays.push(DisplaySpec::String(parse_common_display(ml)?));
220            }
221            Meta::List(ml) if ml.path.is_ident("text_display") => {
222                parsed.displays.push(DisplaySpec::Text(parse_common_display(ml)?));
223            }
224            Meta::List(ml) if ml.path.is_ident("object_display") => {
225                parsed.displays.push(DisplaySpec::Object(parse_common_display(ml)?));
226            }
227            Meta::List(ml) if ml.path.is_ident("any_display") => {
228                parsed.displays.push(DisplaySpec::Any(parse_common_display(ml)?));
229            }
230            other => {
231                return Err(syn::Error::new_spanned(
232                    other,
233                    "unsupported askit_agent argument",
234                ));
235            }
236        }
237    }
238
239    let ident = &item.ident;
240    let generics = item.generics.clone();
241    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
242    let data_impl = quote! {
243        impl #impl_generics ::agent_stream_kit::HasAgentData for #ident #ty_generics #where_clause {
244            fn data(&self) -> &::agent_stream_kit::AsAgentData {
245                &self.data
246            }
247
248            fn mut_data(&mut self) -> &mut ::agent_stream_kit::AsAgentData {
249                &mut self.data
250            }
251        }
252    };
253
254    let kind = parsed.kind.unwrap_or_else(|| parse_quote! { "Agent" });
255    let name_tokens = parsed.name.map(|n| quote! { #n }).unwrap_or_else(|| {
256        quote! { concat!(module_path!(), "::", stringify!(#ident)) }
257    });
258
259    let title = parsed
260        .title
261        .ok_or_else(|| syn::Error::new(Span::call_site(), "askit_agent: missing `title`"))?;
262    let category = parsed
263        .category
264        .ok_or_else(|| syn::Error::new(Span::call_site(), "askit_agent: missing `category`"))?;
265    let title = quote! { .title(#title) };
266    let description = parsed.description.map(|d| quote! { .description(#d) });
267    let category = quote! { .category(#category) };
268
269    let inputs = if parsed.inputs.is_empty() {
270        quote! {}
271    } else {
272        let values = parsed.inputs;
273        quote! { .inputs(vec![#(#values),*]) }
274    };
275
276    let outputs = if parsed.outputs.is_empty() {
277        quote! {}
278    } else {
279        let values = parsed.outputs;
280        quote! { .outputs(vec![#(#values),*]) }
281    };
282
283    let config_calls = parsed
284        .configs
285        .into_iter()
286        .map(|cfg| match cfg {
287            ConfigSpec::Unit(c) => {
288                let name = c.name.ok_or_else(|| {
289                    syn::Error::new(Span::call_site(), "unit_config missing `name`")
290                })?;
291                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
292                let description = c
293                    .description
294                    .map(|d| quote! { let entry = entry.description(#d); });
295                Ok(quote! {
296                    .unit_config_with(#name, |entry| {
297                        let entry = entry;
298                        #title
299                        #description
300                        entry
301                    })
302                })
303            }
304            ConfigSpec::Boolean(c) => {
305                let name = c.name.ok_or_else(|| {
306                    syn::Error::new(Span::call_site(), "boolean_config missing `name`")
307                })?;
308                let default = c.default.unwrap_or_else(|| parse_quote! { false });
309                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
310                let description = c
311                    .description
312                    .map(|d| quote! { let entry = entry.description(#d); });
313                Ok(quote! {
314                    .boolean_config_with(#name, #default, |entry| {
315                        let entry = entry;
316                        #title
317                        #description
318                        entry
319                    })
320                })
321            }
322            ConfigSpec::Integer(c) => {
323                let name = c.name.ok_or_else(|| {
324                    syn::Error::new(Span::call_site(), "integer_config missing `name`")
325                })?;
326                let default = c.default.unwrap_or_else(|| parse_quote! { 0i64 });
327                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
328                let description = c
329                    .description
330                    .map(|d| quote! { let entry = entry.description(#d); });
331                Ok(quote! {
332                    .integer_config_with(#name, #default, |entry| {
333                        let entry = entry;
334                        #title
335                        #description
336                        entry
337                    })
338                })
339            }
340            ConfigSpec::Number(c) => {
341                let name = c.name.ok_or_else(|| {
342                    syn::Error::new(Span::call_site(), "number_config missing `name`")
343                })?;
344                let default = c.default.unwrap_or_else(|| parse_quote! { 0.0f64 });
345                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
346                let description = c
347                    .description
348                    .map(|d| quote! { let entry = entry.description(#d); });
349                Ok(quote! {
350                    .number_config_with(#name, #default, |entry| {
351                        let entry = entry;
352                        #title
353                        #description
354                        entry
355                    })
356                })
357            }
358            ConfigSpec::String(c) => {
359                let name = c.name.ok_or_else(|| {
360                    syn::Error::new(Span::call_site(), "string_config missing `name`")
361                })?;
362                let default = c.default.unwrap_or_else(|| parse_quote! { "" });
363                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
364                let description = c
365                    .description
366                    .map(|d| quote! { let entry = entry.description(#d); });
367                Ok(quote! {
368                    .string_config_with(#name, #default, |entry| {
369                        let entry = entry;
370                        #title
371                        #description
372                        entry
373                    })
374                })
375            }
376            ConfigSpec::Text(c) => {
377                let name = c.name.ok_or_else(|| {
378                    syn::Error::new(Span::call_site(), "text_config missing `name`")
379                })?;
380                let default = c.default.unwrap_or_else(|| parse_quote! { "" });
381                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
382                let description = c
383                    .description
384                    .map(|d| quote! { let entry = entry.description(#d); });
385                Ok(quote! {
386                    .text_config_with(#name, #default, |entry| {
387                        let entry = entry;
388                        #title
389                        #description
390                        entry
391                    })
392                })
393            }
394            ConfigSpec::Object(c) => {
395                let name = c.name.ok_or_else(|| {
396                    syn::Error::new(Span::call_site(), "object_config missing `name`")
397                })?;
398                let default = c.default.unwrap_or_else(|| {
399                    parse_quote! { ::agent_stream_kit::AgentValue::object_default() }
400                });
401                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
402                let description = c
403                    .description
404                    .map(|d| quote! { let entry = entry.description(#d); });
405                Ok(quote! {
406                    .object_config_with(#name, #default, |entry| {
407                        let entry = entry;
408                        #title
409                        #description
410                        entry
411                    })
412                })
413            }
414        })
415        .collect::<syn::Result<Vec<_>>>()?;
416
417    let global_config_calls = parsed
418        .global_configs
419        .into_iter()
420        .map(|cfg| match cfg {
421            ConfigSpec::Unit(c) => {
422                let name = c.name.ok_or_else(|| {
423                    syn::Error::new(Span::call_site(), "unit_global_config missing `name`")
424                })?;
425                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
426                let description = c
427                    .description
428                    .map(|d| quote! { let entry = entry.description(#d); });
429                Ok(quote! {
430                    .unit_global_config_with(#name, |entry| {
431                        let entry = entry;
432                        #title
433                        #description
434                        entry
435                    })
436                })
437            }
438            ConfigSpec::Boolean(c) => {
439                let name = c.name.ok_or_else(|| {
440                    syn::Error::new(Span::call_site(), "boolean_global_config missing `name`")
441                })?;
442                let default = c.default.unwrap_or_else(|| parse_quote! { false });
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                Ok(quote! {
448                    .boolean_global_config_with(#name, #default, |entry| {
449                        let entry = entry;
450                        #title
451                        #description
452                        entry
453                    })
454                })
455            }
456            ConfigSpec::Integer(c) => {
457                let name = c.name.ok_or_else(|| {
458                    syn::Error::new(Span::call_site(), "integer_global_config missing `name`")
459                })?;
460                let default = c.default.unwrap_or_else(|| parse_quote! { 0i64 });
461                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
462                let description = c
463                    .description
464                    .map(|d| quote! { let entry = entry.description(#d); });
465                Ok(quote! {
466                    .integer_global_config_with(#name, #default, |entry| {
467                        let entry = entry;
468                        #title
469                        #description
470                        entry
471                    })
472                })
473            }
474            ConfigSpec::Number(c) => {
475                let name = c.name.ok_or_else(|| {
476                    syn::Error::new(Span::call_site(), "number_global_config missing `name`")
477                })?;
478                let default = c.default.unwrap_or_else(|| parse_quote! { 0.0f64 });
479                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
480                let description = c
481                    .description
482                    .map(|d| quote! { let entry = entry.description(#d); });
483                Ok(quote! {
484                    .number_global_config_with(#name, #default, |entry| {
485                        let entry = entry;
486                        #title
487                        #description
488                        entry
489                    })
490                })
491            }
492            ConfigSpec::String(c) => {
493                let name = c.name.ok_or_else(|| {
494                    syn::Error::new(Span::call_site(), "string_global_config missing `name`")
495                })?;
496                let default = c.default.unwrap_or_else(|| parse_quote! { "" });
497                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
498                let description = c
499                    .description
500                    .map(|d| quote! { let entry = entry.description(#d); });
501                Ok(quote! {
502                    .string_global_config_with(#name, #default, |entry| {
503                        let entry = entry;
504                        #title
505                        #description
506                        entry
507                    })
508                })
509            }
510            ConfigSpec::Text(c) => {
511                let name = c.name.ok_or_else(|| {
512                    syn::Error::new(Span::call_site(), "text_global_config missing `name`")
513                })?;
514                let default = c.default.unwrap_or_else(|| parse_quote! { "" });
515                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
516                let description = c
517                    .description
518                    .map(|d| quote! { let entry = entry.description(#d); });
519                Ok(quote! {
520                    .text_global_config_with(#name, #default, |entry| {
521                        let entry = entry;
522                        #title
523                        #description
524                        entry
525                    })
526                })
527            }
528            ConfigSpec::Object(c) => {
529                let name = c.name.ok_or_else(|| {
530                    syn::Error::new(Span::call_site(), "object_global_config missing `name`")
531                })?;
532                let default = c.default.unwrap_or_else(|| {
533                    parse_quote! { ::agent_stream_kit::AgentValue::object_default() }
534                });
535                let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
536                let description = c
537                    .description
538                    .map(|d| quote! { let entry = entry.description(#d); });
539                Ok(quote! {
540                    .object_global_config_with(#name, #default, |entry| {
541                        let entry = entry;
542                        #title
543                        #description
544                        entry
545                    })
546                })
547            }
548        })
549        .collect::<syn::Result<Vec<_>>>()?;
550
551    let display_calls = parsed
552        .displays
553        .into_iter()
554        .map(|disp| match disp {
555            DisplaySpec::Unit(c) => display_call("unit", c),
556            DisplaySpec::Boolean(c) => display_call("boolean", c),
557            DisplaySpec::Integer(c) => display_call("integer", c),
558            DisplaySpec::Number(c) => display_call("number", c),
559            DisplaySpec::String(c) => display_call("string", c),
560            DisplaySpec::Text(c) => display_call("text", c),
561            DisplaySpec::Object(c) => display_call("object", c),
562            DisplaySpec::Any(c) => display_call("*", c),
563        })
564        .collect::<syn::Result<Vec<_>>>()?;
565
566    let definition_builder = quote! {
567        ::agent_stream_kit::AgentDefinition::new(
568            #kind,
569            #name_tokens,
570            Some(::agent_stream_kit::new_agent_boxed::<#ident>),
571        )
572        #title
573        #description
574        #category
575        #inputs
576        #outputs
577        #(#config_calls)*
578        #(#global_config_calls)*
579        #(#display_calls)*
580    };
581
582    let expanded = quote! {
583        #item
584
585        #data_impl
586
587        impl #impl_generics #ident #ty_generics #where_clause {
588            pub fn agent_definition() -> ::agent_stream_kit::AgentDefinition {
589                #definition_builder
590            }
591
592            pub fn register(askit: &::agent_stream_kit::ASKit) {
593                askit.register_agent(Self::agent_definition());
594            }
595        }
596
597        ::agent_stream_kit::inventory::submit! {
598            ::agent_stream_kit::AgentRegistration {
599                build: || #definition_builder,
600            }
601        }
602    };
603
604    Ok(expanded)
605}
606
607fn collect_exprs(list: MetaList) -> syn::Result<Vec<Expr>> {
608    let values = list.parse_args_with(Punctuated::<Expr, Comma>::parse_terminated)?;
609    Ok(values.into_iter().collect())
610}
611
612fn parse_expr_array(expr: Expr) -> syn::Result<Vec<Expr>> {
613    if let Expr::Array(arr) = expr {
614        Ok(arr.elems.into_iter().collect())
615    } else {
616        Err(syn::Error::new_spanned(
617            expr,
618            "inputs/outputs expect array expressions",
619        ))
620    }
621}
622
623fn parse_common_config(list: MetaList) -> syn::Result<CommonConfig> {
624    let mut cfg = CommonConfig::default();
625    let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
626
627    for meta in nested {
628        match meta {
629            Meta::NameValue(nv) if nv.path.is_ident("name") => {
630                cfg.name = Some(match &nv.value {
631                    Expr::Lit(expr_lit) => match &expr_lit.lit {
632                        Lit::Str(s) => syn::parse_str::<Expr>(&s.value())?,
633                        _ => nv.value.clone(),
634                    },
635                    _ => nv.value.clone(),
636                });
637            }
638            Meta::NameValue(nv) if nv.path.is_ident("default") => {
639                cfg.default = Some(nv.value.clone());
640            }
641            Meta::NameValue(nv) if nv.path.is_ident("title") => {
642                cfg.title = Some(nv.value.clone());
643            }
644            Meta::NameValue(nv) if nv.path.is_ident("description") => {
645                cfg.description = Some(nv.value.clone());
646            }
647            other => {
648                return Err(syn::Error::new_spanned(
649                    other,
650                    "config supports name, default, title, description",
651                ));
652            }
653        }
654    }
655
656    if cfg.name.is_none() {
657        return Err(syn::Error::new(
658            list.span(),
659            "config missing `name`",
660        ));
661    }
662    Ok(cfg)
663}
664
665fn parse_common_display(list: MetaList) -> syn::Result<CommonDisplay> {
666    let mut cfg = CommonDisplay::default();
667    let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
668
669    for meta in nested {
670        match meta {
671            Meta::NameValue(nv) if nv.path.is_ident("name") => {
672                cfg.name = Some(match &nv.value {
673                    Expr::Lit(expr_lit) => match &expr_lit.lit {
674                        Lit::Str(s) => syn::parse_str::<Expr>(&s.value())?,
675                        _ => nv.value.clone(),
676                    },
677                    _ => nv.value.clone(),
678                });
679            }
680            Meta::NameValue(nv) if nv.path.is_ident("title") => {
681                cfg.title = Some(nv.value.clone());
682            }
683            Meta::NameValue(nv) if nv.path.is_ident("description") => {
684                cfg.description = Some(nv.value.clone());
685            }
686            Meta::Path(p) if p.is_ident("hide_title") => {
687                cfg.hide_title = true;
688            }
689            other => {
690                return Err(syn::Error::new_spanned(
691                    other,
692                    "display supports name, title, description, hide_title",
693                ));
694            }
695        }
696    }
697
698    if cfg.name.is_none() {
699        return Err(syn::Error::new(list.span(), "display missing `name`"));
700    }
701    Ok(cfg)
702}
703
704fn display_call(type_name: &str, cfg: CommonDisplay) -> syn::Result<proc_macro2::TokenStream> {
705    let name = cfg
706        .name
707        .ok_or_else(|| syn::Error::new(Span::call_site(), "display missing `name`"))?;
708    let title = cfg.title.map(|t| quote! { let entry = entry.title(#t); });
709    let description = cfg
710        .description
711        .map(|d| quote! { let entry = entry.description(#d); });
712    let hide_title = if cfg.hide_title {
713        quote! { let entry = entry.hide_title(); }
714    } else {
715        quote! {}
716    };
717
718    Ok(quote! {
719        .custom_display_config_with(#name, #type_name, |entry| {
720            let entry = entry;
721            #title
722            #description
723            #hide_title
724            entry
725        })
726    })
727}