Skip to main content

backbone_core/
error.rs

1//! Module Error Trait
2//!
3//! Provides a unified error trait for all Backbone modules.
4//! This enables consistent error handling and API responses across modules.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use backbone_core::ModuleError;
10//!
11//! #[derive(Debug, thiserror::Error)]
12//! pub enum UserError {
13//!     #[error("User not found: {0}")]
14//!     NotFound(String),
15//!
16//!     #[error("Email already exists: {0}")]
17//!     EmailExists(String),
18//!
19//!     #[error("Invalid password")]
20//!     InvalidPassword,
21//! }
22//!
23//! impl ModuleError for UserError {
24//!     fn error_code(&self) -> &str {
25//!         match self {
26//!             Self::NotFound(_) => "USER_NOT_FOUND",
27//!             Self::EmailExists(_) => "EMAIL_EXISTS",
28//!             Self::InvalidPassword => "INVALID_PASSWORD",
29//!         }
30//!     }
31//!
32//!     fn status_code(&self) -> u16 {
33//!         match self {
34//!             Self::NotFound(_) => 404,
35//!             Self::EmailExists(_) => 409,
36//!             Self::InvalidPassword => 401,
37//!         }
38//!     }
39//!
40//!     fn user_message(&self) -> &str {
41//!         match self {
42//!             Self::NotFound(_) => "The requested user was not found",
43//!             Self::EmailExists(_) => "This email is already registered",
44//!             Self::InvalidPassword => "Invalid password provided",
45//!         }
46//!     }
47//! }
48//! ```
49
50use std::fmt::Debug;
51
52/// Unified error trait for Backbone modules.
53///
54/// Implementing this trait allows errors to be:
55/// - Converted to consistent API responses
56/// - Logged with proper context
57/// - Categorized for monitoring and alerting
58pub trait ModuleError: std::error::Error + Send + Sync + Debug {
59    /// Machine-readable error code.
60    ///
61    /// Format: `{ENTITY}_{ERROR_TYPE}` (e.g., `USER_NOT_FOUND`)
62    fn error_code(&self) -> &str;
63
64    /// HTTP status code for this error.
65    ///
66    /// Common codes:
67    /// - 400: Bad Request (validation errors)
68    /// - 401: Unauthorized
69    /// - 403: Forbidden
70    /// - 404: Not Found
71    /// - 409: Conflict (duplicate, version mismatch)
72    /// - 422: Unprocessable Entity
73    /// - 500: Internal Server Error
74    fn status_code(&self) -> u16;
75
76    /// User-friendly error message.
77    ///
78    /// This message is safe to display to end users.
79    /// Avoid including sensitive information.
80    fn user_message(&self) -> &str;
81
82    /// Whether this error is retriable.
83    ///
84    /// Returns `true` for transient errors like timeouts
85    /// or temporary service unavailability.
86    fn is_retriable(&self) -> bool {
87        false
88    }
89
90    /// Whether this error should be logged at error level.
91    ///
92    /// Returns `true` for unexpected errors (500s).
93    /// Returns `false` for expected errors (4xx).
94    fn is_loggable(&self) -> bool {
95        self.status_code() >= 500
96    }
97
98    /// Error category for monitoring.
99    fn category(&self) -> ErrorCategory {
100        match self.status_code() {
101            400..=499 => ErrorCategory::ClientError,
102            500..=599 => ErrorCategory::ServerError,
103            _ => ErrorCategory::Unknown,
104        }
105    }
106
107    /// Additional context for debugging.
108    ///
109    /// This information is logged but not exposed to users.
110    fn debug_context(&self) -> Option<String> {
111        None
112    }
113}
114
115/// Error category for monitoring and alerting.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum ErrorCategory {
118    /// Client-side errors (4xx).
119    ClientError,
120    /// Server-side errors (5xx).
121    ServerError,
122    /// Validation errors.
123    ValidationError,
124    /// Authentication/authorization errors.
125    AuthError,
126    /// Database errors.
127    DatabaseError,
128    /// External service errors.
129    ExternalServiceError,
130    /// Unknown category.
131    Unknown,
132}
133
134impl std::fmt::Display for ErrorCategory {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        match self {
137            Self::ClientError => write!(f, "client_error"),
138            Self::ServerError => write!(f, "server_error"),
139            Self::ValidationError => write!(f, "validation_error"),
140            Self::AuthError => write!(f, "auth_error"),
141            Self::DatabaseError => write!(f, "database_error"),
142            Self::ExternalServiceError => write!(f, "external_service_error"),
143            Self::Unknown => write!(f, "unknown"),
144        }
145    }
146}
147
148/// Standard error response for API endpoints.
149#[derive(Debug, Clone, serde::Serialize)]
150pub struct ErrorResponse {
151    /// Machine-readable error code.
152    pub code: String,
153    /// Human-readable error message.
154    pub message: String,
155    /// Additional error details (optional).
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub details: Option<serde_json::Value>,
158}
159
160impl ErrorResponse {
161    /// Create a new error response from a ModuleError.
162    pub fn from_error<E: ModuleError>(error: &E) -> Self {
163        Self {
164            code: error.error_code().to_string(),
165            message: error.user_message().to_string(),
166            details: None,
167        }
168    }
169
170    /// Create an error response with details.
171    pub fn with_details<E: ModuleError>(error: &E, details: serde_json::Value) -> Self {
172        Self {
173            code: error.error_code().to_string(),
174            message: error.user_message().to_string(),
175            details: Some(details),
176        }
177    }
178}
179
180/// Common module errors that can be reused.
181#[derive(Debug, thiserror::Error)]
182pub enum CommonError {
183    /// Entity not found.
184    #[error("Entity not found: {entity_type} with id {id}")]
185    NotFound {
186        entity_type: &'static str,
187        id: String,
188    },
189
190    /// Duplicate entity.
191    #[error("Entity already exists: {entity_type}")]
192    AlreadyExists { entity_type: &'static str },
193
194    /// Validation failed.
195    #[error("Validation failed: {message}")]
196    ValidationFailed { message: String },
197
198    /// Unauthorized access.
199    #[error("Unauthorized")]
200    Unauthorized,
201
202    /// Forbidden access.
203    #[error("Forbidden: {message}")]
204    Forbidden { message: String },
205
206    /// Internal error.
207    #[error("Internal error: {message}")]
208    Internal { message: String },
209
210    /// Conflict (e.g., version mismatch).
211    #[error("Conflict: {message}")]
212    Conflict { message: String },
213}
214
215impl ModuleError for CommonError {
216    fn error_code(&self) -> &str {
217        match self {
218            Self::NotFound { .. } => "NOT_FOUND",
219            Self::AlreadyExists { .. } => "ALREADY_EXISTS",
220            Self::ValidationFailed { .. } => "VALIDATION_FAILED",
221            Self::Unauthorized => "UNAUTHORIZED",
222            Self::Forbidden { .. } => "FORBIDDEN",
223            Self::Internal { .. } => "INTERNAL_ERROR",
224            Self::Conflict { .. } => "CONFLICT",
225        }
226    }
227
228    fn status_code(&self) -> u16 {
229        match self {
230            Self::NotFound { .. } => 404,
231            Self::AlreadyExists { .. } => 409,
232            Self::ValidationFailed { .. } => 400,
233            Self::Unauthorized => 401,
234            Self::Forbidden { .. } => 403,
235            Self::Internal { .. } => 500,
236            Self::Conflict { .. } => 409,
237        }
238    }
239
240    fn user_message(&self) -> &str {
241        match self {
242            Self::NotFound { .. } => "The requested resource was not found",
243            Self::AlreadyExists { .. } => "A resource with this identifier already exists",
244            Self::ValidationFailed { message } => message,
245            Self::Unauthorized => "Authentication required",
246            Self::Forbidden { message } => message,
247            Self::Internal { .. } => "An internal error occurred",
248            Self::Conflict { message } => message,
249        }
250    }
251
252    fn is_retriable(&self) -> bool {
253        matches!(self, Self::Internal { .. })
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[derive(Debug, thiserror::Error)]
262    #[error("Test error")]
263    struct TestError;
264
265    impl ModuleError for TestError {
266        fn error_code(&self) -> &str {
267            "TEST_ERROR"
268        }
269
270        fn status_code(&self) -> u16 {
271            400
272        }
273
274        fn user_message(&self) -> &str {
275            "This is a test error"
276        }
277    }
278
279    #[test]
280    fn test_module_error_trait() {
281        let error = TestError;
282        assert_eq!(error.error_code(), "TEST_ERROR");
283        assert_eq!(error.status_code(), 400);
284        assert_eq!(error.user_message(), "This is a test error");
285        assert!(!error.is_retriable());
286        assert!(!error.is_loggable());
287        assert_eq!(error.category(), ErrorCategory::ClientError);
288    }
289
290    #[test]
291    fn test_error_response() {
292        let error = TestError;
293        let response = ErrorResponse::from_error(&error);
294        assert_eq!(response.code, "TEST_ERROR");
295        assert_eq!(response.message, "This is a test error");
296    }
297
298    #[test]
299    fn test_common_error() {
300        let error = CommonError::NotFound {
301            entity_type: "User",
302            id: "123".to_string(),
303        };
304        assert_eq!(error.error_code(), "NOT_FOUND");
305        assert_eq!(error.status_code(), 404);
306    }
307}