1use std::path::PathBuf;
10use thiserror::Error;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum ErrorCategory {
21 Network,
23 Process,
25 Parsing,
27 Configuration,
29 Validation,
31 Permission,
33 Resource,
35 Internal,
37 External,
39}
40
41impl ErrorCategory {
42 pub fn is_retryable(&self) -> bool {
44 matches!(
45 self,
46 ErrorCategory::Network | ErrorCategory::External | ErrorCategory::Process
47 )
48 }
49
50 pub fn description(&self) -> &'static str {
52 match self {
53 ErrorCategory::Network => "Network connectivity or communication error",
54 ErrorCategory::Process => "CLI process execution error",
55 ErrorCategory::Parsing => "Data parsing or serialization error",
56 ErrorCategory::Configuration => "Configuration or setup error",
57 ErrorCategory::Validation => "Input validation error",
58 ErrorCategory::Permission => "Permission or authentication error",
59 ErrorCategory::Resource => "Resource not found or unavailable",
60 ErrorCategory::Internal => "Internal SDK error",
61 ErrorCategory::External => "External service error",
62 }
63 }
64}
65
66impl std::fmt::Display for ErrorCategory {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 ErrorCategory::Network => write!(f, "network"),
70 ErrorCategory::Process => write!(f, "process"),
71 ErrorCategory::Parsing => write!(f, "parsing"),
72 ErrorCategory::Configuration => write!(f, "configuration"),
73 ErrorCategory::Validation => write!(f, "validation"),
74 ErrorCategory::Permission => write!(f, "permission"),
75 ErrorCategory::Resource => write!(f, "resource"),
76 ErrorCategory::Internal => write!(f, "internal"),
77 ErrorCategory::External => write!(f, "external"),
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum HttpStatus {
88 BadRequest,
90 Unauthorized,
92 Forbidden,
94 NotFound,
96 RequestTimeout,
98 Conflict,
100 UnprocessableEntity,
102 TooManyRequests,
104 InternalServerError,
106 BadGateway,
108 ServiceUnavailable,
110 GatewayTimeout,
112}
113
114impl HttpStatus {
115 pub fn code(&self) -> u16 {
117 match self {
118 HttpStatus::BadRequest => 400,
119 HttpStatus::Unauthorized => 401,
120 HttpStatus::Forbidden => 403,
121 HttpStatus::NotFound => 404,
122 HttpStatus::RequestTimeout => 408,
123 HttpStatus::Conflict => 409,
124 HttpStatus::UnprocessableEntity => 422,
125 HttpStatus::TooManyRequests => 429,
126 HttpStatus::InternalServerError => 500,
127 HttpStatus::BadGateway => 502,
128 HttpStatus::ServiceUnavailable => 503,
129 HttpStatus::GatewayTimeout => 504,
130 }
131 }
132}
133
134impl From<HttpStatus> for u16 {
135 fn from(status: HttpStatus) -> u16 {
136 status.code()
137 }
138}
139
140#[derive(Debug, Error)]
142pub enum ClaudeError {
143 #[error("CLI connection error: {0}")]
145 Connection(#[from] ConnectionError),
146
147 #[error("Process error: {0}")]
149 Process(#[from] ProcessError),
150
151 #[error("JSON decode error: {0}")]
153 JsonDecode(#[from] JsonDecodeError),
154
155 #[error("Message parse error: {0}")]
157 MessageParse(#[from] MessageParseError),
158
159 #[error("Transport error: {0}")]
161 Transport(String),
162
163 #[error("Control protocol error: {0}")]
165 ControlProtocol(String),
166
167 #[error("Invalid configuration: {0}")]
169 InvalidConfig(String),
170
171 #[error("CLI not found: {0}")]
173 CliNotFound(#[from] CliNotFoundError),
174
175 #[error("Image validation error: {0}")]
177 ImageValidation(#[from] ImageValidationError),
178
179 #[error("IO error: {0}")]
181 Io(#[from] std::io::Error),
182
183 #[error(transparent)]
185 Other(#[from] anyhow::Error),
186
187 #[error("Not found: {0}")]
189 NotFound(String),
190
191 #[error("Invalid input: {0}")]
193 InvalidInput(String),
194
195 #[error("Internal error: {0}")]
197 InternalError(String),
198}
199
200#[derive(Debug, Error)]
202#[error("CLI not found: {message}")]
203pub struct CliNotFoundError {
204 pub message: String,
206 pub cli_path: Option<PathBuf>,
208}
209
210impl CliNotFoundError {
211 pub fn new(message: impl Into<String>, cli_path: Option<PathBuf>) -> Self {
213 Self {
214 message: message.into(),
215 cli_path,
216 }
217 }
218}
219
220#[derive(Debug, Error)]
222#[error("Connection error: {message}")]
223pub struct ConnectionError {
224 pub message: String,
226}
227
228impl ConnectionError {
229 pub fn new(message: impl Into<String>) -> Self {
231 Self {
232 message: message.into(),
233 }
234 }
235}
236
237#[derive(Debug, Error)]
239#[error("Process error (exit code {exit_code:?}): {message}")]
240pub struct ProcessError {
241 pub message: String,
243 pub exit_code: Option<i32>,
245 pub stderr: Option<String>,
247}
248
249impl ProcessError {
250 pub fn new(message: impl Into<String>, exit_code: Option<i32>, stderr: Option<String>) -> Self {
252 Self {
253 message: message.into(),
254 exit_code,
255 stderr,
256 }
257 }
258}
259
260#[derive(Debug, Error)]
262#[error("JSON decode error: {message}")]
263pub struct JsonDecodeError {
264 pub message: String,
266 pub line: String,
268}
269
270impl JsonDecodeError {
271 pub fn new(message: impl Into<String>, line: impl Into<String>) -> Self {
273 Self {
274 message: message.into(),
275 line: line.into(),
276 }
277 }
278}
279
280#[derive(Debug, Error)]
282#[error("Message parse error: {message}")]
283pub struct MessageParseError {
284 pub message: String,
286 pub data: Option<serde_json::Value>,
288}
289
290impl MessageParseError {
291 pub fn new(message: impl Into<String>, data: Option<serde_json::Value>) -> Self {
293 Self {
294 message: message.into(),
295 data,
296 }
297 }
298}
299
300#[derive(Debug, Error)]
302#[error("Image validation error: {message}")]
303pub struct ImageValidationError {
304 pub message: String,
306}
307
308impl ImageValidationError {
309 pub fn new(message: impl Into<String>) -> Self {
311 Self {
312 message: message.into(),
313 }
314 }
315}
316
317pub type Result<T> = std::result::Result<T, ClaudeError>;
319
320impl ClaudeError {
321 pub fn category(&self) -> ErrorCategory {
328 match self {
329 ClaudeError::Connection(_) => ErrorCategory::Network,
330 ClaudeError::Process(_) => ErrorCategory::Process,
331 ClaudeError::JsonDecode(_) => ErrorCategory::Parsing,
332 ClaudeError::MessageParse(_) => ErrorCategory::Parsing,
333 ClaudeError::Transport(_) => ErrorCategory::Network,
334 ClaudeError::ControlProtocol(_) => ErrorCategory::Internal,
335 ClaudeError::InvalidConfig(_) => ErrorCategory::Configuration,
336 ClaudeError::CliNotFound(_) => ErrorCategory::Configuration,
337 ClaudeError::ImageValidation(_) => ErrorCategory::Validation,
338 ClaudeError::Io(_) => ErrorCategory::Internal,
339 ClaudeError::Other(_) => ErrorCategory::Internal,
340 ClaudeError::NotFound(_) => ErrorCategory::Resource,
341 ClaudeError::InvalidInput(_) => ErrorCategory::Validation,
342 ClaudeError::InternalError(_) => ErrorCategory::Internal,
343 }
344 }
345
346 pub fn error_code(&self) -> &'static str {
355 match self {
356 ClaudeError::Connection(_) => "ENET001",
357 ClaudeError::Process(_) => "EPROC001",
358 ClaudeError::JsonDecode(_) => "EPARSE001",
359 ClaudeError::MessageParse(_) => "EPARSE002",
360 ClaudeError::Transport(_) => "ENET002",
361 ClaudeError::ControlProtocol(_) => "EINT001",
362 ClaudeError::InvalidConfig(_) => "ECFG001",
363 ClaudeError::CliNotFound(_) => "ECFG002",
364 ClaudeError::ImageValidation(_) => "EVAL001",
365 ClaudeError::Io(_) => "EINT002",
366 ClaudeError::Other(_) => "EINT003",
367 ClaudeError::NotFound(_) => "ERES001",
368 ClaudeError::InvalidInput(_) => "EVAL002",
369 ClaudeError::InternalError(_) => "EINT004",
370 }
371 }
372
373 pub fn is_retryable(&self) -> bool {
381 self.category().is_retryable()
382 }
383
384 pub fn http_status(&self) -> HttpStatus {
388 match self {
389 ClaudeError::Connection(_) => HttpStatus::ServiceUnavailable,
390 ClaudeError::Process(_) => HttpStatus::BadGateway,
391 ClaudeError::JsonDecode(_) => HttpStatus::UnprocessableEntity,
392 ClaudeError::MessageParse(_) => HttpStatus::UnprocessableEntity,
393 ClaudeError::Transport(_) => HttpStatus::ServiceUnavailable,
394 ClaudeError::ControlProtocol(_) => HttpStatus::InternalServerError,
395 ClaudeError::InvalidConfig(_) => HttpStatus::InternalServerError,
396 ClaudeError::CliNotFound(_) => HttpStatus::InternalServerError,
397 ClaudeError::ImageValidation(_) => HttpStatus::BadRequest,
398 ClaudeError::Io(_) => HttpStatus::InternalServerError,
399 ClaudeError::Other(_) => HttpStatus::InternalServerError,
400 ClaudeError::NotFound(_) => HttpStatus::NotFound,
401 ClaudeError::InvalidInput(_) => HttpStatus::BadRequest,
402 ClaudeError::InternalError(_) => HttpStatus::InternalServerError,
403 }
404 }
405
406 pub fn to_error_context(&self) -> ErrorContext {
414 ErrorContext {
415 code: self.error_code().to_string(),
416 category: self.category(),
417 message: self.to_string(),
418 retryable: self.is_retryable(),
419 http_status: self.http_status().code(),
420 }
421 }
422}
423
424#[derive(Debug, Clone)]
426pub struct ErrorContext {
427 pub code: String,
429 pub category: ErrorCategory,
431 pub message: String,
433 pub retryable: bool,
435 pub http_status: u16,
437}
438
439impl std::fmt::Display for ErrorContext {
440 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441 write!(
442 f,
443 "[{}] [{}] {} (retryable: {}, http: {})",
444 self.code, self.category, self.message, self.retryable, self.http_status
445 )
446 }
447}
448
449impl std::fmt::Display for HttpStatus {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 match self {
452 HttpStatus::BadRequest => write!(f, "400 Bad Request"),
453 HttpStatus::Unauthorized => write!(f, "401 Unauthorized"),
454 HttpStatus::Forbidden => write!(f, "403 Forbidden"),
455 HttpStatus::NotFound => write!(f, "404 Not Found"),
456 HttpStatus::RequestTimeout => write!(f, "408 Request Timeout"),
457 HttpStatus::Conflict => write!(f, "409 Conflict"),
458 HttpStatus::UnprocessableEntity => write!(f, "422 Unprocessable Entity"),
459 HttpStatus::TooManyRequests => write!(f, "429 Too Many Requests"),
460 HttpStatus::InternalServerError => write!(f, "500 Internal Server Error"),
461 HttpStatus::BadGateway => write!(f, "502 Bad Gateway"),
462 HttpStatus::ServiceUnavailable => write!(f, "503 Service Unavailable"),
463 HttpStatus::GatewayTimeout => write!(f, "504 Gateway Timeout"),
464 }
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn test_error_categories() {
474 let error = ClaudeError::Connection(ConnectionError::new("test"));
475 assert_eq!(error.category(), ErrorCategory::Network);
476 assert!(error.is_retryable());
477 assert_eq!(error.error_code(), "ENET001");
478
479 let error = ClaudeError::InvalidConfig("test".to_string());
480 assert_eq!(error.category(), ErrorCategory::Configuration);
481 assert!(!error.is_retryable());
482 assert_eq!(error.error_code(), "ECFG001");
483 }
484
485 #[test]
486 fn test_http_status_mapping() {
487 let error = ClaudeError::NotFound("test".to_string());
488 assert_eq!(error.http_status(), HttpStatus::NotFound);
489 assert_eq!(error.http_status().code(), 404);
490
491 let error = ClaudeError::InvalidInput("test".to_string());
492 assert_eq!(error.http_status(), HttpStatus::BadRequest);
493 assert_eq!(error.http_status().code(), 400);
494 }
495
496 #[test]
497 fn test_error_context() {
498 let error = ClaudeError::Connection(ConnectionError::new("connection failed"));
499 let ctx = error.to_error_context();
500 assert_eq!(ctx.code, "ENET001");
501 assert_eq!(ctx.category, ErrorCategory::Network);
502 assert!(ctx.retryable);
503 assert_eq!(ctx.http_status, 503);
504 }
505
506 #[test]
507 fn test_category_display() {
508 assert_eq!(ErrorCategory::Network.to_string(), "network");
509 assert_eq!(ErrorCategory::Process.to_string(), "process");
510 assert_eq!(ErrorCategory::Parsing.to_string(), "parsing");
511 }
512
513 #[test]
514 fn test_category_description() {
515 assert!(!ErrorCategory::Network.description().is_empty());
516 assert!(ErrorCategory::Network.is_retryable());
517 assert!(!ErrorCategory::Configuration.is_retryable());
518 }
519}