oino_extension_macros/
lib.rs1use proc_macro::TokenStream;
17use quote::{format_ident, quote};
18use syn::parse::Parser;
19use syn::punctuated::Punctuated;
20use syn::{Expr, ExprLit, FnArg, ImplItemFn, Lit, Meta, Pat, Token};
21
22#[proc_macro_attribute]
26pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
27 match expand(attr.into(), item.into()) {
28 Ok(ts) => ts.into(),
29 Err(e) => e.to_compile_error().into(),
30 }
31}
32
33fn lit_str(expr: &Expr) -> syn::Result<String> {
34 match expr {
35 Expr::Lit(ExprLit {
36 lit: Lit::Str(s), ..
37 }) => Ok(s.value()),
38 other => Err(syn::Error::new_spanned(other, "expected a string literal")),
39 }
40}
41
42fn expand(
43 attr: proc_macro2::TokenStream,
44 item: proc_macro2::TokenStream,
45) -> syn::Result<proc_macro2::TokenStream> {
46 let func: ImplItemFn = syn::parse2(item)?;
47 let args = Punctuated::<Meta, Token![,]>::parse_terminated.parse2(attr)?;
48 let fn_name = func.sig.ident.clone();
49 let mut tool_name = fn_name.to_string();
50 let mut label = None;
51 let mut kind = None;
52 for meta in &args {
53 let Meta::NameValue(nv) = meta else {
54 return Err(syn::Error::new_spanned(meta, "expected `key = value`"));
55 };
56 let key = nv
57 .path
58 .get_ident()
59 .map(ToString::to_string)
60 .unwrap_or_default();
61 match key.as_str() {
62 "name" => tool_name = lit_str(&nv.value)?,
63 "label" => label = Some(lit_str(&nv.value)?),
64 "kind" => kind = Some(nv.value.clone()),
65 _ => {
66 return Err(syn::Error::new_spanned(
67 &nv.path,
68 "unknown key (name, label, kind)",
69 ));
70 }
71 }
72 }
73 let doc: Vec<String> = func
74 .attrs
75 .iter()
76 .filter(|a| a.path().is_ident("doc"))
77 .filter_map(|a| match &a.meta {
78 Meta::NameValue(nv) => lit_str(&nv.value).ok().map(|s| s.trim().to_owned()),
79 _ => None,
80 })
81 .collect();
82 let description = doc.join("\n").trim().to_owned();
83 let inputs: Vec<&FnArg> = func.sig.inputs.iter().collect();
84 let arg_ty = match inputs.get(1) {
85 Some(FnArg::Typed(pt)) if matches!(&*pt.pat, Pat::Ident(_)) => pt.ty.clone(),
86 _ => {
87 return Err(syn::Error::new_spanned(
88 &func.sig,
89 "expected `(&'static self, args: A, cx: ToolCtx)`",
90 ));
91 }
92 };
93 let builder = format_ident!("{}_tool", fn_name);
94 let label = label.map(|l| quote!(.label(#l)));
95 let kind = kind.map(|k| quote!(.kind(::oino_extension::tool::ToolKind::#k)));
96 Ok(quote! {
97 #func
98
99 #[doc = concat!("Tool definition generated for [`Self::", stringify!(#fn_name), "`].")]
100 fn #builder() -> ::oino_extension::tool::Tool<Self> {
101 ::oino_extension::tool::Tool::new(#tool_name)
102 .description(#description)
103 #label
104 #kind
105 .args::<#arg_ty>()
106 .run(|ext: &'static Self, args: #arg_ty, cx| ext.#fn_name(args, cx))
107 }
108 })
109}