1use 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, token::Comma,
14};
15
16#[proc_macro_attribute]
17pub fn askit(attr: TokenStream, item: TokenStream) -> TokenStream {
18 askit_agent(attr, item)
19}
20
21#[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 displays: Vec<DisplaySpec>,
60}
61
62#[derive(Default)]
63struct CommonConfig {
64 name: Option<Expr>,
65 default: Option<Expr>,
66 title: Option<Expr>,
67 description: Option<Expr>,
68}
69
70enum ConfigSpec {
71 Unit(CommonConfig),
72 Boolean(CommonConfig),
73 Integer(CommonConfig),
74 Number(CommonConfig),
75 String(CommonConfig),
76 Text(CommonConfig),
77 Object(CommonConfig),
78}
79
80enum DisplaySpec {
81 Unit(CommonDisplay),
82 Boolean(CommonDisplay),
83 Integer(CommonDisplay),
84 Number(CommonDisplay),
85 String(CommonDisplay),
86 Text(CommonDisplay),
87 Object(CommonDisplay),
88 Any(CommonDisplay),
89}
90
91#[derive(Default)]
92struct CommonDisplay {
93 name: Option<Expr>,
94 title: Option<Expr>,
95 description: Option<Expr>,
96 hide_title: bool,
97}
98
99fn expand_askit_agent(
100 args: Punctuated<Meta, Comma>,
101 item: ItemStruct,
102) -> syn::Result<proc_macro2::TokenStream> {
103 let mut parsed = AgentArgs {
104 kind: None,
105 name: None,
106 title: None,
107 description: None,
108 category: None,
109 inputs: Vec::new(),
110 outputs: Vec::new(),
111 configs: Vec::new(),
112 displays: Vec::new(),
113 };
114
115 for meta in args {
116 match meta {
117 Meta::NameValue(nv) if nv.path.is_ident("kind") => {
118 parsed.kind = Some(nv.value);
119 }
120 Meta::NameValue(nv) if nv.path.is_ident("name") => {
121 parsed.name = Some(nv.value);
122 }
123 Meta::NameValue(nv) if nv.path.is_ident("title") => {
124 parsed.title = Some(nv.value);
125 }
126 Meta::NameValue(nv) if nv.path.is_ident("description") => {
127 parsed.description = Some(nv.value);
128 }
129 Meta::NameValue(nv) if nv.path.is_ident("category") => {
130 parsed.category = Some(nv.value);
131 }
132 Meta::NameValue(nv) if nv.path.is_ident("inputs") => {
133 parsed.inputs = parse_expr_array(nv.value)?;
134 }
135 Meta::NameValue(nv) if nv.path.is_ident("outputs") => {
136 parsed.outputs = parse_expr_array(nv.value)?;
137 }
138 Meta::List(ml) if ml.path.is_ident("inputs") => {
139 parsed.inputs = collect_exprs(ml)?;
140 }
141 Meta::List(ml) if ml.path.is_ident("outputs") => {
142 parsed.outputs = collect_exprs(ml)?;
143 }
144 Meta::List(ml) if ml.path.is_ident("string_config") => {
145 parsed.configs.push(ConfigSpec::String(parse_common_config(ml)?));
146 }
147 Meta::List(ml) if ml.path.is_ident("text_config") => {
148 parsed.configs.push(ConfigSpec::Text(parse_common_config(ml)?));
149 }
150 Meta::List(ml) if ml.path.is_ident("boolean_config") => {
151 parsed.configs.push(ConfigSpec::Boolean(parse_common_config(ml)?));
152 }
153 Meta::List(ml) if ml.path.is_ident("integer_config") => {
154 parsed.configs.push(ConfigSpec::Integer(parse_common_config(ml)?));
155 }
156 Meta::List(ml) if ml.path.is_ident("number_config") => {
157 parsed.configs.push(ConfigSpec::Number(parse_common_config(ml)?));
158 }
159 Meta::List(ml) if ml.path.is_ident("object_config") => {
160 parsed.configs.push(ConfigSpec::Object(parse_common_config(ml)?));
161 }
162 Meta::List(ml) if ml.path.is_ident("unit_config") => {
163 parsed.configs.push(ConfigSpec::Unit(parse_common_config(ml)?));
164 }
165 Meta::List(ml) if ml.path.is_ident("unit_display") => {
166 parsed.displays.push(DisplaySpec::Unit(parse_common_display(ml)?));
167 }
168 Meta::List(ml) if ml.path.is_ident("boolean_display") => {
169 parsed.displays.push(DisplaySpec::Boolean(parse_common_display(ml)?));
170 }
171 Meta::List(ml) if ml.path.is_ident("integer_display") => {
172 parsed.displays.push(DisplaySpec::Integer(parse_common_display(ml)?));
173 }
174 Meta::List(ml) if ml.path.is_ident("number_display") => {
175 parsed.displays.push(DisplaySpec::Number(parse_common_display(ml)?));
176 }
177 Meta::List(ml) if ml.path.is_ident("string_display") => {
178 parsed.displays.push(DisplaySpec::String(parse_common_display(ml)?));
179 }
180 Meta::List(ml) if ml.path.is_ident("text_display") => {
181 parsed.displays.push(DisplaySpec::Text(parse_common_display(ml)?));
182 }
183 Meta::List(ml) if ml.path.is_ident("object_display") => {
184 parsed.displays.push(DisplaySpec::Object(parse_common_display(ml)?));
185 }
186 Meta::List(ml) if ml.path.is_ident("any_display") => {
187 parsed.displays.push(DisplaySpec::Any(parse_common_display(ml)?));
188 }
189 other => {
190 return Err(syn::Error::new_spanned(
191 other,
192 "unsupported askit_agent argument",
193 ));
194 }
195 }
196 }
197
198 let ident = &item.ident;
199 let generics = item.generics.clone();
200 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
201
202 let kind = parsed.kind.unwrap_or_else(|| parse_quote! { "Agent" });
203 let name_tokens = parsed.name.map(|n| quote! { #n }).unwrap_or_else(|| {
204 quote! { concat!(module_path!(), "::", stringify!(#ident)) }
205 });
206
207 let title = parsed
208 .title
209 .ok_or_else(|| syn::Error::new(Span::call_site(), "askit_agent: missing `title`"))?;
210 let category = parsed
211 .category
212 .ok_or_else(|| syn::Error::new(Span::call_site(), "askit_agent: missing `category`"))?;
213 let title = quote! { .title(#title) };
214 let description = parsed.description.map(|d| quote! { .description(#d) });
215 let category = quote! { .category(#category) };
216
217 let inputs = if parsed.inputs.is_empty() {
218 quote! {}
219 } else {
220 let values = parsed.inputs;
221 quote! { .inputs(vec![#(#values),*]) }
222 };
223
224 let outputs = if parsed.outputs.is_empty() {
225 quote! {}
226 } else {
227 let values = parsed.outputs;
228 quote! { .outputs(vec![#(#values),*]) }
229 };
230
231 let config_calls = parsed
232 .configs
233 .into_iter()
234 .map(|cfg| match cfg {
235 ConfigSpec::Unit(c) => {
236 let name = c.name.ok_or_else(|| {
237 syn::Error::new(Span::call_site(), "unit_config missing `name`")
238 })?;
239 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
240 let description = c
241 .description
242 .map(|d| quote! { let entry = entry.description(#d); });
243 Ok(quote! {
244 .unit_config_with(#name, |entry| {
245 let entry = entry;
246 #title
247 #description
248 entry
249 })
250 })
251 }
252 ConfigSpec::Boolean(c) => {
253 let name = c.name.ok_or_else(|| {
254 syn::Error::new(Span::call_site(), "boolean_config missing `name`")
255 })?;
256 let default = c.default.unwrap_or_else(|| parse_quote! { false });
257 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
258 let description = c
259 .description
260 .map(|d| quote! { let entry = entry.description(#d); });
261 Ok(quote! {
262 .boolean_config_with(#name, #default, |entry| {
263 let entry = entry;
264 #title
265 #description
266 entry
267 })
268 })
269 }
270 ConfigSpec::Integer(c) => {
271 let name = c.name.ok_or_else(|| {
272 syn::Error::new(Span::call_site(), "integer_config missing `name`")
273 })?;
274 let default = c.default.unwrap_or_else(|| parse_quote! { 0i64 });
275 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
276 let description = c
277 .description
278 .map(|d| quote! { let entry = entry.description(#d); });
279 Ok(quote! {
280 .integer_config_with(#name, #default, |entry| {
281 let entry = entry;
282 #title
283 #description
284 entry
285 })
286 })
287 }
288 ConfigSpec::Number(c) => {
289 let name = c.name.ok_or_else(|| {
290 syn::Error::new(Span::call_site(), "number_config missing `name`")
291 })?;
292 let default = c.default.unwrap_or_else(|| parse_quote! { 0.0f64 });
293 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
294 let description = c
295 .description
296 .map(|d| quote! { let entry = entry.description(#d); });
297 Ok(quote! {
298 .number_config_with(#name, #default, |entry| {
299 let entry = entry;
300 #title
301 #description
302 entry
303 })
304 })
305 }
306 ConfigSpec::String(c) => {
307 let name = c.name.ok_or_else(|| {
308 syn::Error::new(Span::call_site(), "string_config missing `name`")
309 })?;
310 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
311 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
312 let description = c
313 .description
314 .map(|d| quote! { let entry = entry.description(#d); });
315 Ok(quote! {
316 .string_config_with(#name, #default, |entry| {
317 let entry = entry;
318 #title
319 #description
320 entry
321 })
322 })
323 }
324 ConfigSpec::Text(c) => {
325 let name = c.name.ok_or_else(|| {
326 syn::Error::new(Span::call_site(), "text_config missing `name`")
327 })?;
328 let default = c.default.unwrap_or_else(|| parse_quote! { "" });
329 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
330 let description = c
331 .description
332 .map(|d| quote! { let entry = entry.description(#d); });
333 Ok(quote! {
334 .text_config_with(#name, #default, |entry| {
335 let entry = entry;
336 #title
337 #description
338 entry
339 })
340 })
341 }
342 ConfigSpec::Object(c) => {
343 let name = c.name.ok_or_else(|| {
344 syn::Error::new(Span::call_site(), "object_config missing `name`")
345 })?;
346 let default = c.default.unwrap_or_else(|| {
347 parse_quote! { ::agent_stream_kit::AgentValue::object_default() }
348 });
349 let title = c.title.map(|t| quote! { let entry = entry.title(#t); });
350 let description = c
351 .description
352 .map(|d| quote! { let entry = entry.description(#d); });
353 Ok(quote! {
354 .object_config_with(#name, #default, |entry| {
355 let entry = entry;
356 #title
357 #description
358 entry
359 })
360 })
361 }
362 })
363 .collect::<syn::Result<Vec<_>>>()?;
364
365 let display_calls = parsed
366 .displays
367 .into_iter()
368 .map(|disp| match disp {
369 DisplaySpec::Unit(c) => display_call("unit", c),
370 DisplaySpec::Boolean(c) => display_call("boolean", c),
371 DisplaySpec::Integer(c) => display_call("integer", c),
372 DisplaySpec::Number(c) => display_call("number", c),
373 DisplaySpec::String(c) => display_call("string", c),
374 DisplaySpec::Text(c) => display_call("text", c),
375 DisplaySpec::Object(c) => display_call("object", c),
376 DisplaySpec::Any(c) => display_call("*", c),
377 })
378 .collect::<syn::Result<Vec<_>>>()?;
379
380 let definition_builder = quote! {
381 ::agent_stream_kit::AgentDefinition::new(
382 #kind,
383 #name_tokens,
384 Some(::agent_stream_kit::new_agent_boxed::<#ident>),
385 )
386 #title
387 #description
388 #category
389 #inputs
390 #outputs
391 #(#config_calls)*
392 #(#display_calls)*
393 };
394
395 let expanded = quote! {
396 #item
397
398 impl #impl_generics #ident #ty_generics #where_clause {
399 pub fn agent_definition() -> ::agent_stream_kit::AgentDefinition {
400 #definition_builder
401 }
402
403 pub fn register(askit: &::agent_stream_kit::ASKit) {
404 askit.register_agent(Self::agent_definition());
405 }
406 }
407
408 ::agent_stream_kit::inventory::submit! {
409 ::agent_stream_kit::AgentRegistration {
410 build: || #definition_builder,
411 }
412 }
413 };
414
415 Ok(expanded)
416}
417
418fn collect_exprs(list: MetaList) -> syn::Result<Vec<Expr>> {
419 let values = list.parse_args_with(Punctuated::<Expr, Comma>::parse_terminated)?;
420 Ok(values.into_iter().collect())
421}
422
423fn parse_expr_array(expr: Expr) -> syn::Result<Vec<Expr>> {
424 if let Expr::Array(arr) = expr {
425 Ok(arr.elems.into_iter().collect())
426 } else {
427 Err(syn::Error::new_spanned(
428 expr,
429 "inputs/outputs expect array expressions",
430 ))
431 }
432}
433
434fn parse_common_config(list: MetaList) -> syn::Result<CommonConfig> {
435 let mut cfg = CommonConfig::default();
436 let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
437
438 for meta in nested {
439 match meta {
440 Meta::NameValue(nv) if nv.path.is_ident("name") => {
441 cfg.name = Some(match &nv.value {
442 Expr::Lit(expr_lit) => match &expr_lit.lit {
443 Lit::Str(s) => syn::parse_str::<Expr>(&s.value())?,
444 _ => nv.value.clone(),
445 },
446 _ => nv.value.clone(),
447 });
448 }
449 Meta::NameValue(nv) if nv.path.is_ident("default") => {
450 cfg.default = Some(nv.value.clone());
451 }
452 Meta::NameValue(nv) if nv.path.is_ident("title") => {
453 cfg.title = Some(nv.value.clone());
454 }
455 Meta::NameValue(nv) if nv.path.is_ident("description") => {
456 cfg.description = Some(nv.value.clone());
457 }
458 other => {
459 return Err(syn::Error::new_spanned(
460 other,
461 "config supports name, default, title, description",
462 ));
463 }
464 }
465 }
466
467 if cfg.name.is_none() {
468 return Err(syn::Error::new(
469 list.span(),
470 "config missing `name`",
471 ));
472 }
473 Ok(cfg)
474}
475
476fn parse_common_display(list: MetaList) -> syn::Result<CommonDisplay> {
477 let mut cfg = CommonDisplay::default();
478 let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
479
480 for meta in nested {
481 match meta {
482 Meta::NameValue(nv) if nv.path.is_ident("name") => {
483 cfg.name = Some(match &nv.value {
484 Expr::Lit(expr_lit) => match &expr_lit.lit {
485 Lit::Str(s) => syn::parse_str::<Expr>(&s.value())?,
486 _ => nv.value.clone(),
487 },
488 _ => nv.value.clone(),
489 });
490 }
491 Meta::NameValue(nv) if nv.path.is_ident("title") => {
492 cfg.title = Some(nv.value.clone());
493 }
494 Meta::NameValue(nv) if nv.path.is_ident("description") => {
495 cfg.description = Some(nv.value.clone());
496 }
497 Meta::Path(p) if p.is_ident("hide_title") => {
498 cfg.hide_title = true;
499 }
500 other => {
501 return Err(syn::Error::new_spanned(
502 other,
503 "display supports name, title, description, hide_title",
504 ));
505 }
506 }
507 }
508
509 if cfg.name.is_none() {
510 return Err(syn::Error::new(list.span(), "display missing `name`"));
511 }
512 Ok(cfg)
513}
514
515fn display_call(type_name: &str, cfg: CommonDisplay) -> syn::Result<proc_macro2::TokenStream> {
516 let name = cfg
517 .name
518 .ok_or_else(|| syn::Error::new(Span::call_site(), "display missing `name`"))?;
519 let title = cfg.title.map(|t| quote! { let entry = entry.title(#t); });
520 let description = cfg
521 .description
522 .map(|d| quote! { let entry = entry.description(#d); });
523 let hide_title = if cfg.hide_title {
524 quote! { let entry = entry.hide_title(); }
525 } else {
526 quote! {}
527 };
528
529 Ok(quote! {
530 .custom_display_config_with(#name, #type_name, |entry| {
531 let entry = entry;
532 #title
533 #description
534 #hide_title
535 entry
536 })
537 })
538}