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