loopctl_derive/lib.rs
1//! Procedural-macro companion to `loopctl`'s [`Tool`] trait.
2//!
3//! [`Tool`] here is a derive macro: apply it to a
4//! `Deserialize` struct describing the tool's input and it generates the
5//! `impl loopctl::tool::Tool` block — the name, description, JSON-Schema
6//! built statically from the struct's fields, and a `call` that
7//! deserializes the incoming `Value` into the struct and dispatches to
8//! an inherent `async fn run` handler you supply.
9//!
10//! # Attribute Grammar
11//!
12//! On the struct (container attributes):
13//!
14//! ```text
15//! #[tool(
16//! name = "snake_case_name", // default: snake_cased struct ident
17//! description = "one-liner", // default: struct doc comment
18//! read_only, // override is_read_only() -> true
19//! concurrency_safe, // override is_concurrency_safe() -> true
20//! system_prompt = "extra hint", // override system_prompt()
21//! handler = "my_run", // default: "run"
22//! allow_extra // omit additionalProperties: false
23//! )]
24//! ```
25//!
26//! On each field:
27//!
28//! ```text
29//! #[tool(
30//! name = "json_key", // default: Rust field name (or serde's rename)
31//! description = "field help", // default: field doc comment
32//! skip, // exclude from schema + required
33//! default // omit from required, keep in properties
34//! )]
35//! ```
36//!
37//! `#[serde(rename = "…")]` and `#[serde(rename_all = "…")]` are
38//! honored for schema naming, mirroring what serde deserializes with;
39//! `#[tool(name = "…")]` takes precedence over both.
40//!
41//! Re-exported from the `loopctl` crate behind its `derive` feature, so
42//! the usual spelling is:
43//!
44//! ```rust,ignore
45//! use loopctl::Tool; // brings both the trait and the derive into scope
46//! ```
47//!
48//! See the `Tool` trait docs in `loopctl` for the attribute grammar.
49//!
50//! [`Tool`]: https://docs.rs/loopctl/latest/loopctl/tool/trait.Tool.html
51
52mod attr;
53mod expand;
54
55use proc_macro::TokenStream;
56
57/// Derive `loopctl::tool::Tool` for a struct.
58///
59/// Generates all four required methods from the struct's shape: the
60/// name (container override or the `snake_cased` identifier), the
61/// description (container override or the struct's doc comment — one
62/// of the two is required), the JSON Schema built statically from the
63/// fields, and a `call` that deserializes the incoming `Value` and
64/// dispatches to a handler. The struct must implement
65/// `serde::Deserialize` (add `#[derive(serde::Deserialize)]`
66/// alongside) and provide an inherent
67/// `async fn run(&self, input: Self, ctx: &ToolContext)` returning
68/// `Result<ToolOutput, ToolError>`; rename the handler with
69/// `#[tool(handler = "…")]`. Tools with dynamic schemas or
70/// unmappable field types fall back to a manual `impl`.
71#[proc_macro_derive(Tool, attributes(tool))]
72pub fn derive_tool(input: TokenStream) -> TokenStream {
73 let input = syn::parse_macro_input!(input as syn::DeriveInput);
74 expand::expand_derive_tool(input).into()
75}