Skip to main content

descry_tool_core/
registry.rs

1//! Compile-time tool registry using inventory
2//!
3//! Provides zero-cost tool registration at compile time.
4
5use inventory::collect;
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9
10use crate::context::ToolContext;
11use crate::error::ToolError;
12
13/// Tool metadata (type-erased)
14///
15/// This struct is submitted to inventory by the `#[tool]` macro.
16pub struct ToolMeta {
17    /// Tool name
18    pub name: &'static str,
19
20    /// Tool description
21    pub description: &'static str,
22
23    /// Type-erased call function
24    ///
25    /// Takes `Arc<ToolContext>` and JSON value, returns future.
26    pub call: fn(Arc<ToolContext>, serde_json::Value) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, ToolError>> + Send>>,
27
28    /// Schema generator function (returns JSON Value for flexibility)
29    pub schema: fn() -> &'static serde_json::Value,
30
31    /// Examples accessor function
32    pub examples: fn() -> &'static [(&'static str, &'static str)],
33}
34
35// Collect ToolMeta using inventory
36collect!(ToolMeta);
37
38/// Get all registered tools
39///
40/// Returns an iterator over all tools submitted via inventory.
41///
42/// # Examples
43///
44/// ```
45/// let tools: Vec<_> = descry_tool_core::all_tools().collect();
46/// println!("Available tools: {}", tools.len());
47/// ```
48pub fn all_tools() -> impl Iterator<Item = &'static ToolMeta> {
49    inventory::iter::<ToolMeta>.into_iter()
50}
51
52/// Find tool by name
53///
54/// # Examples
55///
56/// ```ignore
57/// let tool = descry_tool_core::find_tool("add");
58/// assert!(tool.is_some());
59/// ```
60pub fn find_tool(name: &str) -> Option<&'static ToolMeta> {
61    all_tools().find(|meta| meta.name == name)
62}
63
64/// Call tool by name
65///
66/// # Examples
67///
68/// ```ignore
69/// use std::sync::Arc;
70/// use descry_tool_core::{call_tool, ToolContext};
71///
72/// #[tokio::main]
73/// async fn main() {
74///     let ctx = Arc::new(ToolContext::new());
75///     let result = call_tool("add", json!({"a": 1, "b": 2}), ctx).await.unwrap();
76/// }
77/// ```
78pub async fn call_tool(
79    name: &str,
80    params: serde_json::Value,
81    ctx: Arc<ToolContext>,
82) -> Result<serde_json::Value, ToolError> {
83    let meta = find_tool(name).ok_or_else(|| ToolError::not_found(name))?;
84    (meta.call)(ctx, params).await
85}
86
87/// Get tool schema by name
88pub fn get_tool_schema(name: &str) -> Option<&'static serde_json::Value> {
89    find_tool(name).map(|meta| (meta.schema)())
90}
91
92/// Get tool examples by name
93pub fn get_tool_examples(name: &str) -> Option<&'static [(&'static str, &'static str)]> {
94    find_tool(name).map(|meta| (meta.examples)())
95}
96
97/// Check if tool exists
98pub fn tool_exists(name: &str) -> bool {
99    find_tool(name).is_some()
100}
101
102/// Get tool count
103pub fn tool_count() -> usize {
104    all_tools().count()
105}
106
107/// Get all tool names
108pub fn tool_names() -> Vec<&'static str> {
109    all_tools().map(|meta| meta.name).collect()
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::Tool;
116    use serde::{Deserialize, Serialize};
117    use schemars::JsonSchema;
118
119    #[derive(Deserialize, JsonSchema)]
120    struct TestParams {
121        value: i32,
122    }
123
124    #[derive(Serialize, JsonSchema)]
125    struct TestOutput {
126        result: i32,
127    }
128
129    struct TestTool;
130
131    impl Tool for TestTool {
132        type Params = TestParams;
133        type Output = TestOutput;
134
135        const NAME: &'static str = "test_tool_v2";
136        const DESCRIPTION: &'static str = "Test tool for v2";
137
138        async fn call(
139            _ctx: Arc<ToolContext>,
140            params: Self::Params,
141        ) -> Result<Self::Output, ToolError> {
142            Ok(TestOutput {
143                result: params.value * 2,
144            })
145        }
146    }
147
148    // Manual inventory submission for testing
149    inventory::submit! {
150        ToolMeta {
151            name: TestTool::NAME,
152            description: TestTool::DESCRIPTION,
153            call: |ctx, params| {
154                Box::pin(async move {
155                    let params: TestParams = serde_json::from_value(params)?;
156                    let result = <TestTool as Tool>::call(ctx, params).await?;
157                    Ok(serde_json::to_value(result)?)
158                })
159            },
160            schema: || <TestTool as Tool>::schema(),
161            examples: || <TestTool as Tool>::EXAMPLES,
162        }
163    }
164
165    #[test]
166    fn test_find_tool() {
167        let tool = find_tool("test_tool_v2");
168        assert!(tool.is_some());
169        assert_eq!(tool.unwrap().name, "test_tool_v2");
170    }
171
172    #[test]
173    fn test_tool_exists() {
174        assert!(tool_exists("test_tool_v2"));
175        assert!(!tool_exists("nonexistent"));
176    }
177
178    #[test]
179    fn test_tool_count() {
180        assert!(tool_count() > 0);
181    }
182
183    #[test]
184    fn test_tool_names() {
185        let names = tool_names();
186        assert!(names.contains(&"test_tool_v2"));
187    }
188
189    #[tokio::test]
190    async fn test_call_tool() {
191        let ctx = Arc::new(ToolContext::new());
192        let params = serde_json::json!({"value": 5});
193        let result = call_tool("test_tool_v2", params, ctx).await.unwrap();
194        assert_eq!(result["result"], 10);
195    }
196
197    #[tokio::test]
198    async fn test_call_nonexistent_tool() {
199        let ctx = Arc::new(ToolContext::new());
200        let result = call_tool("nonexistent", serde_json::json!({}), ctx).await;
201        assert!(result.is_err());
202    }
203}