1use std::fmt;
7
8use thiserror::Error;
9
10#[derive(Error, Debug)]
12pub enum Error {
13 #[error("Configuration error: {0}")]
15 Configuration(String),
16
17 #[error("Validation error: {0}")]
19 Validation(String),
20
21 #[error("Pipeline error: {0}")]
23 Pipeline(String),
24
25 #[error("Context error: {0}")]
27 Context(String),
28
29 #[error("Plugin error: {0}")]
31 Plugin(String),
32
33 #[error("AI provider error: {0}")]
35 Provider(String),
36
37 #[error("Network error: {0}")]
39 Network(String),
40
41 #[error("Operation timed out after {0:?}")]
43 Timeout(std::time::Duration),
44
45 #[error("Rate limit exceeded")]
47 RateLimit,
48
49 #[error("Authentication failed: {0}")]
51 Authentication(String),
52
53 #[error("Authorization failed: {0}")]
55 Authorization(String),
56
57 #[error("Resource not found: {0}")]
59 NotFound(String),
60
61 #[error("Invalid input: {0}")]
63 InvalidInput(String),
64
65 #[error("Serialization error: {0}")]
67 Serialization(String),
68
69 #[error("Database error: {0}")]
71 Database(String),
72
73 #[error("Cache error: {0}")]
75 Cache(String),
76
77 #[error("Initialization failed: {0}")]
79 Initialization(String),
80
81 #[error("Internal error: {0}")]
83 Internal(String),
84
85 #[error("{message}")]
87 Other {
88 message: String,
90 #[source]
92 source: Option<Box<dyn std::error::Error + Send + Sync>>,
93 },
94}
95
96impl Error {
97 pub fn new(message: impl Into<String>) -> Self {
99 Self::Other {
100 message: message.into(),
101 source: None,
102 }
103 }
104
105 pub fn with_source(
107 message: impl Into<String>,
108 source: impl std::error::Error + Send + Sync + 'static,
109 ) -> Self {
110 Self::Other {
111 message: message.into(),
112 source: Some(Box::new(source)),
113 }
114 }
115
116 #[must_use]
118 pub const fn is_retryable(&self) -> bool {
119 matches!(
120 self,
121 Self::Network(_) | Self::Timeout(_) | Self::RateLimit | Self::Provider(_)
122 )
123 }
124
125 #[must_use]
127 pub const fn is_client_error(&self) -> bool {
128 matches!(
129 self,
130 Self::InvalidInput(_)
131 | Self::Validation(_)
132 | Self::Authentication(_)
133 | Self::Authorization(_)
134 | Self::NotFound(_)
135 )
136 }
137
138 #[must_use]
140 pub const fn is_server_error(&self) -> bool {
141 matches!(
142 self,
143 Self::Internal(_) | Self::Database(_) | Self::Cache(_) | Self::Initialization(_)
144 )
145 }
146
147 #[must_use]
149 pub const fn error_code(&self) -> &'static str {
150 match self {
151 Self::Configuration(_) => "E001",
152 Self::Validation(_) => "E002",
153 Self::Pipeline(_) => "E003",
154 Self::Context(_) => "E004",
155 Self::Plugin(_) => "E005",
156 Self::Provider(_) => "E006",
157 Self::Network(_) => "E007",
158 Self::Timeout(_) => "E008",
159 Self::RateLimit => "E009",
160 Self::Authentication(_) => "E010",
161 Self::Authorization(_) => "E011",
162 Self::NotFound(_) => "E012",
163 Self::InvalidInput(_) => "E013",
164 Self::Serialization(_) => "E014",
165 Self::Database(_) => "E015",
166 Self::Cache(_) => "E016",
167 Self::Initialization(_) => "E017",
168 Self::Internal(_) => "E018",
169 Self::Other { .. } => "E999",
170 }
171 }
172
173 #[must_use]
175 pub const fn http_status_code(&self) -> u16 {
176 match self {
177 Self::InvalidInput(_) | Self::Validation(_) => 400,
178 Self::Authentication(_) => 401,
179 Self::Authorization(_) => 403,
180 Self::NotFound(_) => 404,
181 Self::Timeout(_) => 408,
182 Self::RateLimit => 429,
183 Self::Network(_) | Self::Provider(_) => 502,
184 Self::Initialization(_) => 503,
185 _ => 500,
186 }
187 }
188}
189
190pub type Result<T> = std::result::Result<T, Error>;
192
193pub trait ErrorContext<T> {
195 fn context(self, msg: impl fmt::Display) -> Result<T>;
201
202 fn with_context<F>(self, f: F) -> Result<T>
208 where
209 F: FnOnce() -> String;
210}
211
212impl<T, E> ErrorContext<T> for std::result::Result<T, E>
213where
214 E: std::error::Error + Send + Sync + 'static,
215{
216 fn context(self, msg: impl fmt::Display) -> Result<T> {
217 self.map_err(|e| Error::with_source(msg.to_string(), e))
218 }
219
220 fn with_context<F>(self, f: F) -> Result<T>
221 where
222 F: FnOnce() -> String,
223 {
224 self.map_err(|e| Error::with_source(f(), e))
225 }
226}
227
228#[derive(Debug, serde::Serialize, serde::Deserialize)]
230pub struct ErrorResponse {
231 pub code: String,
233 pub message: String,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub details: Option<serde_json::Value>,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub request_id: Option<String>,
241}
242
243impl From<Error> for ErrorResponse {
244 fn from(error: Error) -> Self {
245 Self {
246 code: error.error_code().to_string(),
247 message: error.to_string(),
248 details: None,
249 request_id: None,
250 }
251 }
252}
253
254impl ErrorResponse {
255 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
257 Self {
258 code: code.into(),
259 message: message.into(),
260 details: None,
261 request_id: None,
262 }
263 }
264
265 #[must_use]
267 pub fn with_details(mut self, details: serde_json::Value) -> Self {
268 self.details = Some(details);
269 self
270 }
271
272 #[must_use]
274 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
275 self.request_id = Some(request_id.into());
276 self
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use std::error::Error as StdError;
284
285 #[test]
286 fn test_error_creation() {
287 let error = Error::new("test error");
288 assert_eq!(error.to_string(), "test error");
289 assert_eq!(error.error_code(), "E999");
290 }
291
292 #[test]
293 fn test_error_with_source() {
294 let source = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
295 let error = Error::with_source("wrapper error", source);
296 assert_eq!(error.to_string(), "wrapper error");
297 assert!(StdError::source(&error).is_some());
298 }
299
300 #[test]
301 fn test_retryable_errors() {
302 assert!(Error::Network("network error".into()).is_retryable());
303 assert!(Error::Timeout(std::time::Duration::from_secs(30)).is_retryable());
304 assert!(Error::RateLimit.is_retryable());
305 assert!(Error::Provider("provider error".into()).is_retryable());
306
307 assert!(!Error::InvalidInput("bad input".into()).is_retryable());
308 assert!(!Error::Authentication("auth failed".into()).is_retryable());
309 }
310
311 #[test]
312 fn test_client_errors() {
313 assert!(Error::InvalidInput("bad input".into()).is_client_error());
314 assert!(Error::Validation("validation failed".into()).is_client_error());
315 assert!(Error::Authentication("auth failed".into()).is_client_error());
316 assert!(Error::Authorization("not authorized".into()).is_client_error());
317 assert!(Error::NotFound("not found".into()).is_client_error());
318
319 assert!(!Error::Internal("internal error".into()).is_client_error());
320 assert!(!Error::Database("db error".into()).is_client_error());
321 }
322
323 #[test]
324 fn test_server_errors() {
325 assert!(Error::Internal("internal error".into()).is_server_error());
326 assert!(Error::Database("db error".into()).is_server_error());
327 assert!(Error::Cache("cache error".into()).is_server_error());
328 assert!(Error::Initialization("init failed".into()).is_server_error());
329
330 assert!(!Error::InvalidInput("bad input".into()).is_server_error());
331 assert!(!Error::Authentication("auth failed".into()).is_server_error());
332 }
333
334 #[test]
335 fn test_http_status_codes() {
336 assert_eq!(Error::InvalidInput("bad".into()).http_status_code(), 400);
337 assert_eq!(Error::Validation("bad".into()).http_status_code(), 400);
338 assert_eq!(Error::Authentication("auth".into()).http_status_code(), 401);
339 assert_eq!(Error::Authorization("authz".into()).http_status_code(), 403);
340 assert_eq!(Error::NotFound("404".into()).http_status_code(), 404);
341 assert_eq!(
342 Error::Timeout(std::time::Duration::from_secs(30)).http_status_code(),
343 408
344 );
345 assert_eq!(Error::RateLimit.http_status_code(), 429);
346 assert_eq!(Error::Internal("500".into()).http_status_code(), 500);
347 assert_eq!(Error::Network("net".into()).http_status_code(), 502);
348 assert_eq!(Error::Initialization("init".into()).http_status_code(), 503);
349 }
350
351 #[test]
352 fn test_error_response() {
353 let error = Error::InvalidInput("bad input".into());
354 let response = ErrorResponse::from(error);
355
356 assert_eq!(response.code, "E013");
357 assert_eq!(response.message, "Invalid input: bad input");
358 assert!(response.details.is_none());
359 assert!(response.request_id.is_none());
360 }
361
362 #[test]
363 fn test_error_response_with_details() {
364 let response = ErrorResponse::new("E001", "test error")
365 .with_details(serde_json::json!({"field": "name"}))
366 .with_request_id("req-123");
367
368 assert_eq!(response.code, "E001");
369 assert_eq!(response.message, "test error");
370 assert!(response.details.is_some());
371 assert_eq!(response.request_id.as_deref(), Some("req-123"));
372 }
373
374 #[test]
375 fn test_error_context() {
376 let result: std::result::Result<(), std::io::Error> = Err(std::io::Error::new(
377 std::io::ErrorKind::NotFound,
378 "file not found",
379 ));
380
381 let error = result.context("Failed to read file").unwrap_err();
382 assert_eq!(error.to_string(), "Failed to read file");
383 }
384}