use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ParameterSchema {
pub name: String,
pub description: String,
pub param_type: String,
pub required: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ToolSchema {
pub name: String,
pub description: String,
pub parameters: Vec<ParameterSchema>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ToolInput {
pub parameters: HashMap<String, Value>,
}
impl ToolInput {
#[must_use]
pub fn new(parameters: HashMap<String, Value>) -> Self {
Self { parameters }
}
#[inline]
#[must_use]
pub fn get_str(&self, key: &str) -> Option<&str> {
self.parameters.get(key).and_then(|v| v.as_str())
}
#[inline]
#[must_use]
pub fn get_f64(&self, key: &str) -> Option<f64> {
self.parameters.get(key).and_then(|v| v.as_f64())
}
#[inline]
#[must_use]
pub fn get_u64(&self, key: &str) -> Option<u64> {
self.parameters.get(key).and_then(|v| v.as_u64())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ToolOutput {
pub success: bool,
pub result: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl ToolOutput {
#[inline]
#[must_use]
pub fn ok(result: Value) -> Self {
Self {
success: true,
result,
error: None,
}
}
#[inline]
#[must_use]
pub fn err(msg: impl Into<String>) -> Self {
Self {
success: false,
result: Value::Null,
error: Some(msg.into()),
}
}
}
pub trait NativeTool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn schema(&self) -> ToolSchema;
fn execute(&self, input: ToolInput) -> Pin<Box<dyn Future<Output = ToolOutput> + Send + '_>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_output_ok_has_correct_fields() {
let output = ToolOutput::ok(serde_json::json!(42));
assert!(output.success);
assert_eq!(output.result, serde_json::json!(42));
assert!(output.error.is_none());
}
#[test]
fn tool_output_err_has_correct_fields() {
let output = ToolOutput::err("something broke");
assert!(!output.success);
assert_eq!(output.result, serde_json::Value::Null);
assert_eq!(output.error.as_deref(), Some("something broke"));
}
#[test]
fn tool_input_get_str_returns_none_for_missing() {
let input = ToolInput {
parameters: HashMap::new(),
};
assert!(input.get_str("missing").is_none());
}
#[test]
fn tool_input_get_str_returns_none_for_wrong_type() {
let input = ToolInput {
parameters: HashMap::from([("count".into(), serde_json::json!(42))]),
};
assert!(input.get_str("count").is_none());
}
#[test]
fn tool_input_get_str_returns_value() {
let input = ToolInput {
parameters: HashMap::from([("name".into(), serde_json::json!("alice"))]),
};
assert_eq!(input.get_str("name"), Some("alice"));
}
#[test]
fn tool_input_get_f64_returns_value() {
let input = ToolInput {
parameters: HashMap::from([("ratio".into(), serde_json::json!(1.5))]),
};
assert_eq!(input.get_f64("ratio"), Some(1.5));
}
#[test]
fn tool_input_get_u64_returns_value() {
let input = ToolInput {
parameters: HashMap::from([("count".into(), serde_json::json!(42))]),
};
assert_eq!(input.get_u64("count"), Some(42));
}
#[test]
fn tool_input_get_u64_returns_none_for_negative() {
let input = ToolInput {
parameters: HashMap::from([("count".into(), serde_json::json!(-1))]),
};
assert!(input.get_u64("count").is_none());
}
#[test]
fn tool_output_ok_serde_round_trip() {
let output = ToolOutput::ok(serde_json::json!({"key": "value"}));
let json = serde_json::to_string(&output).unwrap();
let restored: ToolOutput = serde_json::from_str(&json).unwrap();
assert!(restored.success);
assert_eq!(restored.result["key"], "value");
}
#[test]
fn tool_output_err_skips_none_error_in_ok() {
let output = ToolOutput::ok(serde_json::json!(1));
let json = serde_json::to_string(&output).unwrap();
assert!(!json.contains("error"));
}
#[test]
fn tool_schema_serde_round_trip() {
let schema = ToolSchema {
name: "test".into(),
description: "desc".into(),
parameters: vec![ParameterSchema {
name: "p1".into(),
description: "param 1".into(),
param_type: "string".into(),
required: true,
}],
};
let json = serde_json::to_string(&schema).unwrap();
let restored: ToolSchema = serde_json::from_str(&json).unwrap();
assert_eq!(restored.name, "test");
assert_eq!(restored.parameters.len(), 1);
assert!(restored.parameters[0].required);
}
}