use inventory::collect;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use crate::context::ToolContext;
use crate::error::ToolError;
pub struct ToolMeta {
pub name: &'static str,
pub description: &'static str,
pub call: fn(Arc<ToolContext>, serde_json::Value) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, ToolError>> + Send>>,
pub schema: fn() -> &'static serde_json::Value,
pub examples: fn() -> &'static [(&'static str, &'static str)],
}
collect!(ToolMeta);
pub fn all_tools() -> impl Iterator<Item = &'static ToolMeta> {
inventory::iter::<ToolMeta>.into_iter()
}
pub fn find_tool(name: &str) -> Option<&'static ToolMeta> {
all_tools().find(|meta| meta.name == name)
}
pub async fn call_tool(
name: &str,
params: serde_json::Value,
ctx: Arc<ToolContext>,
) -> Result<serde_json::Value, ToolError> {
let meta = find_tool(name).ok_or_else(|| ToolError::not_found(name))?;
(meta.call)(ctx, params).await
}
pub fn get_tool_schema(name: &str) -> Option<&'static serde_json::Value> {
find_tool(name).map(|meta| (meta.schema)())
}
pub fn get_tool_examples(name: &str) -> Option<&'static [(&'static str, &'static str)]> {
find_tool(name).map(|meta| (meta.examples)())
}
pub fn tool_exists(name: &str) -> bool {
find_tool(name).is_some()
}
pub fn tool_count() -> usize {
all_tools().count()
}
pub fn tool_names() -> Vec<&'static str> {
all_tools().map(|meta| meta.name).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Tool;
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;
#[derive(Deserialize, JsonSchema)]
struct TestParams {
value: i32,
}
#[derive(Serialize, JsonSchema)]
struct TestOutput {
result: i32,
}
struct TestTool;
impl Tool for TestTool {
type Params = TestParams;
type Output = TestOutput;
const NAME: &'static str = "test_tool_v2";
const DESCRIPTION: &'static str = "Test tool for v2";
async fn call(
_ctx: Arc<ToolContext>,
params: Self::Params,
) -> Result<Self::Output, ToolError> {
Ok(TestOutput {
result: params.value * 2,
})
}
}
inventory::submit! {
ToolMeta {
name: TestTool::NAME,
description: TestTool::DESCRIPTION,
call: |ctx, params| {
Box::pin(async move {
let params: TestParams = serde_json::from_value(params)?;
let result = <TestTool as Tool>::call(ctx, params).await?;
Ok(serde_json::to_value(result)?)
})
},
schema: || <TestTool as Tool>::schema(),
examples: || <TestTool as Tool>::EXAMPLES,
}
}
#[test]
fn test_find_tool() {
let tool = find_tool("test_tool_v2");
assert!(tool.is_some());
assert_eq!(tool.unwrap().name, "test_tool_v2");
}
#[test]
fn test_tool_exists() {
assert!(tool_exists("test_tool_v2"));
assert!(!tool_exists("nonexistent"));
}
#[test]
fn test_tool_count() {
assert!(tool_count() > 0);
}
#[test]
fn test_tool_names() {
let names = tool_names();
assert!(names.contains(&"test_tool_v2"));
}
#[tokio::test]
async fn test_call_tool() {
let ctx = Arc::new(ToolContext::new());
let params = serde_json::json!({"value": 5});
let result = call_tool("test_tool_v2", params, ctx).await.unwrap();
assert_eq!(result["result"], 10);
}
#[tokio::test]
async fn test_call_nonexistent_tool() {
let ctx = Arc::new(ToolContext::new());
let result = call_tool("nonexistent", serde_json::json!({}), ctx).await;
assert!(result.is_err());
}
}