aquaregia_macros/lib.rs
1//! Procedural macros for the Aquaregia crate.
2//!
3//! This crate provides the `#[tool]` procedural macro for concise tool definitions
4//! in Aquaregia agents.
5//!
6//! ## The `#[tool]` Macro
7//!
8//! The `#[tool]` macro generates a function that returns a [`aquaregia::Tool`] with
9//! automatic JSON Schema derivation and typed argument handling.
10//!
11//! ### Usage
12//!
13//! ```rust,no_run
14//! use aquaregia::tool;
15//! use serde_json::{Value, json};
16//!
17//! /// Get weather information by city
18//! #[tool(description = "Get weather by city")]
19//! async fn get_weather(city: String) -> Result<Value, String> {
20//! Ok(json!({ "city": city, "temp_c": 23, "condition": "sunny" }))
21//! }
22//!
23//! // The macro generates:
24//! // - A struct `get_weather_args` with Deserialize and JsonSchema derives
25//! // - A function `get_weather()` that returns a `Tool`
26//! // - Automatic schema validation for arguments
27//! ```
28//!
29//! ### Macro Requirements
30//!
31//! - Must be an `async fn`
32//! - Must return `Result<Value, String>` or similar error type
33//! - Parameters must be simple identifiers with types (no patterns)
34//! - No generic parameters or where clauses (currently)
35//! - No `self` receivers (must be free functions)
36//!
37//! ### Generated Code
38//!
39//! For a function like:
40//!
41//! ```rust,ignore
42//! #[tool(description = "Example tool")]
43//! async fn example(x: String, y: i32) -> Result<Value, String> {
44//! // body
45//! }
46//! ```
47//!
48//! The macro generates:
49//!
50//! ```rust,ignore
51//! #[derive(Deserialize, JsonSchema)]
52//! struct __AquaregiaToolArgs_example {
53//! x: String,
54//! y: i32,
55//! }
56//!
57//! fn example() -> Tool {
58//! tool("example")
59//! .description("Example tool")
60//! .execute(|args: __AquaregiaToolArgs_example| async move {
61//! __aquaregia_tool_handler_example(args.x, args.y)
62//! .await
63//! .map_err(|err| ToolExecError::Execution(err.to_string()))
64//! })
65//! }
66//!
67//! async fn __aquaregia_tool_handler_example(x: String, y: i32) -> Result<Value, String> {
68//! // original body
69//! }
70//! ```
71
72use proc_macro::TokenStream;
73use quote::{format_ident, quote};
74use syn::parse::Parser;
75use syn::spanned::Spanned;
76use syn::{
77 Expr, FnArg, ItemFn, Lit, LitStr, Meta, Pat, Token, punctuated::Punctuated, parse_macro_input,
78};
79
80#[proc_macro_attribute]
81pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
82 let metas = match Punctuated::<Meta, Token![,]>::parse_terminated.parse(attr) {
83 Ok(metas) => metas,
84 Err(err) => return err.to_compile_error().into(),
85 };
86
87 let mut description: Option<LitStr> = None;
88 for meta in metas {
89 match meta {
90 Meta::NameValue(name_value) if name_value.path.is_ident("description") => {
91 if description.is_some() {
92 return syn::Error::new(
93 name_value.span(),
94 "duplicate `description` argument in #[tool(...)]",
95 )
96 .to_compile_error()
97 .into();
98 }
99 match name_value.value {
100 Expr::Lit(expr_lit) => match expr_lit.lit {
101 Lit::Str(lit) => description = Some(lit),
102 _ => {
103 return syn::Error::new(
104 expr_lit.span(),
105 "`description` must be a string literal",
106 )
107 .to_compile_error()
108 .into();
109 }
110 },
111 _ => {
112 return syn::Error::new(
113 name_value.value.span(),
114 "`description` must be a string literal",
115 )
116 .to_compile_error()
117 .into();
118 }
119 }
120 }
121 other => {
122 return syn::Error::new(
123 other.span(),
124 "unsupported #[tool(...)] argument; expected `description = \"...\"`",
125 )
126 .to_compile_error()
127 .into();
128 }
129 }
130 }
131
132 let input = parse_macro_input!(item as ItemFn);
133 if input.sig.asyncness.is_none() {
134 return syn::Error::new(
135 input.sig.fn_token.span(),
136 "#[tool] requires an `async fn` handler",
137 )
138 .to_compile_error()
139 .into();
140 }
141 if !input.sig.generics.params.is_empty() || input.sig.generics.where_clause.is_some() {
142 return syn::Error::new(
143 input.sig.generics.span(),
144 "#[tool] does not support generic parameters yet",
145 )
146 .to_compile_error()
147 .into();
148 }
149
150 let vis = input.vis;
151 let attrs = input.attrs;
152 let fn_name = input.sig.ident;
153 let output = input.sig.output;
154 let body = input.block;
155
156 let mut arg_idents = Vec::new();
157 let mut arg_tys = Vec::new();
158 for arg in input.sig.inputs {
159 match arg {
160 FnArg::Receiver(receiver) => {
161 return syn::Error::new(
162 receiver.span(),
163 "#[tool] does not support methods with `self`",
164 )
165 .to_compile_error()
166 .into();
167 }
168 FnArg::Typed(pat_type) => {
169 let ident = match *pat_type.pat {
170 Pat::Ident(pat_ident)
171 if pat_ident.by_ref.is_none()
172 && pat_ident.mutability.is_none()
173 && pat_ident.subpat.is_none() =>
174 {
175 pat_ident.ident
176 }
177 other => {
178 return syn::Error::new(
179 other.span(),
180 "#[tool] parameters must be simple identifiers, e.g. `city: String`",
181 )
182 .to_compile_error()
183 .into();
184 }
185 };
186 arg_idents.push(ident);
187 arg_tys.push(*pat_type.ty);
188 }
189 }
190 }
191
192 let description_lit =
193 description.unwrap_or_else(|| LitStr::new("", proc_macro2::Span::call_site()));
194 let args_ident = format_ident!("__AquaregiaToolArgs_{}", fn_name);
195 let handler_ident = format_ident!("__aquaregia_tool_handler_{}", fn_name);
196 let arg_extracts = arg_idents.iter().map(|ident| quote! { args.#ident });
197
198 quote! {
199 #(#attrs)*
200 #vis fn #fn_name() -> ::aquaregia::Tool {
201 #[allow(non_camel_case_types)]
202 #[derive(::aquaregia::__aquaregia_serde::Deserialize, ::aquaregia::__aquaregia_schemars::JsonSchema)]
203 struct #args_ident {
204 #( #arg_idents: #arg_tys, )*
205 }
206
207 ::aquaregia::tool(stringify!(#fn_name))
208 .description(#description_lit)
209 .execute(|args: #args_ident| async move {
210 #handler_ident( #( #arg_extracts ),* )
211 .await
212 .map_err(|err| ::aquaregia::ToolExecError::Execution(err.to_string()))
213 })
214 }
215
216 async fn #handler_ident( #( #arg_idents: #arg_tys ),* ) #output {
217 #body
218 }
219 }
220 .into()
221}