1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![forbid(unsafe_code)]
3use proc_macro::TokenStream;
33use proc_macro2::Span;
34use quote::{format_ident, quote};
35use syn::{
36 Data, DeriveInput, Expr, ExprLit, Fields, FnArg, ItemFn, Lit, LitInt, LitStr, Meta, Pat,
37 PatType, ReturnType, Type, TypePath, parse_macro_input,
38};
39
40fn runtime() -> (proc_macro2::TokenStream, String) {
50 use proc_macro_crate::{FoundCrate, crate_name};
51 match crate_name("gemini-adk-rs") {
52 Ok(FoundCrate::Name(name)) => {
53 let ident = format_ident!("{name}");
54 (quote! { ::#ident }, name)
55 }
56 Ok(FoundCrate::Itself) => (quote! { ::gemini_adk_rs }, "gemini_adk_rs".to_string()),
57 Err(_) => ["gemini-adk", "gemini-adk-fluent-rs"]
61 .into_iter()
62 .find_map(|facade| match crate_name(facade) {
63 Ok(FoundCrate::Name(name)) => Some(name),
64 Ok(FoundCrate::Itself) => Some(facade.replace('-', "_")),
65 Err(_) => None,
66 })
67 .map_or_else(
68 || (quote! { ::gemini_adk_rs }, "gemini_adk_rs".to_string()),
69 |name| {
70 let ident = format_ident!("{name}");
71 (
72 quote! { ::#ident::gemini_adk_rs },
73 format!("{name}::gemini_adk_rs"),
74 )
75 },
76 ),
77 }
78}
79
80#[proc_macro_attribute]
153pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
154 let description = if attr.is_empty() {
155 None
156 } else {
157 Some(parse_macro_input!(attr as LitStr))
158 };
159 let func = parse_macro_input!(item as ItemFn);
160
161 match expand(description, func) {
162 Ok(ts) => ts.into(),
163 Err(e) => e.to_compile_error().into(),
164 }
165}
166
167#[derive(Debug, Default, PartialEq)]
169struct ToolDocs {
170 description: String,
172 arguments: Vec<(String, String)>,
174}
175
176fn doc_lines(attrs: &[syn::Attribute]) -> Vec<String> {
178 attrs
179 .iter()
180 .filter(|a| a.path().is_ident("doc"))
181 .filter_map(|a| match &a.meta {
182 Meta::NameValue(nv) => match &nv.value {
183 Expr::Lit(ExprLit {
184 lit: Lit::Str(s), ..
185 }) => Some(s.value()),
186 _ => None,
187 },
188 _ => None,
189 })
190 .flat_map(|text| text.lines().map(str::to_owned).collect::<Vec<_>>())
191 .collect()
192}
193
194fn parse_docs(lines: &[String]) -> ToolDocs {
196 let mut docs = ToolDocs::default();
197 let mut paragraphs: Vec<String> = Vec::new();
198 let mut paragraph = String::new();
199 let mut section: Option<bool> = None;
201
202 let flush = |paragraph: &mut String, paragraphs: &mut Vec<String>| {
203 if !paragraph.is_empty() {
204 paragraphs.push(std::mem::take(paragraph));
205 }
206 };
207
208 let mut in_code = false;
209 for raw in lines {
210 let line = raw.trim();
211 if line.starts_with("```") || line.starts_with("~~~") {
213 in_code = !in_code;
214 continue;
215 }
216 if in_code {
217 continue;
218 }
219 if let Some(heading) = line.strip_prefix('#') {
220 let heading = heading.trim_start_matches('#').trim().to_ascii_lowercase();
221 section = Some(matches!(
222 heading.as_str(),
223 "arguments" | "args" | "parameters" | "params"
224 ));
225 continue;
226 }
227 match section {
228 None if line.is_empty() => flush(&mut paragraph, &mut paragraphs),
229 None => {
230 if !paragraph.is_empty() {
231 paragraph.push(' ');
232 }
233 paragraph.push_str(line);
234 }
235 Some(true) => {
236 if let Some(item) = line.strip_prefix(['*', '-']) {
237 if let Some(parsed) = parse_argument_item(item) {
238 docs.arguments.push(parsed);
239 }
240 } else if !line.is_empty()
241 && let Some((_, text)) = docs.arguments.last_mut()
242 {
243 if !text.is_empty() {
244 text.push(' ');
245 }
246 text.push_str(line);
247 }
248 }
249 Some(false) => {}
250 }
251 }
252 flush(&mut paragraph, &mut paragraphs);
253 docs.description = paragraphs.join("\n\n");
254 docs
255}
256
257fn parse_argument_item(item: &str) -> Option<(String, String)> {
259 let item = item.trim();
260 let (name, rest) = if let Some(quoted) = item.strip_prefix('`') {
261 let end = quoted.find('`')?;
262 ("ed[..end], "ed[end + 1..])
263 } else {
264 let end = item
265 .find(|c: char| !(c.is_alphanumeric() || c == '_'))
266 .unwrap_or(item.len());
267 (&item[..end], &item[end..])
268 };
269 if name.is_empty() {
270 return None;
271 }
272 let text = rest
273 .trim_start()
274 .trim_start_matches(['-', ':', '\u{2013}', '\u{2014}'])
275 .trim();
276 Some((name.to_owned(), text.to_owned()))
277}
278
279fn is_tool_context(ty: &Type) -> bool {
283 match ty {
284 Type::Path(path) => path
285 .path
286 .segments
287 .last()
288 .is_some_and(|seg| seg.ident == "ToolContext" && seg.arguments.is_empty()),
289 _ => false,
290 }
291}
292
293fn is_result(ty: &Type) -> bool {
294 let Type::Path(TypePath { qself: None, path }) = ty else {
295 return false;
296 };
297 path.segments
298 .last()
299 .is_some_and(|seg| seg.ident == "Result" && !seg.arguments.is_none())
300}
301
302fn expand(description: Option<LitStr>, func: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
303 let sig = &func.sig;
304
305 if sig.asyncness.is_none() {
306 return Err(syn::Error::new_spanned(
307 sig.fn_token,
308 "#[tool] requires an `async fn`",
309 ));
310 }
311 if let Some(variadic) = &sig.variadic {
312 return Err(syn::Error::new_spanned(
313 variadic,
314 "#[tool] does not support variadic functions",
315 ));
316 }
317 if !sig.generics.params.is_empty() {
318 return Err(syn::Error::new_spanned(
319 sig.generics.clone(),
320 "#[tool] does not support generic functions",
321 ));
322 }
323
324 let fn_name = &sig.ident;
325 let vis = &func.vis;
326 let body = &func.block;
327
328 let mut field_idents = Vec::new();
331 let mut field_types = Vec::new();
332 let mut param_idents = Vec::new();
335 let mut param_types = Vec::new();
336 let mut context_ident: Option<syn::Ident> = None;
337 for input in &sig.inputs {
338 match input {
339 FnArg::Receiver(r) => {
340 return Err(syn::Error::new_spanned(
341 r,
342 "#[tool] cannot be applied to methods taking `self`",
343 ));
344 }
345 FnArg::Typed(PatType { pat, ty, .. }) => {
346 let ident = match pat.as_ref() {
347 Pat::Ident(pat_ident) => pat_ident.ident.clone(),
348 other => {
349 return Err(syn::Error::new_spanned(
350 other,
351 "#[tool] parameters must be simple identifiers (no patterns)",
352 ));
353 }
354 };
355 param_idents.push(ident.clone());
356 param_types.push((*ty).clone());
357 if is_tool_context(ty) {
358 if context_ident.is_some() {
359 return Err(syn::Error::new_spanned(
360 ty,
361 "#[tool] takes at most one `ToolContext` parameter",
362 ));
363 }
364 context_ident = Some(ident);
365 continue;
366 }
367 if let Type::Reference(reference) = ty.as_ref() {
368 return Err(syn::Error::new_spanned(
369 reference,
370 "#[tool] parameters are deserialized from the model's JSON, so they \
371 must be owned: use `String` for `&str`, `Vec<T>` for `&[T]`",
372 ));
373 }
374 field_idents.push(ident);
375 field_types.push((*ty).clone());
376 }
377 }
378 }
379
380 let docs = parse_docs(&doc_lines(&func.attrs));
382 for (name, _) in &docs.arguments {
383 if !field_idents.iter().any(|ident| ident == name) {
384 return Err(syn::Error::new_spanned(
385 fn_name,
386 format!(
387 "the `# Arguments` section documents `{name}`, which is not a parameter \
388 of `{fn_name}`"
389 ),
390 ));
391 }
392 }
393 let description = match description {
394 Some(text) => text.value(),
395 None if !docs.description.is_empty() => docs.description.clone(),
396 None => {
397 return Err(syn::Error::new_spanned(
398 fn_name,
399 "#[tool] needs a description for the model: add a `///` doc comment to the \
400 function, or pass one as `#[tool(\"...\")]`",
401 ));
402 }
403 };
404
405 let (return_type, adapt) = match &sig.output {
407 ReturnType::Default => (quote! { () }, quote! { tool_output }),
408 ReturnType::Type(_, ty) if is_result(ty) => (quote! { #ty }, quote! { tool_result }),
409 ReturnType::Type(_, ty) => (quote! { #ty }, quote! { tool_output }),
410 };
411
412 let mut cfg_attrs = Vec::new();
415 let mut constructor_attrs = Vec::new();
416 let mut body_attrs = Vec::new();
417 for attr in &func.attrs {
418 let path = attr.path();
419 if path.is_ident("cfg") {
420 cfg_attrs.push(attr);
421 } else if path.is_ident("doc") || path.is_ident("deprecated") {
422 constructor_attrs.push(attr);
423 } else {
424 body_attrs.push(attr);
425 }
426 }
427
428 let pascal = to_pascal_case(&fn_name.to_string());
430 let args_struct = format_ident!("__{}Args", pascal);
431 let tool_struct = format_ident!("__{}Tool", pascal);
432 let inner_fn = format_ident!("__{}_impl", fn_name);
434
435 let fn_name_str = fn_name.to_string();
436
437 let struct_fields = field_idents
440 .iter()
441 .zip(field_types.iter())
442 .map(|(ident, ty)| {
443 let doc = docs
444 .arguments
445 .iter()
446 .find(|(name, _)| ident == name)
447 .map(|(_, text)| text.as_str())
448 .filter(|text| !text.is_empty())
449 .map(|text| quote! { #[doc = #text] });
450 let default = is_option(ty).then(|| quote! { #[serde(default)] });
453 quote! {
454 #doc
455 #default
456 #ident: #ty
457 }
458 });
459
460 let (rt, rt_str) = runtime();
463 let serde = quote! { #rt::__macros::serde };
464 let schemars = quote! { #rt::__macros::schemars };
465 let async_trait = quote! { #rt::__macros::async_trait };
466 let serde_json = quote! { #rt::__macros::serde_json };
467 let serde_crate = LitStr::new(&format!("{rt_str}::__macros::serde"), Span::call_site());
468 let schemars_crate = LitStr::new(&format!("{rt_str}::__macros::schemars"), Span::call_site());
469
470 let bind_context = match &context_ident {
472 Some(ident) => quote! { let #ident = ctx; },
473 None => quote! { let _ = ctx; },
474 };
475
476 let expanded = quote! {
477 #(#cfg_attrs)*
479 #[derive(#serde::Deserialize, #schemars::JsonSchema)]
480 #[serde(crate = #serde_crate)]
481 #[schemars(crate = #schemars_crate)]
482 #[allow(non_camel_case_types, non_snake_case)]
483 struct #args_struct {
484 #(#struct_fields),*
485 }
486
487 #(#cfg_attrs)*
489 #(#body_attrs)*
490 #[allow(non_snake_case)]
491 async fn #inner_fn ( #(#param_idents : #param_types),* ) -> #return_type #body
492
493 #(#cfg_attrs)*
495 #[allow(non_camel_case_types)]
496 #[derive(Clone, Copy, Debug, Default)]
497 #vis struct #tool_struct;
498
499 #(#cfg_attrs)*
500 #[#async_trait::async_trait]
501 impl #rt::tool::ToolFunction for #tool_struct {
502 fn name(&self) -> &str {
503 #fn_name_str
504 }
505
506 fn description(&self) -> &str {
507 #description
508 }
509
510 fn parameters(&self) -> ::core::option::Option<#serde_json::Value> {
511 let mut schema = #rt::tool::wire_schema::<#args_struct>();
514 if let ::core::option::Option::Some(object) = schema.as_object_mut() {
515 object.remove("title");
516 }
517 ::core::option::Option::Some(schema)
518 }
519
520 async fn call(
521 &self,
522 args: #serde_json::Value,
523 ) -> ::core::result::Result<#serde_json::Value, #rt::error::ToolError> {
524 self.call_with_context(args, #rt::tool::ToolContext::detached()).await
525 }
526
527 async fn call_with_context(
528 &self,
529 args: #serde_json::Value,
530 ctx: #rt::tool::ToolContext,
531 ) -> ::core::result::Result<#serde_json::Value, #rt::error::ToolError> {
532 let #args_struct { #(#field_idents),* } =
533 #serde_json::from_value(args).map_err(|e| {
534 #rt::error::ToolError::InvalidArgs(
535 ::std::format!("Failed to deserialize arguments: {e}"),
536 )
537 })?;
538 #bind_context
539 #rt::__macros::#adapt(#inner_fn ( #(#param_idents),* ).await)
540 }
541 }
542
543 #(#cfg_attrs)*
545 #(#constructor_attrs)*
546 #[allow(non_snake_case)]
547 #vis fn #fn_name () -> #tool_struct {
548 #tool_struct
549 }
550 };
551
552 Ok(expanded)
553}
554
555#[proc_macro_derive(Extract, attributes(recognize, extract))]
605pub fn derive_extract(item: TokenStream) -> TokenStream {
606 let input = parse_macro_input!(item as DeriveInput);
607 match expand_extract(input) {
608 Ok(ts) => ts.into(),
609 Err(e) => e.to_compile_error().into(),
610 }
611}
612
613fn expand_extract(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
614 let (rt, _) = runtime();
615 let ident = &input.ident;
616
617 let fields = match &input.data {
618 Data::Struct(s) => match &s.fields {
619 Fields::Named(named) => &named.named,
620 _ => {
621 return Err(syn::Error::new_spanned(
622 ident,
623 "#[derive(Extract)] requires a struct with named fields",
624 ));
625 }
626 },
627 _ => {
628 return Err(syn::Error::new_spanned(
629 ident,
630 "#[derive(Extract)] can only be applied to structs",
631 ));
632 }
633 };
634
635 let mut name = to_snake_case(&ident.to_string());
637 let mut window: usize = 3;
638 for attr in &input.attrs {
639 if attr.path().is_ident("extract") {
640 attr.parse_nested_meta(|meta| {
641 if meta.path.is_ident("name") {
642 let v: LitStr = meta.value()?.parse()?;
643 name = v.value();
644 } else if meta.path.is_ident("window") {
645 let v: LitInt = meta.value()?.parse()?;
646 window = v.base10_parse()?;
647 } else {
648 return Err(
649 meta.error("unknown `extract` option (expected `name` or `window`)")
650 );
651 }
652 Ok(())
653 })?;
654 }
655 }
656
657 let all_field_idents: Vec<_> = fields.iter().filter_map(|f| f.ident.clone()).collect();
660
661 let mut field_calls = Vec::new();
663 for field in fields {
664 let Some(recognize) = field.attrs.iter().find(|a| a.path().is_ident("recognize")) else {
665 continue;
666 };
667 let fname = field.ident.as_ref().expect("named field").to_string();
668 let recognizer = recognizer_expr(recognize)?;
669
670 let mut state_key: Option<String> = None;
672 for attr in &field.attrs {
673 if attr.path().is_ident("extract") {
674 attr.parse_nested_meta(|meta| {
675 if meta.path.is_ident("state") {
676 let v: LitStr = meta.value()?.parse()?;
677 state_key = Some(v.value());
678 } else {
679 return Err(meta.error("unknown field `extract` option (expected `state`)"));
680 }
681 Ok(())
682 })?;
683 }
684 }
685
686 field_calls.push(match state_key {
687 Some(sk) => quote! { .field_to(#fname, #sk, #recognizer) },
688 None => quote! { .field(#fname, #recognizer) },
689 });
690 }
691
692 let doc = format!("The `Extract` record derived from `{ident}`'s `#[recognize(..)]` fields.");
693 Ok(quote! {
694 impl #ident {
695 #[doc = #doc]
696 pub fn extract() -> #rt::extract::Extract {
697 #rt::extract::Extract::record(#name)
698 #(#field_calls)*
699 .window(#window)
700 .build()
701 }
702
703 #[allow(dead_code)]
704 #[doc(hidden)]
705 fn __extract_mark_fields_used(&self) {
706 #( let _ = &self.#all_field_idents; )*
707 }
708 }
709 })
710}
711
712#[proc_macro_derive(Frame, attributes(slot, frame, recognize))]
734pub fn derive_frame(item: TokenStream) -> TokenStream {
735 let input = parse_macro_input!(item as DeriveInput);
736 match expand_frame(input) {
737 Ok(ts) => ts.into(),
738 Err(e) => e.to_compile_error().into(),
739 }
740}
741
742fn expand_frame(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
743 let (rt, _) = runtime();
744 let ident = &input.ident;
745
746 let fields = match &input.data {
747 Data::Struct(s) => match &s.fields {
748 Fields::Named(named) => &named.named,
749 _ => {
750 return Err(syn::Error::new_spanned(
751 ident,
752 "#[derive(Frame)] requires a struct with named fields",
753 ));
754 }
755 },
756 _ => {
757 return Err(syn::Error::new_spanned(
758 ident,
759 "#[derive(Frame)] can only be applied to structs",
760 ));
761 }
762 };
763
764 let mut name = to_snake_case(&ident.to_string());
766 for attr in &input.attrs {
767 if attr.path().is_ident("frame") {
768 attr.parse_nested_meta(|meta| {
769 if meta.path.is_ident("name") {
770 let v: LitStr = meta.value()?.parse()?;
771 name = v.value();
772 Ok(())
773 } else {
774 Err(meta.error("unknown `frame` option (expected `name`)"))
775 }
776 })?;
777 }
778 }
779
780 let all_field_idents: Vec<_> = fields.iter().filter_map(|f| f.ident.clone()).collect();
781
782 let mut slot_exprs = Vec::new();
783 for field in fields {
784 let fname = field.ident.as_ref().expect("named field").to_string();
785 let mut state_key = fname.clone();
786 let mut prompt: Option<String> = None;
787 let mut reprompt: Option<String> = None;
788 let mut confirm = quote! { #rt::frame::ConfirmPolicy::Never };
789 let mut pii = false;
790 let mut min: Option<f64> = None;
791 let mut max: Option<f64> = None;
792 let mut non_empty = false;
793
794 let recognizer = match field.attrs.iter().find(|a| a.path().is_ident("recognize")) {
796 Some(attr) => {
797 let r = slot_recognizer_expr(attr)?;
798 quote! { Some(#r) }
799 }
800 None => quote! { None },
801 };
802
803 for attr in &field.attrs {
804 if !attr.path().is_ident("slot") {
805 continue;
806 }
807 attr.parse_nested_meta(|meta| {
808 if meta.path.is_ident("prompt") {
809 let v: LitStr = meta.value()?.parse()?;
810 prompt = Some(v.value());
811 } else if meta.path.is_ident("reprompt") {
812 let v: LitStr = meta.value()?.parse()?;
813 reprompt = Some(v.value());
814 } else if meta.path.is_ident("state") {
815 let v: LitStr = meta.value()?.parse()?;
816 state_key = v.value();
817 } else if meta.path.is_ident("confirm") {
818 let v: LitStr = meta.value()?.parse()?;
819 confirm = match v.value().as_str() {
820 "never" => quote! { #rt::frame::ConfirmPolicy::Never },
821 "low_confidence" => {
822 quote! { #rt::frame::ConfirmPolicy::LowConfidence }
823 }
824 "always" => quote! { #rt::frame::ConfirmPolicy::Always },
825 other => {
826 return Err(meta.error(format!(
827 "unknown confirm policy '{other}' (expected never/low_confidence/always)"
828 )))
829 }
830 };
831 } else if meta.path.is_ident("pii") {
832 pii = true;
833 } else if meta.path.is_ident("min") {
834 min = Some(lit_to_f64(&meta.value()?.parse()?)?);
835 } else if meta.path.is_ident("max") {
836 max = Some(lit_to_f64(&meta.value()?.parse()?)?);
837 } else if meta.path.is_ident("non_empty") {
838 non_empty = true;
839 } else {
840 return Err(meta.error(
841 "unknown `slot` option (expected prompt/reprompt/state/confirm/pii/min/max/non_empty)",
842 ));
843 }
844 Ok(())
845 })?;
846 }
847
848 let validate = if min.is_some() || max.is_some() {
850 let min_tok = match min {
851 Some(v) => quote! { Some(#v) },
852 None => quote! { None },
853 };
854 let max_tok = match max {
855 Some(v) => quote! { Some(#v) },
856 None => quote! { None },
857 };
858 quote! { Some(#rt::frame::SlotValidator::Range { min: #min_tok, max: #max_tok }) }
859 } else if non_empty {
860 quote! { Some(#rt::frame::SlotValidator::NonEmpty) }
861 } else {
862 quote! { None }
863 };
864
865 let prompt_tok = match prompt {
866 Some(p) => quote! { Some(#p.to_string()) },
867 None => quote! { None },
868 };
869 let reprompt_tok = match reprompt {
870 Some(p) => quote! { Some(#p.to_string()) },
871 None => quote! { None },
872 };
873 slot_exprs.push(quote! {
874 #rt::frame::SlotSpec {
875 name: #fname.to_string(),
876 state_key: #state_key.to_string(),
877 prompt: #prompt_tok,
878 reprompt: #reprompt_tok,
879 confirm: #confirm,
880 pii: #pii,
881 recognizer: #recognizer,
882 validate: #validate,
883 }
884 });
885 }
886
887 let doc = format!("The `FrameSpec` derived from `{ident}`'s `#[slot(..)]` fields.");
888 Ok(quote! {
889 impl #rt::frame::Frame for #ident {
890 #[doc = #doc]
891 fn frame() -> #rt::frame::FrameSpec {
892 #rt::frame::FrameSpec {
893 name: #name.to_string(),
894 slots: ::std::vec![ #(#slot_exprs),* ],
895 }
896 }
897 }
898
899 impl #ident {
900 #[allow(dead_code)]
901 #[doc(hidden)]
902 fn __frame_mark_fields_used(&self) {
903 #( let _ = &self.#all_field_idents; )*
904 }
905 }
906 })
907}
908
909fn recognizer_expr(attr: &syn::Attribute) -> syn::Result<proc_macro2::TokenStream> {
911 let (rt, _) = runtime();
912 let r = quote! { #rt::extract::Recognizer };
913 let meta: Meta = attr.parse_args()?;
914 match meta {
915 Meta::Path(p) => {
916 let id = p
917 .get_ident()
918 .ok_or_else(|| syn::Error::new_spanned(&p, "expected a recognizer name"))?;
919 match id.to_string().as_str() {
920 "integer" => Ok(quote! { #r::integer() }),
921 "money" => Ok(quote! { #r::money() }),
922 "yes_no" => Ok(quote! { #r::yes_no() }),
923 "datetime" => Ok(quote! { #r::datetime() }),
924 other => Err(syn::Error::new_spanned(
925 &p,
926 format!("unknown recognizer `{other}`"),
927 )),
928 }
929 }
930 Meta::NameValue(nv) => {
931 let id = nv
932 .path
933 .get_ident()
934 .ok_or_else(|| syn::Error::new_spanned(&nv.path, "expected a recognizer name"))?;
935 match id.to_string().as_str() {
936 "integer_near" => {
937 let a = str_array(&nv.value)?;
938 Ok(quote! { #r::integer_near([ #(#a),* ]) })
939 }
940 "one_of" => {
941 let a = str_array(&nv.value)?;
942 Ok(quote! { #r::one_of([ #(#a),* ]) })
943 }
944 "fuzzy" => {
945 let a = str_array(&nv.value)?;
946 Ok(quote! { #r::fuzzy([ #(#a),* ]) })
947 }
948 "regex" => {
949 let s = str_lit(&nv.value)?;
950 Ok(quote! { #r::regex(#s) })
951 }
952 other => Err(syn::Error::new_spanned(
953 &nv.path,
954 format!("`{other}` does not take a value"),
955 )),
956 }
957 }
958 Meta::List(l) => Err(syn::Error::new_spanned(
959 l,
960 "unexpected nested list in `#[recognize(..)]`",
961 )),
962 }
963}
964
965fn slot_recognizer_expr(attr: &syn::Attribute) -> syn::Result<proc_macro2::TokenStream> {
968 let (rt, _) = runtime();
969 let r = quote! { #rt::frame::SlotRecognizer };
970 let meta: Meta = attr.parse_args()?;
971 match meta {
972 Meta::Path(p) => {
973 let id = p
974 .get_ident()
975 .ok_or_else(|| syn::Error::new_spanned(&p, "expected a recognizer name"))?;
976 match id.to_string().as_str() {
977 "integer" => Ok(quote! { #r::Integer }),
978 "money" => Ok(quote! { #r::Money }),
979 "yes_no" => Ok(quote! { #r::YesNo }),
980 "datetime" => Ok(quote! { #r::DateTime }),
981 other => Err(syn::Error::new_spanned(
982 &p,
983 format!("unknown recognizer `{other}`"),
984 )),
985 }
986 }
987 Meta::NameValue(nv) => {
988 let id = nv
989 .path
990 .get_ident()
991 .ok_or_else(|| syn::Error::new_spanned(&nv.path, "expected a recognizer name"))?;
992 match id.to_string().as_str() {
993 "integer_near" => {
994 let a = str_array(&nv.value)?;
995 Ok(quote! { #r::IntegerNear(::std::vec![ #(#a.to_string()),* ]) })
996 }
997 "one_of" => {
998 let a = str_array(&nv.value)?;
999 Ok(quote! { #r::OneOf(::std::vec![ #(#a.to_string()),* ]) })
1000 }
1001 "fuzzy" => {
1002 let a = str_array(&nv.value)?;
1003 Ok(quote! { #r::Fuzzy(::std::vec![ #(#a.to_string()),* ]) })
1004 }
1005 "regex" => {
1006 let s = str_lit(&nv.value)?;
1007 Ok(quote! { #r::Regex(#s.to_string()) })
1008 }
1009 other => Err(syn::Error::new_spanned(
1010 &nv.path,
1011 format!("`{other}` does not take a value"),
1012 )),
1013 }
1014 }
1015 Meta::List(l) => Err(syn::Error::new_spanned(
1016 l,
1017 "unexpected nested list in `#[recognize(..)]`",
1018 )),
1019 }
1020}
1021
1022fn lit_to_f64(lit: &Lit) -> syn::Result<f64> {
1024 match lit {
1025 Lit::Int(i) => i.base10_parse::<f64>(),
1026 Lit::Float(f) => f.base10_parse::<f64>(),
1027 other => Err(syn::Error::new_spanned(
1028 other,
1029 "expected a numeric literal for `min`/`max`",
1030 )),
1031 }
1032}
1033
1034fn str_array(expr: &Expr) -> syn::Result<Vec<LitStr>> {
1036 match expr {
1037 Expr::Array(arr) => arr
1038 .elems
1039 .iter()
1040 .map(|e| match e {
1041 Expr::Lit(ExprLit {
1042 lit: Lit::Str(s), ..
1043 }) => Ok(s.clone()),
1044 other => Err(syn::Error::new_spanned(
1045 other,
1046 "expected a string literal in the array",
1047 )),
1048 })
1049 .collect(),
1050 other => Err(syn::Error::new_spanned(
1051 other,
1052 "expected an array of string literals, e.g. [\"a\", \"b\"]",
1053 )),
1054 }
1055}
1056
1057fn str_lit(expr: &Expr) -> syn::Result<LitStr> {
1059 match expr {
1060 Expr::Lit(ExprLit {
1061 lit: Lit::Str(s), ..
1062 }) => Ok(s.clone()),
1063 other => Err(syn::Error::new_spanned(other, "expected a string literal")),
1064 }
1065}
1066
1067fn to_snake_case(s: &str) -> String {
1069 let mut out = String::with_capacity(s.len() + 4);
1070 for (i, ch) in s.chars().enumerate() {
1071 if ch.is_uppercase() {
1072 if i != 0 {
1073 out.push('_');
1074 }
1075 out.extend(ch.to_lowercase());
1076 } else {
1077 out.push(ch);
1078 }
1079 }
1080 out
1081}
1082
1083fn is_option(ty: &Type) -> bool {
1091 let Type::Path(TypePath { qself: None, path }) = ty else {
1092 return false;
1093 };
1094 if path
1096 .segments
1097 .iter()
1098 .rev()
1099 .skip(1)
1100 .any(|seg| !seg.arguments.is_none())
1101 {
1102 return false;
1103 }
1104 let idents: Vec<&syn::Ident> = path.segments.iter().map(|seg| &seg.ident).collect();
1105 match idents.as_slice() {
1106 [opt] => path.leading_colon.is_none() && *opt == "Option",
1109 [module, opt] => path.leading_colon.is_none() && *module == "option" && *opt == "Option",
1110 [root, module, opt] => {
1112 (*root == "std" || *root == "core") && *module == "option" && *opt == "Option"
1113 }
1114 _ => false,
1115 }
1116}
1117
1118fn to_pascal_case(s: &str) -> String {
1120 let mut out = String::with_capacity(s.len());
1121 let mut upper_next = true;
1122 for ch in s.chars() {
1123 if ch == '_' {
1124 upper_next = true;
1125 } else if upper_next {
1126 out.extend(ch.to_uppercase());
1127 upper_next = false;
1128 } else {
1129 out.push(ch);
1130 }
1131 }
1132 out
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::{ToolDocs, is_option, is_result, parse_docs};
1138 use syn::parse_quote;
1139
1140 fn docs(text: &str) -> ToolDocs {
1141 let lines: Vec<String> = text.lines().map(|l| format!(" {l}")).collect();
1142 parse_docs(&lines)
1143 }
1144
1145 #[test]
1146 fn description_is_the_prose_before_the_first_heading() {
1147 let parsed =
1148 docs("Get the weather\nfor a city.\n\nUses the cached forecast.\n\n# Errors\n\nNever.");
1149 assert_eq!(
1150 parsed.description,
1151 "Get the weather for a city.\n\nUses the cached forecast."
1152 );
1153 assert!(parsed.arguments.is_empty());
1154 }
1155
1156 #[test]
1157 fn arguments_accept_the_common_rustdoc_forms() {
1158 let parsed = docs(
1159 "Look up.\n\n# Arguments\n\n\
1160 * `city` - The city,\n e.g. Paris.\n\
1161 - `units`: metric or imperial.\n\
1162 * days \u{2014} how far ahead.\n\n# Examples\n\n* `ignored` - not an argument",
1163 );
1164 assert_eq!(parsed.description, "Look up.");
1165 assert_eq!(
1166 parsed.arguments,
1167 vec![
1168 ("city".to_string(), "The city, e.g. Paris.".to_string()),
1169 ("units".to_string(), "metric or imperial.".to_string()),
1170 ("days".to_string(), "how far ahead.".to_string()),
1171 ]
1172 );
1173 }
1174
1175 #[test]
1176 fn code_blocks_are_neither_description_nor_headings() {
1177 let parsed = docs("Add two numbers.\n```\n# let x = 1;\nadd(1, 2);\n```\nThen return.");
1178 assert_eq!(parsed.description, "Add two numbers. Then return.");
1179 }
1180
1181 #[test]
1182 fn is_result_matches_any_path_ending_in_result() {
1183 assert!(is_result(&parse_quote!(Result<Value, ToolError>)));
1184 assert!(is_result(&parse_quote!(anyhow::Result<u32>)));
1185 assert!(is_result(&parse_quote!(std::io::Result<()>)));
1186 assert!(!is_result(&parse_quote!(SearchResult)));
1187 assert!(!is_result(&parse_quote!(Vec<Result<u8, String>>)));
1188 }
1189
1190 #[test]
1191 fn is_option_accepts_std_core_paths() {
1192 assert!(is_option(&parse_quote!(Option<String>)));
1193 assert!(is_option(&parse_quote!(option::Option<String>)));
1194 assert!(is_option(&parse_quote!(std::option::Option<String>)));
1195 assert!(is_option(&parse_quote!(core::option::Option<String>)));
1196 assert!(is_option(&parse_quote!(::std::option::Option<String>)));
1197 assert!(is_option(&parse_quote!(::core::option::Option<String>)));
1198 }
1199
1200 #[test]
1201 fn is_option_rejects_lookalikes() {
1202 assert!(!is_option(&parse_quote!(String)));
1203 assert!(!is_option(&parse_quote!(Vec<Option<String>>)));
1204 assert!(!is_option(&parse_quote!(my::Option<String>)));
1205 assert!(!is_option(&parse_quote!(my::option::Option<String>)));
1206 assert!(!is_option(&parse_quote!(::option::Option<String>)));
1207 assert!(!is_option(&parse_quote!(<T as Trait>::Option)));
1208 }
1209}