use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCategory {
Network,
Process,
Parsing,
Configuration,
Validation,
Permission,
Resource,
Internal,
External,
}
impl ErrorCategory {
pub fn is_retryable(&self) -> bool {
matches!(
self,
ErrorCategory::Network | ErrorCategory::External | ErrorCategory::Process
)
}
pub fn description(&self) -> &'static str {
match self {
ErrorCategory::Network => "Network connectivity or communication error",
ErrorCategory::Process => "CLI process execution error",
ErrorCategory::Parsing => "Data parsing or serialization error",
ErrorCategory::Configuration => "Configuration or setup error",
ErrorCategory::Validation => "Input validation error",
ErrorCategory::Permission => "Permission or authentication error",
ErrorCategory::Resource => "Resource not found or unavailable",
ErrorCategory::Internal => "Internal SDK error",
ErrorCategory::External => "External service error",
}
}
}
impl std::fmt::Display for ErrorCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ErrorCategory::Network => write!(f, "network"),
ErrorCategory::Process => write!(f, "process"),
ErrorCategory::Parsing => write!(f, "parsing"),
ErrorCategory::Configuration => write!(f, "configuration"),
ErrorCategory::Validation => write!(f, "validation"),
ErrorCategory::Permission => write!(f, "permission"),
ErrorCategory::Resource => write!(f, "resource"),
ErrorCategory::Internal => write!(f, "internal"),
ErrorCategory::External => write!(f, "external"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpStatus {
BadRequest,
Unauthorized,
Forbidden,
NotFound,
RequestTimeout,
Conflict,
UnprocessableEntity,
TooManyRequests,
InternalServerError,
BadGateway,
ServiceUnavailable,
GatewayTimeout,
}
impl HttpStatus {
pub fn code(&self) -> u16 {
match self {
HttpStatus::BadRequest => 400,
HttpStatus::Unauthorized => 401,
HttpStatus::Forbidden => 403,
HttpStatus::NotFound => 404,
HttpStatus::RequestTimeout => 408,
HttpStatus::Conflict => 409,
HttpStatus::UnprocessableEntity => 422,
HttpStatus::TooManyRequests => 429,
HttpStatus::InternalServerError => 500,
HttpStatus::BadGateway => 502,
HttpStatus::ServiceUnavailable => 503,
HttpStatus::GatewayTimeout => 504,
}
}
}
impl From<HttpStatus> for u16 {
fn from(status: HttpStatus) -> u16 {
status.code()
}
}
#[derive(Debug, Error)]
pub enum ClaudeError {
#[error("CLI connection error: {0}")]
Connection(#[from] ConnectionError),
#[error("Process error: {0}")]
Process(#[from] ProcessError),
#[error("JSON decode error: {0}")]
JsonDecode(#[from] JsonDecodeError),
#[error("Message parse error: {0}")]
MessageParse(#[from] MessageParseError),
#[error("Transport error: {0}")]
Transport(String),
#[error("Control protocol error: {0}")]
ControlProtocol(String),
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error("CLI not found: {0}")]
CliNotFound(#[from] CliNotFoundError),
#[error("Image validation error: {0}")]
ImageValidation(#[from] ImageValidationError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
#[error("Not found: {0}")]
NotFound(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Internal error: {0}")]
InternalError(String),
}
#[derive(Debug, Error)]
#[error("CLI not found: {message}")]
pub struct CliNotFoundError {
pub message: String,
pub cli_path: Option<PathBuf>,
}
impl CliNotFoundError {
pub fn new(message: impl Into<String>, cli_path: Option<PathBuf>) -> Self {
Self {
message: message.into(),
cli_path,
}
}
}
#[derive(Debug, Error)]
#[error("Connection error: {message}")]
pub struct ConnectionError {
pub message: String,
}
impl ConnectionError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
#[derive(Debug, Error)]
#[error("Process error (exit code {exit_code:?}): {message}")]
pub struct ProcessError {
pub message: String,
pub exit_code: Option<i32>,
pub stderr: Option<String>,
}
impl ProcessError {
pub fn new(message: impl Into<String>, exit_code: Option<i32>, stderr: Option<String>) -> Self {
Self {
message: message.into(),
exit_code,
stderr,
}
}
}
#[derive(Debug, Error)]
#[error("JSON decode error: {message}")]
pub struct JsonDecodeError {
pub message: String,
pub line: String,
}
impl JsonDecodeError {
pub fn new(message: impl Into<String>, line: impl Into<String>) -> Self {
Self {
message: message.into(),
line: line.into(),
}
}
}
#[derive(Debug, Error)]
#[error("Message parse error: {message}")]
pub struct MessageParseError {
pub message: String,
pub data: Option<serde_json::Value>,
}
impl MessageParseError {
pub fn new(message: impl Into<String>, data: Option<serde_json::Value>) -> Self {
Self {
message: message.into(),
data,
}
}
}
#[derive(Debug, Error)]
#[error("Image validation error: {message}")]
pub struct ImageValidationError {
pub message: String,
}
impl ImageValidationError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
pub type Result<T> = std::result::Result<T, ClaudeError>;
impl ClaudeError {
pub fn category(&self) -> ErrorCategory {
match self {
ClaudeError::Connection(_) => ErrorCategory::Network,
ClaudeError::Process(_) => ErrorCategory::Process,
ClaudeError::JsonDecode(_) => ErrorCategory::Parsing,
ClaudeError::MessageParse(_) => ErrorCategory::Parsing,
ClaudeError::Transport(_) => ErrorCategory::Network,
ClaudeError::ControlProtocol(_) => ErrorCategory::Internal,
ClaudeError::InvalidConfig(_) => ErrorCategory::Configuration,
ClaudeError::CliNotFound(_) => ErrorCategory::Configuration,
ClaudeError::ImageValidation(_) => ErrorCategory::Validation,
ClaudeError::Io(_) => ErrorCategory::Internal,
ClaudeError::Other(_) => ErrorCategory::Internal,
ClaudeError::NotFound(_) => ErrorCategory::Resource,
ClaudeError::InvalidInput(_) => ErrorCategory::Validation,
ClaudeError::InternalError(_) => ErrorCategory::Internal,
}
}
pub fn error_code(&self) -> &'static str {
match self {
ClaudeError::Connection(_) => "ENET001",
ClaudeError::Process(_) => "EPROC001",
ClaudeError::JsonDecode(_) => "EPARSE001",
ClaudeError::MessageParse(_) => "EPARSE002",
ClaudeError::Transport(_) => "ENET002",
ClaudeError::ControlProtocol(_) => "EINT001",
ClaudeError::InvalidConfig(_) => "ECFG001",
ClaudeError::CliNotFound(_) => "ECFG002",
ClaudeError::ImageValidation(_) => "EVAL001",
ClaudeError::Io(_) => "EINT002",
ClaudeError::Other(_) => "EINT003",
ClaudeError::NotFound(_) => "ERES001",
ClaudeError::InvalidInput(_) => "EVAL002",
ClaudeError::InternalError(_) => "EINT004",
}
}
pub fn is_retryable(&self) -> bool {
self.category().is_retryable()
}
pub fn http_status(&self) -> HttpStatus {
match self {
ClaudeError::Connection(_) => HttpStatus::ServiceUnavailable,
ClaudeError::Process(_) => HttpStatus::BadGateway,
ClaudeError::JsonDecode(_) => HttpStatus::UnprocessableEntity,
ClaudeError::MessageParse(_) => HttpStatus::UnprocessableEntity,
ClaudeError::Transport(_) => HttpStatus::ServiceUnavailable,
ClaudeError::ControlProtocol(_) => HttpStatus::InternalServerError,
ClaudeError::InvalidConfig(_) => HttpStatus::InternalServerError,
ClaudeError::CliNotFound(_) => HttpStatus::InternalServerError,
ClaudeError::ImageValidation(_) => HttpStatus::BadRequest,
ClaudeError::Io(_) => HttpStatus::InternalServerError,
ClaudeError::Other(_) => HttpStatus::InternalServerError,
ClaudeError::NotFound(_) => HttpStatus::NotFound,
ClaudeError::InvalidInput(_) => HttpStatus::BadRequest,
ClaudeError::InternalError(_) => HttpStatus::InternalServerError,
}
}
pub fn to_error_context(&self) -> ErrorContext {
ErrorContext {
code: self.error_code().to_string(),
category: self.category(),
message: self.to_string(),
retryable: self.is_retryable(),
http_status: self.http_status().code(),
}
}
}
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub code: String,
pub category: ErrorCategory,
pub message: String,
pub retryable: bool,
pub http_status: u16,
}
impl std::fmt::Display for ErrorContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[{}] [{}] {} (retryable: {}, http: {})",
self.code, self.category, self.message, self.retryable, self.http_status
)
}
}
impl std::fmt::Display for HttpStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HttpStatus::BadRequest => write!(f, "400 Bad Request"),
HttpStatus::Unauthorized => write!(f, "401 Unauthorized"),
HttpStatus::Forbidden => write!(f, "403 Forbidden"),
HttpStatus::NotFound => write!(f, "404 Not Found"),
HttpStatus::RequestTimeout => write!(f, "408 Request Timeout"),
HttpStatus::Conflict => write!(f, "409 Conflict"),
HttpStatus::UnprocessableEntity => write!(f, "422 Unprocessable Entity"),
HttpStatus::TooManyRequests => write!(f, "429 Too Many Requests"),
HttpStatus::InternalServerError => write!(f, "500 Internal Server Error"),
HttpStatus::BadGateway => write!(f, "502 Bad Gateway"),
HttpStatus::ServiceUnavailable => write!(f, "503 Service Unavailable"),
HttpStatus::GatewayTimeout => write!(f, "504 Gateway Timeout"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_categories() {
let error = ClaudeError::Connection(ConnectionError::new("test"));
assert_eq!(error.category(), ErrorCategory::Network);
assert!(error.is_retryable());
assert_eq!(error.error_code(), "ENET001");
let error = ClaudeError::InvalidConfig("test".to_string());
assert_eq!(error.category(), ErrorCategory::Configuration);
assert!(!error.is_retryable());
assert_eq!(error.error_code(), "ECFG001");
}
#[test]
fn test_http_status_mapping() {
let error = ClaudeError::NotFound("test".to_string());
assert_eq!(error.http_status(), HttpStatus::NotFound);
assert_eq!(error.http_status().code(), 404);
let error = ClaudeError::InvalidInput("test".to_string());
assert_eq!(error.http_status(), HttpStatus::BadRequest);
assert_eq!(error.http_status().code(), 400);
}
#[test]
fn test_error_context() {
let error = ClaudeError::Connection(ConnectionError::new("connection failed"));
let ctx = error.to_error_context();
assert_eq!(ctx.code, "ENET001");
assert_eq!(ctx.category, ErrorCategory::Network);
assert!(ctx.retryable);
assert_eq!(ctx.http_status, 503);
}
#[test]
fn test_category_display() {
assert_eq!(ErrorCategory::Network.to_string(), "network");
assert_eq!(ErrorCategory::Process.to_string(), "process");
assert_eq!(ErrorCategory::Parsing.to_string(), "parsing");
}
#[test]
fn test_category_description() {
assert!(!ErrorCategory::Network.description().is_empty());
assert!(ErrorCategory::Network.is_retryable());
assert!(!ErrorCategory::Configuration.is_retryable());
}
}