Skip to main content

universal_bot_core/
error.rs

1//! Error types for Universal Bot
2//!
3//! This module defines the error types used throughout the Universal Bot framework,
4//! following best practices for error handling in Rust.
5
6use std::fmt;
7
8use thiserror::Error;
9
10/// Main error type for Universal Bot
11#[derive(Error, Debug)]
12pub enum Error {
13    /// Configuration error
14    #[error("Configuration error: {0}")]
15    Configuration(String),
16
17    /// Validation error
18    #[error("Validation error: {0}")]
19    Validation(String),
20
21    /// Pipeline processing error
22    #[error("Pipeline error: {0}")]
23    Pipeline(String),
24
25    /// Context management error
26    #[error("Context error: {0}")]
27    Context(String),
28
29    /// Plugin error
30    #[error("Plugin error: {0}")]
31    Plugin(String),
32
33    /// AI provider error
34    #[error("AI provider error: {0}")]
35    Provider(String),
36
37    /// Network error
38    #[error("Network error: {0}")]
39    Network(String),
40
41    /// Timeout error
42    #[error("Operation timed out after {0:?}")]
43    Timeout(std::time::Duration),
44
45    /// Rate limit error
46    #[error("Rate limit exceeded")]
47    RateLimit,
48
49    /// Authentication error
50    #[error("Authentication failed: {0}")]
51    Authentication(String),
52
53    /// Authorization error
54    #[error("Authorization failed: {0}")]
55    Authorization(String),
56
57    /// Resource not found
58    #[error("Resource not found: {0}")]
59    NotFound(String),
60
61    /// Invalid input
62    #[error("Invalid input: {0}")]
63    InvalidInput(String),
64
65    /// Serialization/deserialization error
66    #[error("Serialization error: {0}")]
67    Serialization(String),
68
69    /// Database error
70    #[error("Database error: {0}")]
71    Database(String),
72
73    /// Cache error
74    #[error("Cache error: {0}")]
75    Cache(String),
76
77    /// Initialization error
78    #[error("Initialization failed: {0}")]
79    Initialization(String),
80
81    /// Internal error (should not happen)
82    #[error("Internal error: {0}")]
83    Internal(String),
84
85    /// Other error with context
86    #[error("{message}")]
87    Other {
88        /// Error message
89        message: String,
90        /// Optional error source
91        #[source]
92        source: Option<Box<dyn std::error::Error + Send + Sync>>,
93    },
94}
95
96impl Error {
97    /// Create a new error with a message
98    pub fn new(message: impl Into<String>) -> Self {
99        Self::Other {
100            message: message.into(),
101            source: None,
102        }
103    }
104
105    /// Create a new error with a message and source
106    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    /// Check if this error is retryable
117    #[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    /// Check if this error is a client error
126    #[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    /// Check if this error is a server error
139    #[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    /// Get the error code for API responses
148    #[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    /// Get the HTTP status code for this error
174    #[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
190/// Result type alias using our Error type
191pub type Result<T> = std::result::Result<T, Error>;
192
193/// Extension trait for converting errors with context
194pub trait ErrorContext<T> {
195    /// Add context to an error
196    ///
197    /// # Errors
198    ///
199    /// Returns the original error with added context
200    fn context(self, msg: impl fmt::Display) -> Result<T>;
201
202    /// Add context with a closure
203    ///
204    /// # Errors
205    ///
206    /// Returns the original error with added context from the closure
207    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/// Error response for API
229#[derive(Debug, serde::Serialize, serde::Deserialize)]
230pub struct ErrorResponse {
231    /// Error code
232    pub code: String,
233    /// Error message
234    pub message: String,
235    /// Additional details
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub details: Option<serde_json::Value>,
238    /// Request ID for tracing
239    #[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    /// Create a new error response
256    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    /// Add details to the error response
266    #[must_use]
267    pub fn with_details(mut self, details: serde_json::Value) -> Self {
268        self.details = Some(details);
269        self
270    }
271
272    /// Add request ID for tracing
273    #[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}