use thiserror::Error;
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum ToolError {
#[error("Invalid parameters: {message}")]
InvalidParams {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Tool not found: {0}")]
NotFound(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Forbidden: {0}")]
Forbidden(String),
#[error("Timeout after {timeout_ms}ms: {message}")]
Timeout {
timeout_ms: u64,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Internal error: {message}")]
Internal {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("[{code}] {message}")]
Custom {
code: String,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
impl ToolError {
pub fn invalid_params(message: impl Into<String>) -> Self {
Self::InvalidParams {
message: message.into(),
source: None,
}
}
pub fn invalid_params_with_source(
message: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::InvalidParams {
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn not_found(name: impl Into<String>) -> Self {
Self::NotFound(name.into())
}
pub fn unauthorized(message: impl Into<String>) -> Self {
Self::Unauthorized(message.into())
}
pub fn forbidden(message: impl Into<String>) -> Self {
Self::Forbidden(message.into())
}
pub fn timeout(timeout_ms: u64, message: impl Into<String>) -> Self {
Self::Timeout {
timeout_ms,
message: message.into(),
source: None,
}
}
pub fn timeout_with_source(
timeout_ms: u64,
message: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Timeout {
timeout_ms,
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal {
message: message.into(),
source: None,
}
}
pub fn internal_with_source(
message: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Internal {
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn custom(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::Custom {
code: code.into(),
message: message.into(),
source: None,
}
}
pub fn custom_with_source(
code: impl Into<String>,
message: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Custom {
code: code.into(),
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn code(&self) -> &str {
match self {
Self::InvalidParams { .. } => "INVALID_PARAMS",
Self::NotFound(_) => "NOT_FOUND",
Self::Unauthorized(_) => "UNAUTHORIZED",
Self::Forbidden(_) => "FORBIDDEN",
Self::Timeout { .. } => "TIMEOUT",
Self::Internal { .. } => "INTERNAL_ERROR",
Self::Custom { code, .. } => code,
Self::Json(_) => "JSON_ERROR",
Self::Io(_) => "IO_ERROR",
}
}
pub fn has_source(&self) -> bool {
match self {
Self::InvalidParams { source, .. } => source.is_some(),
Self::Timeout { source, .. } => source.is_some(),
Self::Internal { source, .. } => source.is_some(),
Self::Custom { source, .. } => source.is_some(),
_ => false,
}
}
}
impl From<String> for ToolError {
fn from(message: String) -> Self {
Self::internal(message)
}
}
impl From<&str> for ToolError {
fn from(message: &str) -> Self {
Self::internal(message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = ToolError::internal("Test error");
assert_eq!(err.code(), "INTERNAL_ERROR");
}
#[test]
fn test_error_with_source() {
let source = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err = ToolError::internal_with_source("Failed to read file", source);
assert!(err.has_source());
}
#[test]
fn test_predefined_errors() {
let err = ToolError::invalid_params("Parameter error");
assert_eq!(err.code(), "INVALID_PARAMS");
let err = ToolError::not_found("shell");
assert_eq!(err.code(), "NOT_FOUND");
let err = ToolError::unauthorized("Not logged in");
assert_eq!(err.code(), "UNAUTHORIZED");
let err = ToolError::forbidden("No permission");
assert_eq!(err.code(), "FORBIDDEN");
let err = ToolError::timeout(5000, "Operation timed out");
assert_eq!(err.code(), "TIMEOUT");
}
#[test]
fn test_custom_error() {
let err = ToolError::custom("E001", "Custom error");
assert_eq!(err.code(), "E001");
assert_eq!(err.to_string(), "[E001] Custom error");
}
#[test]
fn test_error_from_string() {
let err: ToolError = "Error message".into();
assert_eq!(err.code(), "INTERNAL_ERROR");
}
#[test]
fn test_error_from_json() {
let json_err = serde_json::from_str::<i32>("invalid");
assert!(json_err.is_err());
let tool_err: ToolError = json_err.unwrap_err().into();
assert_eq!(tool_err.code(), "JSON_ERROR");
}
#[test]
fn test_error_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let tool_err: ToolError = io_err.into();
assert_eq!(tool_err.code(), "IO_ERROR");
}
#[test]
fn test_error_display() {
let err = ToolError::invalid_params("Parameter error");
assert_eq!(format!("{}", err), "Invalid parameters: Parameter error");
}
}