1use std::io;
6use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, ScipixError>;
10
11#[derive(Debug, Error)]
13pub enum ScipixError {
14 #[error("Image error: {0}")]
16 Image(String),
17
18 #[error("Model error: {0}")]
20 Model(String),
21
22 #[error("OCR error: {0}")]
24 Ocr(String),
25
26 #[error("LaTeX error: {0}")]
28 LaTeX(String),
29
30 #[error("Configuration error: {0}")]
32 Config(String),
33
34 #[error("I/O error: {0}")]
36 Io(#[from] io::Error),
37
38 #[error("Serialization error: {0}")]
40 Serialization(String),
41
42 #[error("Invalid input: {0}")]
44 InvalidInput(String),
45
46 #[error("Timeout: operation took longer than {0}s")]
48 Timeout(u64),
49
50 #[error("Not found: {0}")]
52 NotFound(String),
53
54 #[error("Authentication error: {0}")]
56 Auth(String),
57
58 #[error("Rate limit exceeded: {0}")]
60 RateLimit(String),
61
62 #[error("Internal error: {0}")]
64 Internal(String),
65}
66
67impl ScipixError {
68 pub fn is_retryable(&self) -> bool {
86 match self {
87 ScipixError::Timeout(_) => true,
89 ScipixError::RateLimit(_) => true,
90 ScipixError::Io(_) => true,
91 ScipixError::Internal(_) => true,
92
93 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 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 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
156impl From<serde_json::Error> for ScipixError {
158 fn from(err: serde_json::Error) -> Self {
159 ScipixError::Serialization(err.to_string())
160 }
161}
162
163impl From<toml::de::Error> for ScipixError {
165 fn from(err: toml::de::Error) -> Self {
166 ScipixError::Config(err.to_string())
167 }
168}
169
170impl 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}