1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![forbid(unsafe_code)]
3use proc_macro::TokenStream;
28use proc_macro2::Span;
29use quote::{format_ident, quote};
30use syn::{
31 Data, DeriveInput, Expr, ExprLit, Fields, FnArg, ItemFn, Lit, LitInt, LitStr, Meta, Pat,
32 PatType, ReturnType, Type, TypePath, parse_macro_input,
33};
34
35fn runtime() -> (proc_macro2::TokenStream, String) {
45 use proc_macro_crate::{FoundCrate, crate_name};
46 match crate_name("gemini-adk-rs") {
47 Ok(FoundCrate::Name(name)) => {
48 let ident = format_ident!("{name}");
49 (quote! { ::#ident }, name)
50 }
51 Ok(FoundCrate::Itself) => (quote! { ::gemini_adk_rs }, "gemini_adk_rs".to_string()),
52 Err(_) => match crate_name("gemini-adk-fluent-rs") {
53 Ok(FoundCrate::Name(name)) => {
54 let ident = format_ident!("{name}");
55 (
56 quote! { ::#ident::gemini_adk_rs },
57 format!("{name}::gemini_adk_rs"),
58 )
59 }
60 _ => (quote! { ::gemini_adk_rs }, "gemini_adk_rs".to_string()),
61 },
62 }
63}
64
65#[proc_macro_attribute]
121pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
122 let description = parse_macro_input!(attr as LitStr);
123 let func = parse_macro_input!(item as ItemFn);
124
125 match expand(description, func) {
126 Ok(ts) => ts.into(),
127 Err(e) => e.to_compile_error().into(),
128 }
129}
130
131fn expand(description: LitStr, func: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
132 let sig = &func.sig;
133
134 if sig.asyncness.is_none() {
135 return Err(syn::Error::new_spanned(
136 sig.fn_token,
137 "#[tool] requires an `async fn`",
138 ));
139 }
140 if let Some(variadic) = &sig.variadic {
141 return Err(syn::Error::new_spanned(
142 variadic,
143 "#[tool] does not support variadic functions",
144 ));
145 }
146 if !sig.generics.params.is_empty() {
147 return Err(syn::Error::new_spanned(
148 sig.generics.clone(),
149 "#[tool] does not support generic functions",
150 ));
151 }
152
153 let fn_name = &sig.ident;
154 let vis = &func.vis;
155 let body = &func.block;
156 let output = &sig.output;
157
158 let mut field_idents = Vec::new();
160 let mut field_types = Vec::new();
161 for input in &sig.inputs {
162 match input {
163 FnArg::Receiver(r) => {
164 return Err(syn::Error::new_spanned(
165 r,
166 "#[tool] cannot be applied to methods taking `self`",
167 ));
168 }
169 FnArg::Typed(PatType { pat, ty, .. }) => {
170 let ident = match pat.as_ref() {
171 Pat::Ident(pat_ident) => pat_ident.ident.clone(),
172 other => {
173 return Err(syn::Error::new_spanned(
174 other,
175 "#[tool] parameters must be simple identifiers (no patterns)",
176 ));
177 }
178 };
179 field_idents.push(ident);
180 field_types.push((*ty).clone());
181 }
182 }
183 }
184
185 let return_type: proc_macro2::TokenStream = match output {
188 ReturnType::Default => {
189 return Err(syn::Error::new_spanned(
190 sig,
191 "#[tool] requires a return type of `Result<serde_json::Value, ToolError>`",
192 ));
193 }
194 ReturnType::Type(_, ty) => quote! { #ty },
195 };
196
197 let pascal = to_pascal_case(&fn_name.to_string());
199 let args_struct = format_ident!("__{}Args", pascal);
200 let tool_struct = format_ident!("__{}Tool", pascal);
201 let inner_fn = format_ident!("__{}_impl", fn_name);
203
204 let fn_name_str = fn_name.to_string();
205
206 let struct_fields = field_idents
208 .iter()
209 .zip(field_types.iter())
210 .map(|(ident, ty)| {
211 if is_option(ty) {
214 quote! {
215 #[serde(default)]
216 #ident: #ty
217 }
218 } else {
219 quote! { #ident: #ty }
220 }
221 });
222
223 let destructure = &field_idents;
226 let forward_args = &field_idents;
227
228 let (rt, rt_str) = runtime();
231 let serde = quote! { #rt::__macros::serde };
232 let schemars = quote! { #rt::__macros::schemars };
233 let async_trait = quote! { #rt::__macros::async_trait };
234 let serde_json = quote! { #rt::__macros::serde_json };
235 let serde_crate = LitStr::new(&format!("{rt_str}::__macros::serde"), Span::call_site());
236 let schemars_crate = LitStr::new(&format!("{rt_str}::__macros::schemars"), Span::call_site());
237
238 let expanded = quote! {
239 #[derive(#serde::Deserialize, #schemars::JsonSchema)]
241 #[serde(crate = #serde_crate)]
242 #[schemars(crate = #schemars_crate)]
243 #[allow(non_camel_case_types, non_snake_case)]
244 struct #args_struct {
245 #(#struct_fields),*
246 }
247
248 #[allow(non_snake_case)]
250 async fn #inner_fn ( #(#field_idents : #field_types),* ) -> #return_type #body
251
252 #[allow(non_camel_case_types)]
254 #vis struct #tool_struct;
255
256 #[#async_trait::async_trait]
257 impl #rt::tool::ToolFunction for #tool_struct {
258 fn name(&self) -> &str {
259 #fn_name_str
260 }
261
262 fn description(&self) -> &str {
263 #description
264 }
265
266 fn parameters(&self) -> ::core::option::Option<#serde_json::Value> {
267 let root = #schemars::schema_for!(#args_struct);
268 ::core::option::Option::Some(
269 #serde_json::to_value(root)
270 .expect("schemars schema should serialize to JSON"),
271 )
272 }
273
274 async fn call(
275 &self,
276 args: #serde_json::Value,
277 ) -> ::core::result::Result<#serde_json::Value, #rt::error::ToolError> {
278 let #args_struct { #(#destructure),* } =
279 #serde_json::from_value(args).map_err(|e| {
280 #rt::error::ToolError::InvalidArgs(
281 ::std::format!("Failed to deserialize arguments: {e}"),
282 )
283 })?;
284 #inner_fn ( #(#forward_args),* ).await
285 }
286 }
287
288 #[allow(non_snake_case)]
290 #vis fn #fn_name () -> #tool_struct {
291 #tool_struct
292 }
293 };
294
295 Ok(expanded)
296}
297
298#[proc_macro_derive(Extract, attributes(recognize, extract))]
348pub fn derive_extract(item: TokenStream) -> TokenStream {
349 let input = parse_macro_input!(item as DeriveInput);
350 match expand_extract(input) {
351 Ok(ts) => ts.into(),
352 Err(e) => e.to_compile_error().into(),
353 }
354}
355
356fn expand_extract(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
357 let (rt, _) = runtime();
358 let ident = &input.ident;
359
360 let fields = match &input.data {
361 Data::Struct(s) => match &s.fields {
362 Fields::Named(named) => &named.named,
363 _ => {
364 return Err(syn::Error::new_spanned(
365 ident,
366 "#[derive(Extract)] requires a struct with named fields",
367 ));
368 }
369 },
370 _ => {
371 return Err(syn::Error::new_spanned(
372 ident,
373 "#[derive(Extract)] can only be applied to structs",
374 ));
375 }
376 };
377
378 let mut name = to_snake_case(&ident.to_string());
380 let mut window: usize = 3;
381 for attr in &input.attrs {
382 if attr.path().is_ident("extract") {
383 attr.parse_nested_meta(|meta| {
384 if meta.path.is_ident("name") {
385 let v: LitStr = meta.value()?.parse()?;
386 name = v.value();
387 } else if meta.path.is_ident("window") {
388 let v: LitInt = meta.value()?.parse()?;
389 window = v.base10_parse()?;
390 } else {
391 return Err(
392 meta.error("unknown `extract` option (expected `name` or `window`)")
393 );
394 }
395 Ok(())
396 })?;
397 }
398 }
399
400 let all_field_idents: Vec<_> = fields.iter().filter_map(|f| f.ident.clone()).collect();
403
404 let mut field_calls = Vec::new();
406 for field in fields {
407 let Some(recognize) = field.attrs.iter().find(|a| a.path().is_ident("recognize")) else {
408 continue;
409 };
410 let fname = field.ident.as_ref().expect("named field").to_string();
411 let recognizer = recognizer_expr(recognize)?;
412
413 let mut state_key: Option<String> = None;
415 for attr in &field.attrs {
416 if attr.path().is_ident("extract") {
417 attr.parse_nested_meta(|meta| {
418 if meta.path.is_ident("state") {
419 let v: LitStr = meta.value()?.parse()?;
420 state_key = Some(v.value());
421 } else {
422 return Err(meta.error("unknown field `extract` option (expected `state`)"));
423 }
424 Ok(())
425 })?;
426 }
427 }
428
429 field_calls.push(match state_key {
430 Some(sk) => quote! { .field_to(#fname, #sk, #recognizer) },
431 None => quote! { .field(#fname, #recognizer) },
432 });
433 }
434
435 let doc = format!("The `Extract` record derived from `{ident}`'s `#[recognize(..)]` fields.");
436 Ok(quote! {
437 impl #ident {
438 #[doc = #doc]
439 pub fn extract() -> #rt::extract::Extract {
440 #rt::extract::Extract::record(#name)
441 #(#field_calls)*
442 .window(#window)
443 .build()
444 }
445
446 #[allow(dead_code)]
447 #[doc(hidden)]
448 fn __extract_mark_fields_used(&self) {
449 #( let _ = &self.#all_field_idents; )*
450 }
451 }
452 })
453}
454
455#[proc_macro_derive(Frame, attributes(slot, frame, recognize))]
477pub fn derive_frame(item: TokenStream) -> TokenStream {
478 let input = parse_macro_input!(item as DeriveInput);
479 match expand_frame(input) {
480 Ok(ts) => ts.into(),
481 Err(e) => e.to_compile_error().into(),
482 }
483}
484
485fn expand_frame(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
486 let (rt, _) = runtime();
487 let ident = &input.ident;
488
489 let fields = match &input.data {
490 Data::Struct(s) => match &s.fields {
491 Fields::Named(named) => &named.named,
492 _ => {
493 return Err(syn::Error::new_spanned(
494 ident,
495 "#[derive(Frame)] requires a struct with named fields",
496 ));
497 }
498 },
499 _ => {
500 return Err(syn::Error::new_spanned(
501 ident,
502 "#[derive(Frame)] can only be applied to structs",
503 ));
504 }
505 };
506
507 let mut name = to_snake_case(&ident.to_string());
509 for attr in &input.attrs {
510 if attr.path().is_ident("frame") {
511 attr.parse_nested_meta(|meta| {
512 if meta.path.is_ident("name") {
513 let v: LitStr = meta.value()?.parse()?;
514 name = v.value();
515 Ok(())
516 } else {
517 Err(meta.error("unknown `frame` option (expected `name`)"))
518 }
519 })?;
520 }
521 }
522
523 let all_field_idents: Vec<_> = fields.iter().filter_map(|f| f.ident.clone()).collect();
524
525 let mut slot_exprs = Vec::new();
526 for field in fields {
527 let fname = field.ident.as_ref().expect("named field").to_string();
528 let mut state_key = fname.clone();
529 let mut prompt: Option<String> = None;
530 let mut reprompt: Option<String> = None;
531 let mut confirm = quote! { #rt::frame::ConfirmPolicy::Never };
532 let mut pii = false;
533 let mut min: Option<f64> = None;
534 let mut max: Option<f64> = None;
535 let mut non_empty = false;
536
537 let recognizer = match field.attrs.iter().find(|a| a.path().is_ident("recognize")) {
539 Some(attr) => {
540 let r = slot_recognizer_expr(attr)?;
541 quote! { Some(#r) }
542 }
543 None => quote! { None },
544 };
545
546 for attr in &field.attrs {
547 if !attr.path().is_ident("slot") {
548 continue;
549 }
550 attr.parse_nested_meta(|meta| {
551 if meta.path.is_ident("prompt") {
552 let v: LitStr = meta.value()?.parse()?;
553 prompt = Some(v.value());
554 } else if meta.path.is_ident("reprompt") {
555 let v: LitStr = meta.value()?.parse()?;
556 reprompt = Some(v.value());
557 } else if meta.path.is_ident("state") {
558 let v: LitStr = meta.value()?.parse()?;
559 state_key = v.value();
560 } else if meta.path.is_ident("confirm") {
561 let v: LitStr = meta.value()?.parse()?;
562 confirm = match v.value().as_str() {
563 "never" => quote! { #rt::frame::ConfirmPolicy::Never },
564 "low_confidence" => {
565 quote! { #rt::frame::ConfirmPolicy::LowConfidence }
566 }
567 "always" => quote! { #rt::frame::ConfirmPolicy::Always },
568 other => {
569 return Err(meta.error(format!(
570 "unknown confirm policy '{other}' (expected never/low_confidence/always)"
571 )))
572 }
573 };
574 } else if meta.path.is_ident("pii") {
575 pii = true;
576 } else if meta.path.is_ident("min") {
577 min = Some(lit_to_f64(&meta.value()?.parse()?)?);
578 } else if meta.path.is_ident("max") {
579 max = Some(lit_to_f64(&meta.value()?.parse()?)?);
580 } else if meta.path.is_ident("non_empty") {
581 non_empty = true;
582 } else {
583 return Err(meta.error(
584 "unknown `slot` option (expected prompt/reprompt/state/confirm/pii/min/max/non_empty)",
585 ));
586 }
587 Ok(())
588 })?;
589 }
590
591 let validate = if min.is_some() || max.is_some() {
593 let min_tok = match min {
594 Some(v) => quote! { Some(#v) },
595 None => quote! { None },
596 };
597 let max_tok = match max {
598 Some(v) => quote! { Some(#v) },
599 None => quote! { None },
600 };
601 quote! { Some(#rt::frame::SlotValidator::Range { min: #min_tok, max: #max_tok }) }
602 } else if non_empty {
603 quote! { Some(#rt::frame::SlotValidator::NonEmpty) }
604 } else {
605 quote! { None }
606 };
607
608 let prompt_tok = match prompt {
609 Some(p) => quote! { Some(#p.to_string()) },
610 None => quote! { None },
611 };
612 let reprompt_tok = match reprompt {
613 Some(p) => quote! { Some(#p.to_string()) },
614 None => quote! { None },
615 };
616 slot_exprs.push(quote! {
617 #rt::frame::SlotSpec {
618 name: #fname.to_string(),
619 state_key: #state_key.to_string(),
620 prompt: #prompt_tok,
621 reprompt: #reprompt_tok,
622 confirm: #confirm,
623 pii: #pii,
624 recognizer: #recognizer,
625 validate: #validate,
626 }
627 });
628 }
629
630 let doc = format!("The `FrameSpec` derived from `{ident}`'s `#[slot(..)]` fields.");
631 Ok(quote! {
632 impl #rt::frame::Frame for #ident {
633 #[doc = #doc]
634 fn frame() -> #rt::frame::FrameSpec {
635 #rt::frame::FrameSpec {
636 name: #name.to_string(),
637 slots: ::std::vec![ #(#slot_exprs),* ],
638 }
639 }
640 }
641
642 impl #ident {
643 #[allow(dead_code)]
644 #[doc(hidden)]
645 fn __frame_mark_fields_used(&self) {
646 #( let _ = &self.#all_field_idents; )*
647 }
648 }
649 })
650}
651
652fn recognizer_expr(attr: &syn::Attribute) -> syn::Result<proc_macro2::TokenStream> {
654 let (rt, _) = runtime();
655 let r = quote! { #rt::extract::Recognizer };
656 let meta: Meta = attr.parse_args()?;
657 match meta {
658 Meta::Path(p) => {
659 let id = p
660 .get_ident()
661 .ok_or_else(|| syn::Error::new_spanned(&p, "expected a recognizer name"))?;
662 match id.to_string().as_str() {
663 "integer" => Ok(quote! { #r::integer() }),
664 "money" => Ok(quote! { #r::money() }),
665 "yes_no" => Ok(quote! { #r::yes_no() }),
666 "datetime" => Ok(quote! { #r::datetime() }),
667 other => Err(syn::Error::new_spanned(
668 &p,
669 format!("unknown recognizer `{other}`"),
670 )),
671 }
672 }
673 Meta::NameValue(nv) => {
674 let id = nv
675 .path
676 .get_ident()
677 .ok_or_else(|| syn::Error::new_spanned(&nv.path, "expected a recognizer name"))?;
678 match id.to_string().as_str() {
679 "integer_near" => {
680 let a = str_array(&nv.value)?;
681 Ok(quote! { #r::integer_near([ #(#a),* ]) })
682 }
683 "one_of" => {
684 let a = str_array(&nv.value)?;
685 Ok(quote! { #r::one_of([ #(#a),* ]) })
686 }
687 "fuzzy" => {
688 let a = str_array(&nv.value)?;
689 Ok(quote! { #r::fuzzy([ #(#a),* ]) })
690 }
691 "regex" => {
692 let s = str_lit(&nv.value)?;
693 Ok(quote! { #r::regex(#s) })
694 }
695 other => Err(syn::Error::new_spanned(
696 &nv.path,
697 format!("`{other}` does not take a value"),
698 )),
699 }
700 }
701 Meta::List(l) => Err(syn::Error::new_spanned(
702 l,
703 "unexpected nested list in `#[recognize(..)]`",
704 )),
705 }
706}
707
708fn slot_recognizer_expr(attr: &syn::Attribute) -> syn::Result<proc_macro2::TokenStream> {
711 let (rt, _) = runtime();
712 let r = quote! { #rt::frame::SlotRecognizer };
713 let meta: Meta = attr.parse_args()?;
714 match meta {
715 Meta::Path(p) => {
716 let id = p
717 .get_ident()
718 .ok_or_else(|| syn::Error::new_spanned(&p, "expected a recognizer name"))?;
719 match id.to_string().as_str() {
720 "integer" => Ok(quote! { #r::Integer }),
721 "money" => Ok(quote! { #r::Money }),
722 "yes_no" => Ok(quote! { #r::YesNo }),
723 "datetime" => Ok(quote! { #r::DateTime }),
724 other => Err(syn::Error::new_spanned(
725 &p,
726 format!("unknown recognizer `{other}`"),
727 )),
728 }
729 }
730 Meta::NameValue(nv) => {
731 let id = nv
732 .path
733 .get_ident()
734 .ok_or_else(|| syn::Error::new_spanned(&nv.path, "expected a recognizer name"))?;
735 match id.to_string().as_str() {
736 "integer_near" => {
737 let a = str_array(&nv.value)?;
738 Ok(quote! { #r::IntegerNear(::std::vec![ #(#a.to_string()),* ]) })
739 }
740 "one_of" => {
741 let a = str_array(&nv.value)?;
742 Ok(quote! { #r::OneOf(::std::vec![ #(#a.to_string()),* ]) })
743 }
744 "fuzzy" => {
745 let a = str_array(&nv.value)?;
746 Ok(quote! { #r::Fuzzy(::std::vec![ #(#a.to_string()),* ]) })
747 }
748 "regex" => {
749 let s = str_lit(&nv.value)?;
750 Ok(quote! { #r::Regex(#s.to_string()) })
751 }
752 other => Err(syn::Error::new_spanned(
753 &nv.path,
754 format!("`{other}` does not take a value"),
755 )),
756 }
757 }
758 Meta::List(l) => Err(syn::Error::new_spanned(
759 l,
760 "unexpected nested list in `#[recognize(..)]`",
761 )),
762 }
763}
764
765fn lit_to_f64(lit: &Lit) -> syn::Result<f64> {
767 match lit {
768 Lit::Int(i) => i.base10_parse::<f64>(),
769 Lit::Float(f) => f.base10_parse::<f64>(),
770 other => Err(syn::Error::new_spanned(
771 other,
772 "expected a numeric literal for `min`/`max`",
773 )),
774 }
775}
776
777fn str_array(expr: &Expr) -> syn::Result<Vec<LitStr>> {
779 match expr {
780 Expr::Array(arr) => arr
781 .elems
782 .iter()
783 .map(|e| match e {
784 Expr::Lit(ExprLit {
785 lit: Lit::Str(s), ..
786 }) => Ok(s.clone()),
787 other => Err(syn::Error::new_spanned(
788 other,
789 "expected a string literal in the array",
790 )),
791 })
792 .collect(),
793 other => Err(syn::Error::new_spanned(
794 other,
795 "expected an array of string literals, e.g. [\"a\", \"b\"]",
796 )),
797 }
798}
799
800fn str_lit(expr: &Expr) -> syn::Result<LitStr> {
802 match expr {
803 Expr::Lit(ExprLit {
804 lit: Lit::Str(s), ..
805 }) => Ok(s.clone()),
806 other => Err(syn::Error::new_spanned(other, "expected a string literal")),
807 }
808}
809
810fn to_snake_case(s: &str) -> String {
812 let mut out = String::with_capacity(s.len() + 4);
813 for (i, ch) in s.chars().enumerate() {
814 if ch.is_uppercase() {
815 if i != 0 {
816 out.push('_');
817 }
818 out.extend(ch.to_lowercase());
819 } else {
820 out.push(ch);
821 }
822 }
823 out
824}
825
826fn is_option(ty: &Type) -> bool {
834 let Type::Path(TypePath { qself: None, path }) = ty else {
835 return false;
836 };
837 if path
839 .segments
840 .iter()
841 .rev()
842 .skip(1)
843 .any(|seg| !seg.arguments.is_none())
844 {
845 return false;
846 }
847 let idents: Vec<&syn::Ident> = path.segments.iter().map(|seg| &seg.ident).collect();
848 match idents.as_slice() {
849 [opt] => path.leading_colon.is_none() && *opt == "Option",
852 [module, opt] => path.leading_colon.is_none() && *module == "option" && *opt == "Option",
853 [root, module, opt] => {
855 (*root == "std" || *root == "core") && *module == "option" && *opt == "Option"
856 }
857 _ => false,
858 }
859}
860
861fn to_pascal_case(s: &str) -> String {
863 let mut out = String::with_capacity(s.len());
864 let mut upper_next = true;
865 for ch in s.chars() {
866 if ch == '_' {
867 upper_next = true;
868 } else if upper_next {
869 out.extend(ch.to_uppercase());
870 upper_next = false;
871 } else {
872 out.push(ch);
873 }
874 }
875 out
876}
877
878#[cfg(test)]
879mod tests {
880 use super::is_option;
881 use syn::parse_quote;
882
883 #[test]
884 fn is_option_accepts_std_core_paths() {
885 assert!(is_option(&parse_quote!(Option<String>)));
886 assert!(is_option(&parse_quote!(option::Option<String>)));
887 assert!(is_option(&parse_quote!(std::option::Option<String>)));
888 assert!(is_option(&parse_quote!(core::option::Option<String>)));
889 assert!(is_option(&parse_quote!(::std::option::Option<String>)));
890 assert!(is_option(&parse_quote!(::core::option::Option<String>)));
891 }
892
893 #[test]
894 fn is_option_rejects_lookalikes() {
895 assert!(!is_option(&parse_quote!(String)));
896 assert!(!is_option(&parse_quote!(Vec<Option<String>>)));
897 assert!(!is_option(&parse_quote!(my::Option<String>)));
898 assert!(!is_option(&parse_quote!(my::option::Option<String>)));
899 assert!(!is_option(&parse_quote!(::option::Option<String>)));
900 assert!(!is_option(&parse_quote!(<T as Trait>::Option)));
901 }
902}