Skip to main content

cersei_tools_derive/
lib.rs

1//! cersei-tools-derive: Proc macro for deriving the Tool trait.
2//!
3//! Usage:
4//! ```ignore
5//! #[derive(Tool)]
6//! #[tool(name = "my_tool", description = "Does something", permission = "read_only")]
7//! struct MyTool;
8//!
9//! #[async_trait]
10//! impl ToolExecute for MyTool {
11//!     type Input = MyInput;
12//!     async fn run(&self, input: MyInput, ctx: &ToolContext) -> ToolResult { ... }
13//! }
14//! ```
15
16use proc_macro::TokenStream;
17use quote::quote;
18use syn::{parse_macro_input, DeriveInput};
19
20#[proc_macro_derive(Tool, attributes(tool))]
21pub fn derive_tool(input: TokenStream) -> TokenStream {
22    let input = parse_macro_input!(input as DeriveInput);
23    let name = &input.ident;
24
25    // Parse #[tool(...)] attributes
26    let mut tool_name = name.to_string().to_lowercase();
27    let mut tool_description = String::new();
28    let mut tool_permission = quote! { cersei_tools::PermissionLevel::None };
29    let mut tool_category = quote! { cersei_tools::ToolCategory::Custom };
30
31    for attr in &input.attrs {
32        if !attr.path().is_ident("tool") {
33            continue;
34        }
35        let _ = attr.parse_nested_meta(|meta| {
36            if let Some(ident) = meta.path.get_ident() {
37                let key = ident.to_string();
38                let value: syn::LitStr = meta.value()?.parse()?;
39                let val = value.value();
40                match key.as_str() {
41                    "name" => tool_name = val,
42                    "description" => tool_description = val,
43                    "permission" => {
44                        tool_permission = match val.as_str() {
45                            "none" => quote! { cersei_tools::PermissionLevel::None },
46                            "read_only" => quote! { cersei_tools::PermissionLevel::ReadOnly },
47                            "write" => quote! { cersei_tools::PermissionLevel::Write },
48                            "execute" => quote! { cersei_tools::PermissionLevel::Execute },
49                            "dangerous" => quote! { cersei_tools::PermissionLevel::Dangerous },
50                            _ => quote! { cersei_tools::PermissionLevel::None },
51                        };
52                    }
53                    "category" => {
54                        tool_category = match val.as_str() {
55                            "filesystem" => quote! { cersei_tools::ToolCategory::FileSystem },
56                            "shell" => quote! { cersei_tools::ToolCategory::Shell },
57                            "web" => quote! { cersei_tools::ToolCategory::Web },
58                            "memory" => quote! { cersei_tools::ToolCategory::Memory },
59                            "orchestration" => quote! { cersei_tools::ToolCategory::Orchestration },
60                            "mcp" => quote! { cersei_tools::ToolCategory::Mcp },
61                            _ => quote! { cersei_tools::ToolCategory::Custom },
62                        };
63                    }
64                    _ => {}
65                }
66            }
67            Ok(())
68        });
69    }
70
71    let expanded = quote! {
72        #[async_trait::async_trait]
73        impl cersei_tools::Tool for #name {
74            fn name(&self) -> &str {
75                #tool_name
76            }
77
78            fn description(&self) -> &str {
79                #tool_description
80            }
81
82            fn permission_level(&self) -> cersei_tools::PermissionLevel {
83                #tool_permission
84            }
85
86            fn category(&self) -> cersei_tools::ToolCategory {
87                #tool_category
88            }
89
90            fn input_schema(&self) -> serde_json::Value {
91                let schema = schemars::schema_for!(
92                    <Self as cersei_tools::ToolExecute>::Input
93                );
94                serde_json::to_value(schema).unwrap_or(serde_json::json!({}))
95            }
96
97            async fn execute(
98                &self,
99                input: serde_json::Value,
100                ctx: &cersei_tools::ToolContext,
101            ) -> cersei_tools::ToolResult {
102                match serde_json::from_value::<<Self as cersei_tools::ToolExecute>::Input>(input) {
103                    Ok(typed_input) => {
104                        <Self as cersei_tools::ToolExecute>::run(self, typed_input, ctx).await
105                    }
106                    Err(e) => cersei_tools::ToolResult::error(
107                        format!("Invalid input for '{}': {}", #tool_name, e)
108                    ),
109                }
110            }
111        }
112    };
113
114    TokenStream::from(expanded)
115}