1use std::fmt::Debug;
51
52pub trait ModuleError: std::error::Error + Send + Sync + Debug {
59 fn error_code(&self) -> &str;
63
64 fn status_code(&self) -> u16;
75
76 fn user_message(&self) -> &str;
81
82 fn is_retriable(&self) -> bool {
87 false
88 }
89
90 fn is_loggable(&self) -> bool {
95 self.status_code() >= 500
96 }
97
98 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 fn debug_context(&self) -> Option<String> {
111 None
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum ErrorCategory {
118 ClientError,
120 ServerError,
122 ValidationError,
124 AuthError,
126 DatabaseError,
128 ExternalServiceError,
130 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#[derive(Debug, Clone, serde::Serialize)]
150pub struct ErrorResponse {
151 pub code: String,
153 pub message: String,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub details: Option<serde_json::Value>,
158}
159
160impl ErrorResponse {
161 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 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#[derive(Debug, thiserror::Error)]
182pub enum CommonError {
183 #[error("Entity not found: {entity_type} with id {id}")]
185 NotFound {
186 entity_type: &'static str,
187 id: String,
188 },
189
190 #[error("Entity already exists: {entity_type}")]
192 AlreadyExists { entity_type: &'static str },
193
194 #[error("Validation failed: {message}")]
196 ValidationFailed { message: String },
197
198 #[error("Unauthorized")]
200 Unauthorized,
201
202 #[error("Forbidden: {message}")]
204 Forbidden { message: String },
205
206 #[error("Internal error: {message}")]
208 Internal { message: String },
209
210 #[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}