use std::sync::Arc;
use crate::BoxFuture;
use crate::error::{SynwireError, ToolError};
use crate::tools::types::{ToolOutput, ToolSchema};
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn schema(&self) -> &ToolSchema;
fn invoke(&self, input: serde_json::Value) -> BoxFuture<'_, Result<ToolOutput, SynwireError>>;
}
pub fn validate_tool_name(name: &str) -> Result<(), SynwireError> {
if name.is_empty() || name.len() > 64 {
return Err(SynwireError::Tool(ToolError::InvalidName {
name: name.into(),
reason: "name must be 1-64 characters".into(),
}));
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
{
return Err(SynwireError::Tool(ToolError::InvalidName {
name: name.into(),
reason: "name must match [a-zA-Z0-9_.\\-]".into(),
}));
}
if name.starts_with('.') || name.ends_with('.') || name.contains("..") {
return Err(SynwireError::Tool(ToolError::InvalidName {
name: name.into(),
reason: "dots must separate non-empty segments".into(),
}));
}
Ok(())
}
pub trait ToolProvider: Send + Sync {
fn discover_tools(&self) -> BoxFuture<'_, Result<Vec<Arc<dyn Tool>>, SynwireError>>;
fn get_tool(&self, name: &str) -> BoxFuture<'_, Result<Option<Arc<dyn Tool>>, SynwireError>>;
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn valid_names() {
validate_tool_name("search").unwrap();
validate_tool_name("my-tool").unwrap();
validate_tool_name("tool_123").unwrap();
validate_tool_name("A").unwrap();
let long_name = "a".repeat(64);
validate_tool_name(&long_name).unwrap();
}
#[test]
fn rejects_empty_name() {
let err = validate_tool_name("").unwrap_err();
assert!(err.to_string().contains("1-64 characters"));
}
#[test]
fn rejects_too_long_name() {
let name = "a".repeat(65);
let err = validate_tool_name(&name).unwrap_err();
assert!(err.to_string().contains("1-64 characters"));
}
#[test]
fn rejects_special_characters() {
let err = validate_tool_name("my tool").unwrap_err();
assert!(err.to_string().contains("name must match"));
}
#[test]
fn accepts_dotted_names() {
validate_tool_name("my.tool").unwrap();
validate_tool_name("code.search").unwrap();
validate_tool_name("debug.status").unwrap();
validate_tool_name("a.b.c").unwrap();
}
#[test]
fn rejects_leading_dot() {
let err = validate_tool_name(".tool").unwrap_err();
assert!(err.to_string().contains("dots must separate"));
}
#[test]
fn rejects_trailing_dot() {
let err = validate_tool_name("tool.").unwrap_err();
assert!(err.to_string().contains("dots must separate"));
}
#[test]
fn rejects_consecutive_dots() {
let err = validate_tool_name("my..tool").unwrap_err();
assert!(err.to_string().contains("dots must separate"));
}
}