use std::fmt::{self, Write};
fn format_validation_errors(errors: &[String]) -> String {
if errors.is_empty() {
": unknown error".to_string()
} else if errors.len() == 1 {
format!(": {}", errors[0])
} else {
let mut msg = ":\n".to_string();
for (i, error) in errors.iter().enumerate() {
writeln!(msg, " {}. {}", i + 1, error).expect("writing to String cannot fail");
}
msg
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum ToolError {
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("execution failed: {0}")]
ExecutionFailed(String),
#[error("timeout")]
Timeout,
#[error("tool not found: {0}")]
ToolNotFound(String),
#[error("validation failed{}", format_validation_errors(.0))]
ValidationFailed(Vec<String>),
#[error("execution intercepted: {0}")]
Intercepted(String),
}
impl ToolError {
#[must_use]
pub const fn invalid_input(msg: String) -> Self {
Self::InvalidInput(msg)
}
#[must_use]
pub const fn execution_failed(msg: String) -> Self {
Self::ExecutionFailed(msg)
}
#[must_use]
pub const fn timeout() -> Self {
Self::Timeout
}
#[must_use]
pub fn tool_not_found(name: impl fmt::Display) -> Self {
Self::ToolNotFound(name.to_string())
}
#[must_use]
pub fn validation_failed(msg: impl Into<Vec<String>>) -> Self {
Self::ValidationFailed(msg.into())
}
#[must_use]
pub const fn intercepted(msg: String) -> Self {
Self::Intercepted(msg)
}
#[must_use]
pub const fn is_fatal(&self) -> bool {
matches!(self, Self::Timeout | Self::Intercepted(_))
}
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(self, Self::InvalidInput(_) | Self::ValidationFailed(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_error_display() {
let err = ToolError::InvalidInput("bad input".to_string());
assert_eq!(err.to_string(), "invalid input: bad input");
let err = ToolError::ExecutionFailed("execution error".to_string());
assert_eq!(err.to_string(), "execution failed: execution error");
let err = ToolError::Timeout;
assert_eq!(err.to_string(), "timeout");
let err = ToolError::ToolNotFound("my_tool".to_string());
assert_eq!(err.to_string(), "tool not found: my_tool");
let err = ToolError::ValidationFailed(vec!["validation failed".to_string()]);
assert_eq!(err.to_string(), "validation failed: validation failed");
let err = ToolError::Intercepted("blocked".to_string());
assert_eq!(err.to_string(), "execution intercepted: blocked");
}
#[test]
fn test_tool_error_constructors() {
let err = ToolError::invalid_input("test".to_string());
assert!(matches!(err, ToolError::InvalidInput(_)));
let err = ToolError::execution_failed("test".to_string());
assert!(matches!(err, ToolError::ExecutionFailed(_)));
let err = ToolError::timeout();
assert!(matches!(err, ToolError::Timeout));
let err = ToolError::tool_not_found("search");
assert!(matches!(err, ToolError::ToolNotFound(_)));
let err = ToolError::validation_failed(vec!["test".to_string()]);
assert!(matches!(err, ToolError::ValidationFailed(_)));
let err = ToolError::intercepted("test".to_string());
assert!(matches!(err, ToolError::Intercepted(_)));
}
#[test]
fn test_tool_error_is_fatal() {
assert!(ToolError::Timeout.is_fatal());
assert!(ToolError::Intercepted("test".to_string()).is_fatal());
assert!(!ToolError::InvalidInput("test".to_string()).is_fatal());
assert!(!ToolError::ExecutionFailed("test".to_string()).is_fatal());
}
#[test]
fn test_tool_error_is_retryable() {
assert!(ToolError::InvalidInput("test".to_string()).is_retryable());
assert!(ToolError::ValidationFailed(vec!["test".to_string()]).is_retryable());
assert!(!ToolError::Timeout.is_retryable());
assert!(!ToolError::Intercepted("test".to_string()).is_retryable());
}
#[test]
fn test_tool_error_clone() {
let err = ToolError::InvalidInput("test".to_string());
let cloned = err.clone();
assert_eq!(err.to_string(), cloned.to_string());
}
#[test]
fn test_validation_failed_multiple_errors() {
let errors = vec![
"Missing required field: 'name'".to_string(),
"Field 'age' expected type 'integer', got 'string'".to_string(),
];
let err = ToolError::ValidationFailed(errors);
assert!(matches!(err, ToolError::ValidationFailed(_)));
let display = err.to_string();
assert!(display.contains("validation failed"));
assert!(display.contains("Missing required field: 'name'"));
assert!(display.contains("Field 'age' expected type 'integer', got 'string'"));
}
#[test]
fn test_validation_failed_empty_vec() {
let err = ToolError::ValidationFailed(vec![]);
assert!(err.to_string().contains("validation failed: unknown error"));
}
}