miyabi_claudable/
error.rs

1//! Error types for Claudable API client
2
3use miyabi_types::error::{ErrorCode, UnifiedError};
4use std::any::Any;
5use thiserror::Error;
6
7/// Claudable client error types
8#[derive(Debug, Error)]
9pub enum ClaudableError {
10    /// HTTP request failed
11    #[error("HTTP request failed: {0}")]
12    HttpError(#[from] reqwest::Error),
13
14    /// Claudable API returned an error response
15    #[error("Claudable API error {0}: {1}")]
16    ApiError(u16, String),
17
18    /// Failed to parse response
19    #[error("Invalid response format: {0}")]
20    ParseError(String),
21
22    /// IO error during file operations
23    #[error("IO error: {0}")]
24    IoError(#[from] std::io::Error),
25
26    /// npm install failed
27    #[error("npm install failed: {0}")]
28    NpmInstallError(String),
29
30    /// Next.js build failed
31    #[error("Next.js build failed: {0}")]
32    BuildError(String),
33
34    /// Timeout waiting for response
35    #[error("Request timeout after {0}ms")]
36    Timeout(u64),
37
38    /// Configuration error
39    #[error("Configuration error: {0}")]
40    ConfigError(String),
41}
42
43/// Result type alias for Claudable operations
44pub type Result<T> = std::result::Result<T, ClaudableError>;
45
46// ============================================================================
47// UnifiedError Implementation
48// ============================================================================
49
50impl UnifiedError for ClaudableError {
51    fn code(&self) -> ErrorCode {
52        match self {
53            Self::HttpError(_) => ErrorCode::HTTP_ERROR,
54            Self::ApiError(_, _) => ErrorCode::HTTP_ERROR,
55            Self::ParseError(_) => ErrorCode::PARSE_ERROR,
56            Self::IoError(e) => match e.kind() {
57                std::io::ErrorKind::NotFound => ErrorCode::FILE_NOT_FOUND,
58                std::io::ErrorKind::PermissionDenied => ErrorCode::PERMISSION_DENIED,
59                _ => ErrorCode::IO_ERROR,
60            },
61            Self::NpmInstallError(_) => ErrorCode::PROCESS_ERROR,
62            Self::BuildError(_) => ErrorCode::PROCESS_ERROR,
63            Self::Timeout(_) => ErrorCode::TIMEOUT_ERROR,
64            Self::ConfigError(_) => ErrorCode::CONFIG_ERROR,
65        }
66    }
67
68    fn user_message(&self) -> String {
69        match self {
70            Self::HttpError(e) => format!(
71                "Failed to communicate with Claudable API: {}. Please check your network connection and try again.",
72                e
73            ),
74            Self::ApiError(status, msg) => format!(
75                "Claudable API returned error {}: {}. Please check the API documentation or contact support.",
76                status, msg
77            ),
78            Self::Timeout(ms) => format!(
79                "Request to Claudable timed out after {}ms. The service may be experiencing high load. Please try again.",
80                ms
81            ),
82            Self::NpmInstallError(msg) => format!(
83                "Failed to install npm dependencies: {}. Please ensure npm is installed and you have necessary permissions.",
84                msg
85            ),
86            Self::BuildError(msg) => format!(
87                "Next.js build failed: {}. Please check your configuration and dependencies.",
88                msg
89            ),
90            Self::ConfigError(msg) => format!(
91                "Configuration error: {}. Please verify your Claudable settings.",
92                msg
93            ),
94            // Reuse existing thiserror messages for other variants
95            _ => self.to_string(),
96        }
97    }
98
99    fn context(&self) -> Option<&dyn Any> {
100        match self {
101            Self::ApiError(status, _) => Some(status as &dyn Any),
102            Self::Timeout(ms) => Some(ms as &dyn Any),
103            Self::NpmInstallError(msg) => Some(msg as &dyn Any),
104            Self::BuildError(msg) => Some(msg as &dyn Any),
105            _ => None,
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_error_display() {
116        let error = ClaudableError::ApiError(404, "Not Found".to_string());
117        assert_eq!(error.to_string(), "Claudable API error 404: Not Found");
118    }
119
120    #[test]
121    fn test_timeout_error() {
122        let error = ClaudableError::Timeout(30000);
123        assert_eq!(error.to_string(), "Request timeout after 30000ms");
124    }
125
126    #[test]
127    fn test_npm_install_error() {
128        let error = ClaudableError::NpmInstallError("EACCES: permission denied".to_string());
129        assert!(error.to_string().contains("npm install failed"));
130    }
131
132    #[test]
133    fn test_claudable_error_codes() {
134        let error = ClaudableError::ApiError(404, "Not Found".to_string());
135        assert_eq!(error.code(), ErrorCode::HTTP_ERROR);
136
137        let error = ClaudableError::Timeout(30000);
138        assert_eq!(error.code(), ErrorCode::TIMEOUT_ERROR);
139
140        let error = ClaudableError::NpmInstallError("error".to_string());
141        assert_eq!(error.code(), ErrorCode::PROCESS_ERROR);
142
143        let error = ClaudableError::BuildError("error".to_string());
144        assert_eq!(error.code(), ErrorCode::PROCESS_ERROR);
145    }
146
147    #[test]
148    fn test_user_messages() {
149        let error = ClaudableError::ApiError(500, "Internal Server Error".to_string());
150        let msg = error.user_message();
151        assert!(msg.contains("500"));
152        assert!(msg.contains("Internal Server Error"));
153
154        let error = ClaudableError::Timeout(5000);
155        let msg = error.user_message();
156        assert!(msg.contains("5000ms"));
157        assert!(msg.contains("timed out"));
158
159        let error = ClaudableError::NpmInstallError("EACCES".to_string());
160        let msg = error.user_message();
161        assert!(msg.contains("npm dependencies"));
162        assert!(msg.contains("EACCES"));
163    }
164
165    #[test]
166    fn test_context_extraction() {
167        let error = ClaudableError::ApiError(404, "Not Found".to_string());
168        assert!(error.context().is_some());
169
170        let error = ClaudableError::Timeout(30000);
171        assert!(error.context().is_some());
172
173        let error = ClaudableError::NpmInstallError("error".to_string());
174        assert!(error.context().is_some());
175
176        let error = ClaudableError::ParseError("invalid JSON".to_string());
177        assert!(error.context().is_none());
178    }
179}