use crate::error::ToolError;
#[async_trait::async_trait]
pub trait BuiltinTool: Send + Sync + 'static {
fn description(&self) -> &str;
fn parameters_schema(&self) -> serde_json::Value;
async fn execute(&self, arguments: serde_json::Value) -> Result<serde_json::Value, ToolError>;
}
pub struct EchoTool;
#[async_trait::async_trait]
impl BuiltinTool for EchoTool {
fn description(&self) -> &str {
"Echo the input back unchanged"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"text": { "type": "string", "description": "Text to echo" }
},
"required": ["text"]
})
}
async fn execute(&self, arguments: serde_json::Value) -> Result<serde_json::Value, ToolError> {
Ok(arguments)
}
}
pub struct JsonKeysTool;
#[async_trait::async_trait]
impl BuiltinTool for JsonKeysTool {
fn description(&self) -> &str {
"Extract the top-level keys from a JSON object"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"json": { "type": "object", "description": "JSON object to extract keys from" }
},
"required": ["json"]
})
}
async fn execute(&self, arguments: serde_json::Value) -> Result<serde_json::Value, ToolError> {
let obj = arguments
.get("json")
.and_then(|v| v.as_object())
.ok_or_else(|| ToolError::InvalidArgs {
tool: "json_keys".to_string(),
reason: "Expected 'json' field with an object value".to_string(),
})?;
let keys: Vec<&str> = obj.keys().map(|k| k.as_str()).collect();
Ok(serde_json::json!({"keys": keys}))
}
}
pub struct TextSplitTool;
#[async_trait::async_trait]
impl BuiltinTool for TextSplitTool {
fn description(&self) -> &str {
"Split text by a delimiter and return the parts"
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"text": { "type": "string", "description": "Text to split" },
"delimiter": { "type": "string", "description": "Delimiter (default: newline)" }
},
"required": ["text"]
})
}
async fn execute(&self, arguments: serde_json::Value) -> Result<serde_json::Value, ToolError> {
let text = arguments
.get("text")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidArgs {
tool: "text_split".to_string(),
reason: "Expected 'text' string field".to_string(),
})?;
let delimiter = arguments
.get("delimiter")
.and_then(|v| v.as_str())
.unwrap_or("\n");
let parts: Vec<&str> = text.split(delimiter).collect();
Ok(serde_json::json!({"parts": parts, "count": parts.len()}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn echo_returns_input() {
let tool = EchoTool;
let result = tool
.execute(serde_json::json!({"text": "hello", "extra": 42}))
.await
.unwrap();
assert_eq!(result["text"], "hello");
assert_eq!(result["extra"], 42);
}
#[tokio::test]
async fn json_keys_extracts_keys() {
let tool = JsonKeysTool;
let result = tool
.execute(serde_json::json!({"json": {"name": "Alice", "age": 30, "city": "NYC"}}))
.await
.unwrap();
let keys = result["keys"].as_array().unwrap();
assert_eq!(keys.len(), 3);
}
#[tokio::test]
async fn json_keys_invalid_input() {
let tool = JsonKeysTool;
let result = tool
.execute(serde_json::json!({"json": "not an object"}))
.await;
assert!(matches!(result, Err(ToolError::InvalidArgs { .. })));
}
#[tokio::test]
async fn text_split_default_newline() {
let tool = TextSplitTool;
let result = tool
.execute(serde_json::json!({"text": "line1\nline2\nline3"}))
.await
.unwrap();
assert_eq!(result["count"], 3);
assert_eq!(result["parts"][0], "line1");
}
#[tokio::test]
async fn text_split_custom_delimiter() {
let tool = TextSplitTool;
let result = tool
.execute(serde_json::json!({"text": "a,b,c", "delimiter": ","}))
.await
.unwrap();
assert_eq!(result["count"], 3);
}
}