Skip to main content

oino_extension_macros/
lib.rs

1//! Proc macros for the Oino guest SDK.
2//!
3//! `#[oino_extension::tool]` on an `async fn name(&'static self, args: A, cx: ToolCtx) ->
4//! Result<ToolResult>` inside an `impl` block generates a sibling `fn name_tool() -> Tool<Self>`
5//! whose description is the doc comment and whose parameters come from `A: JsonSchema`.
6//!
7//! ```ignore
8//! impl Notify {
9//!     /// Send a notification to the user.
10//!     #[oino_extension::tool(label = "Notify user", kind = Other)]
11//!     async fn notify_user(&'static self, args: NotifyArgs, cx: ToolCtx) -> Result<ToolResult> { … }
12//! }
13//! // in init: cx.tool(Self::notify_user_tool());
14//! ```
15
16use 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/// Generates `fn <name>_tool() -> ::oino_extension::tool::Tool<Self>` for a tool method.
23///
24/// Arguments: `name = "..."` (default: fn name), `label = "..."`, `kind = Read|Edit|…`.
25#[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}