ruvector_scipix/
error.rs

1//! Error types for Ruvector-Scipix
2//!
3//! Comprehensive error handling with context, HTTP status mapping, and retry logic.
4
5use std::io;
6use thiserror::Error;
7
8/// Result type alias for Scipix operations
9pub type Result<T> = std::result::Result<T, ScipixError>;
10
11/// Comprehensive error types for all Scipix operations
12#[derive(Debug, Error)]
13pub enum ScipixError {
14    /// Image loading or processing error
15    #[error("Image error: {0}")]
16    Image(String),
17
18    /// Machine learning model error
19    #[error("Model error: {0}")]
20    Model(String),
21
22    /// OCR processing error
23    #[error("OCR error: {0}")]
24    Ocr(String),
25
26    /// LaTeX generation or parsing error
27    #[error("LaTeX error: {0}")]
28    LaTeX(String),
29
30    /// Configuration error
31    #[error("Configuration error: {0}")]
32    Config(String),
33
34    /// I/O error
35    #[error("I/O error: {0}")]
36    Io(#[from] io::Error),
37
38    /// Serialization/deserialization error
39    #[error("Serialization error: {0}")]
40    Serialization(String),
41
42    /// Invalid input error
43    #[error("Invalid input: {0}")]
44    InvalidInput(String),
45
46    /// Operation timeout
47    #[error("Timeout: operation took longer than {0}s")]
48    Timeout(u64),
49
50    /// Resource not found
51    #[error("Not found: {0}")]
52    NotFound(String),
53
54    /// Authentication error
55    #[error("Authentication error: {0}")]
56    Auth(String),
57
58    /// Rate limit exceeded
59    #[error("Rate limit exceeded: {0}")]
60    RateLimit(String),
61
62    /// Internal error
63    #[error("Internal error: {0}")]
64    Internal(String),
65}
66
67impl ScipixError {
68    /// Check if the error is retryable
69    ///
70    /// # Returns
71    ///
72    /// `true` if the operation should be retried, `false` otherwise
73    ///
74    /// # Examples
75    ///
76    /// ```rust
77    /// use ruvector_scipix::ScipixError;
78    ///
79    /// let timeout_error = ScipixError::Timeout(30);
80    /// assert!(timeout_error.is_retryable());
81    ///
82    /// let config_error = ScipixError::Config("Invalid parameter".to_string());
83    /// assert!(!config_error.is_retryable());
84    /// ```
85    pub fn is_retryable(&self) -> bool {
86        match self {
87            // Retryable errors
88            ScipixError::Timeout(_) => true,
89            ScipixError::RateLimit(_) => true,
90            ScipixError::Io(_) => true,
91            ScipixError::Internal(_) => true,
92
93            // Non-retryable errors
94            ScipixError::Image(_) => false,
95            ScipixError::Model(_) => false,
96            ScipixError::Ocr(_) => false,
97            ScipixError::LaTeX(_) => false,
98            ScipixError::Config(_) => false,
99            ScipixError::Serialization(_) => false,
100            ScipixError::InvalidInput(_) => false,
101            ScipixError::NotFound(_) => false,
102            ScipixError::Auth(_) => false,
103        }
104    }
105
106    /// Map error to HTTP status code
107    ///
108    /// # Returns
109    ///
110    /// HTTP status code representing the error type
111    ///
112    /// # Examples
113    ///
114    /// ```rust
115    /// use ruvector_scipix::ScipixError;
116    ///
117    /// let auth_error = ScipixError::Auth("Invalid token".to_string());
118    /// assert_eq!(auth_error.status_code(), 401);
119    ///
120    /// let not_found = ScipixError::NotFound("Model not found".to_string());
121    /// assert_eq!(not_found.status_code(), 404);
122    /// ```
123    pub fn status_code(&self) -> u16 {
124        match self {
125            ScipixError::Auth(_) => 401,
126            ScipixError::NotFound(_) => 404,
127            ScipixError::InvalidInput(_) => 400,
128            ScipixError::RateLimit(_) => 429,
129            ScipixError::Timeout(_) => 408,
130            ScipixError::Config(_) => 400,
131            ScipixError::Internal(_) => 500,
132            _ => 500,
133        }
134    }
135
136    /// Get error category for logging and metrics
137    pub fn category(&self) -> &'static str {
138        match self {
139            ScipixError::Image(_) => "image",
140            ScipixError::Model(_) => "model",
141            ScipixError::Ocr(_) => "ocr",
142            ScipixError::LaTeX(_) => "latex",
143            ScipixError::Config(_) => "config",
144            ScipixError::Io(_) => "io",
145            ScipixError::Serialization(_) => "serialization",
146            ScipixError::InvalidInput(_) => "invalid_input",
147            ScipixError::Timeout(_) => "timeout",
148            ScipixError::NotFound(_) => "not_found",
149            ScipixError::Auth(_) => "auth",
150            ScipixError::RateLimit(_) => "rate_limit",
151            ScipixError::Internal(_) => "internal",
152        }
153    }
154}
155
156// Conversion from serde_json::Error
157impl From<serde_json::Error> for ScipixError {
158    fn from(err: serde_json::Error) -> Self {
159        ScipixError::Serialization(err.to_string())
160    }
161}
162
163// Conversion from toml::de::Error
164impl From<toml::de::Error> for ScipixError {
165    fn from(err: toml::de::Error) -> Self {
166        ScipixError::Config(err.to_string())
167    }
168}
169
170// Conversion from toml::ser::Error
171impl From<toml::ser::Error> for ScipixError {
172    fn from(err: toml::ser::Error) -> Self {
173        ScipixError::Serialization(err.to_string())
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_error_display() {
183        let err = ScipixError::Image("Failed to load".to_string());
184        assert_eq!(err.to_string(), "Image error: Failed to load");
185    }
186
187    #[test]
188    fn test_is_retryable() {
189        assert!(ScipixError::Timeout(30).is_retryable());
190        assert!(ScipixError::RateLimit("Exceeded".to_string()).is_retryable());
191        assert!(!ScipixError::Config("Invalid".to_string()).is_retryable());
192        assert!(!ScipixError::Auth("Unauthorized".to_string()).is_retryable());
193    }
194
195    #[test]
196    fn test_status_codes() {
197        assert_eq!(ScipixError::Auth("".to_string()).status_code(), 401);
198        assert_eq!(ScipixError::NotFound("".to_string()).status_code(), 404);
199        assert_eq!(ScipixError::InvalidInput("".to_string()).status_code(), 400);
200        assert_eq!(ScipixError::RateLimit("".to_string()).status_code(), 429);
201        assert_eq!(ScipixError::Timeout(0).status_code(), 408);
202        assert_eq!(ScipixError::Internal("".to_string()).status_code(), 500);
203    }
204
205    #[test]
206    fn test_category() {
207        assert_eq!(ScipixError::Image("".to_string()).category(), "image");
208        assert_eq!(ScipixError::Model("".to_string()).category(), "model");
209        assert_eq!(ScipixError::Ocr("".to_string()).category(), "ocr");
210        assert_eq!(ScipixError::LaTeX("".to_string()).category(), "latex");
211        assert_eq!(ScipixError::Config("".to_string()).category(), "config");
212        assert_eq!(ScipixError::Auth("".to_string()).category(), "auth");
213    }
214
215    #[test]
216    fn test_from_io_error() {
217        let io_err = io::Error::new(io::ErrorKind::NotFound, "File not found");
218        let scipix_err: ScipixError = io_err.into();
219        assert!(matches!(scipix_err, ScipixError::Io(_)));
220    }
221
222    #[test]
223    fn test_from_json_error() {
224        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
225        let scipix_err: ScipixError = json_err.into();
226        assert!(matches!(scipix_err, ScipixError::Serialization(_)));
227    }
228}