reinhardt_core/exception.rs
1use thiserror::Error;
2
3pub mod param_error;
4pub use param_error::{ParamErrorContext, ParamType};
5
6/// The main error type for the Reinhardt framework.
7///
8/// This enum represents all possible errors that can occur within the Reinhardt
9/// ecosystem. Each variant corresponds to a specific error category with an
10/// associated HTTP status code.
11///
12/// # Examples
13///
14/// ```
15/// use reinhardt_core::exception::Error;
16///
17// Create an HTTP error
18/// let http_err = Error::Http("Invalid request format".to_string());
19/// assert_eq!(http_err.to_string(), "HTTP error: Invalid request format");
20/// assert_eq!(http_err.status_code(), 400);
21///
22// Create a database error
23/// let db_err = Error::Database("Connection timeout".to_string());
24/// assert_eq!(db_err.status_code(), 500);
25///
26// Create an authentication error
27/// let auth_err = Error::Authentication("Invalid token".to_string());
28/// assert_eq!(auth_err.status_code(), 401);
29/// ```
30#[derive(Error, Debug)]
31pub enum Error {
32 /// HTTP-related errors (status code: 400)
33 ///
34 /// # Examples
35 ///
36 /// ```
37 /// use reinhardt_core::exception::Error;
38 ///
39 /// let error = Error::Http("Malformed request body".to_string());
40 /// assert_eq!(error.status_code(), 400);
41 /// assert!(error.to_string().contains("HTTP error"));
42 /// ```
43 #[error("HTTP error: {0}")]
44 Http(String),
45
46 /// Database-related errors (status code: 500)
47 ///
48 /// # Examples
49 ///
50 /// ```
51 /// use reinhardt_core::exception::Error;
52 ///
53 /// let error = Error::Database("Query execution failed".to_string());
54 /// assert_eq!(error.status_code(), 500);
55 /// assert!(error.to_string().contains("Database error"));
56 /// ```
57 #[error("Database error: {0}")]
58 Database(String),
59
60 /// Serialization/deserialization errors (status code: 400)
61 ///
62 /// # Examples
63 ///
64 /// ```
65 /// use reinhardt_core::exception::Error;
66 ///
67 /// let error = Error::Serialization("Invalid JSON format".to_string());
68 /// assert_eq!(error.status_code(), 400);
69 /// assert!(error.to_string().contains("Serialization error"));
70 /// ```
71 #[error("Serialization error: {0}")]
72 Serialization(String),
73
74 /// Validation errors (status code: 400)
75 ///
76 /// # Examples
77 ///
78 /// ```
79 /// use reinhardt_core::exception::Error;
80 ///
81 /// let error = Error::Validation("Email format is invalid".to_string());
82 /// assert_eq!(error.status_code(), 400);
83 /// assert!(error.to_string().contains("Validation error"));
84 /// ```
85 #[error("Validation error: {0}")]
86 Validation(String),
87
88 /// Authentication errors (status code: 401)
89 ///
90 /// # Examples
91 ///
92 /// ```
93 /// use reinhardt_core::exception::Error;
94 ///
95 /// let error = Error::Authentication("Invalid credentials".to_string());
96 /// assert_eq!(error.status_code(), 401);
97 /// assert!(error.to_string().contains("Authentication error"));
98 /// ```
99 #[error("Authentication error: {0}")]
100 Authentication(String),
101
102 /// Authorization errors (status code: 403)
103 ///
104 /// # Examples
105 ///
106 /// ```
107 /// use reinhardt_core::exception::Error;
108 ///
109 /// let error = Error::Authorization("Insufficient permissions".to_string());
110 /// assert_eq!(error.status_code(), 403);
111 /// assert!(error.to_string().contains("Authorization error"));
112 /// ```
113 #[error("Authorization error: {0}")]
114 Authorization(String),
115
116 /// Resource not found errors (status code: 404)
117 ///
118 /// # Examples
119 ///
120 /// ```
121 /// use reinhardt_core::exception::Error;
122 ///
123 /// let error = Error::NotFound("User with ID 123 not found".to_string());
124 /// assert_eq!(error.status_code(), 404);
125 /// assert!(error.to_string().contains("Not found"));
126 /// ```
127 #[error("Not found: {0}")]
128 NotFound(String),
129
130 /// Template not found errors (status code: 404)
131 #[error("Template not found: {0}")]
132 TemplateNotFound(String),
133
134 /// Method not allowed errors (status code: 405)
135 ///
136 /// This error occurs when the HTTP method used is not supported for the
137 /// requested resource, even though the resource exists.
138 ///
139 /// # Examples
140 ///
141 /// ```
142 /// use reinhardt_core::exception::Error;
143 ///
144 /// let error = Error::MethodNotAllowed("Method PATCH not allowed for /api/articles/1".to_string());
145 /// assert_eq!(error.status_code(), 405);
146 /// assert!(error.to_string().contains("Method not allowed"));
147 /// ```
148 #[error("Method not allowed: {0}")]
149 MethodNotAllowed(String),
150
151 /// Conflict errors (status code: 409)
152 ///
153 /// This error occurs when the request could not be completed due to a
154 /// conflict with the current state of the resource. Commonly used for
155 /// duplicate resources or conflicting operations.
156 ///
157 /// # Examples
158 ///
159 /// ```
160 /// use reinhardt_core::exception::Error;
161 ///
162 /// let error = Error::Conflict("User with this email already exists".to_string());
163 /// assert_eq!(error.status_code(), 409);
164 /// assert!(error.to_string().contains("Conflict"));
165 /// ```
166 #[error("Conflict: {0}")]
167 Conflict(String),
168
169 /// Internal server errors (status code: 500)
170 ///
171 /// # Examples
172 ///
173 /// ```
174 /// use reinhardt_core::exception::Error;
175 ///
176 /// let error = Error::Internal("Unexpected server error".to_string());
177 /// assert_eq!(error.status_code(), 500);
178 /// assert!(error.to_string().contains("Internal server error"));
179 /// ```
180 #[error("Internal server error: {0}")]
181 Internal(String),
182
183 /// Configuration errors (status code: 500)
184 ///
185 /// # Examples
186 ///
187 /// ```
188 /// use reinhardt_core::exception::Error;
189 ///
190 /// let error = Error::ImproperlyConfigured("Missing DATABASE_URL".to_string());
191 /// assert_eq!(error.status_code(), 500);
192 /// assert!(error.to_string().contains("Improperly configured"));
193 /// ```
194 #[error("Improperly configured: {0}")]
195 ImproperlyConfigured(String),
196
197 /// Body already consumed error (status code: 400)
198 ///
199 /// This error occurs when attempting to read a request body that has already
200 /// been consumed.
201 ///
202 /// # Examples
203 ///
204 /// ```
205 /// use reinhardt_core::exception::Error;
206 ///
207 /// let error = Error::BodyAlreadyConsumed;
208 /// assert_eq!(error.status_code(), 400);
209 /// assert_eq!(error.to_string(), "Body already consumed");
210 /// ```
211 #[error("Body already consumed")]
212 BodyAlreadyConsumed,
213
214 /// Parse errors (status code: 400)
215 ///
216 /// # Examples
217 ///
218 /// ```
219 /// use reinhardt_core::exception::Error;
220 ///
221 /// let error = Error::ParseError("Invalid integer value".to_string());
222 /// assert_eq!(error.status_code(), 400);
223 /// assert!(error.to_string().contains("Parse error"));
224 /// ```
225 #[error("Parse error: {0}")]
226 ParseError(String),
227
228 /// Missing Content-Type header
229 #[error("Missing Content-Type header")]
230 MissingContentType,
231
232 /// Invalid page error for pagination (status code: 400)
233 #[error("Invalid page: {0}")]
234 InvalidPage(String),
235
236 /// Invalid cursor error for pagination (status code: 400)
237 #[error("Invalid cursor: {0}")]
238 InvalidCursor(String),
239
240 /// Invalid limit error for pagination (status code: 400)
241 #[error("Invalid limit: {0}")]
242 InvalidLimit(String),
243
244 /// Missing parameter error for URL reverse (status code: 400)
245 #[error("Missing parameter: {0}")]
246 MissingParameter(String),
247
248 /// Parameter validation errors with detailed context (status code: 400)
249 ///
250 /// This variant provides structured error information for HTTP parameter
251 /// extraction failures, including field names, expected types, and raw values.
252 ///
253 /// # Examples
254 ///
255 /// ```
256 /// use reinhardt_core::exception::{Error, ParamErrorContext, ParamType};
257 ///
258 /// let ctx = ParamErrorContext::new(ParamType::Json, "missing field 'email'")
259 /// .with_field("email")
260 /// .with_expected_type::<String>();
261 /// let error = Error::ParamValidation(Box::new(ctx));
262 /// assert_eq!(error.status_code(), 400);
263 /// ```
264 #[error("{}", .0.format_error())]
265 // Box wrapper to reduce enum size (clippy::result_large_err mitigation)
266 // ParamErrorContext contains multiple String fields which make the enum large
267 ParamValidation(Box<ParamErrorContext>),
268
269 /// Wraps any other error type using `anyhow::Error` (status code: 500)
270 ///
271 /// # Examples
272 ///
273 /// ```
274 /// use reinhardt_core::exception::Error;
275 /// use anyhow::anyhow;
276 ///
277 /// let other_error = anyhow!("Something went wrong");
278 /// let error: Error = other_error.into();
279 /// assert_eq!(error.status_code(), 500);
280 /// ```
281 #[error(transparent)]
282 Other(#[from] anyhow::Error),
283}
284
285/// A convenient `Result` type alias using `reinhardt_core::exception::Error` as the error type.
286///
287/// This type alias is used throughout the Reinhardt framework to simplify
288/// function signatures that return results.
289///
290/// # Examples
291///
292/// ```
293/// use reinhardt_core::exception::{Error, Result};
294///
295/// fn validate_email(email: &str) -> Result<()> {
296/// if email.contains('@') {
297/// Ok(())
298/// } else {
299/// Err(Error::Validation("Email must contain @".to_string()))
300/// }
301/// }
302///
303// Successful validation
304/// assert!(validate_email("user@example.com").is_ok());
305///
306// Failed validation
307/// let result = validate_email("invalid-email");
308/// assert!(result.is_err());
309/// match result {
310/// Err(Error::Validation(msg)) => assert!(msg.contains("@")),
311/// _ => panic!("Expected validation error"),
312/// }
313/// ```
314pub type Result<T> = std::result::Result<T, Error>;
315
316/// Categorical classification of `Error` variants.
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub enum ErrorKind {
319 Http,
320 Database,
321 Serialization,
322 Validation,
323 Authentication,
324 Authorization,
325 NotFound,
326 MethodNotAllowed,
327 Conflict,
328 Internal,
329 ImproperlyConfigured,
330 BodyAlreadyConsumed,
331 Parse,
332 ParamValidation,
333 Other,
334}
335
336impl Error {
337 /// Returns the HTTP status code associated with this error.
338 ///
339 /// Each error variant maps to an appropriate HTTP status code that can be
340 /// used when converting errors to HTTP responses.
341 ///
342 /// # Status Code Mapping
343 ///
344 /// - `Http`, `Serialization`, `Validation`, `BodyAlreadyConsumed`, `ParseError`: 400 (Bad Request)
345 /// - `Authentication`: 401 (Unauthorized)
346 /// - `Authorization`: 403 (Forbidden)
347 /// - `NotFound`, `TemplateNotFound`: 404 (Not Found)
348 /// - `MethodNotAllowed`: 405 (Method Not Allowed)
349 /// - `Conflict`: 409 (Conflict)
350 /// - `Database`, `Internal`, `ImproperlyConfigured`, `Other`: 500 (Internal Server Error)
351 ///
352 /// # Examples
353 ///
354 /// ```
355 /// use reinhardt_core::exception::Error;
356 ///
357 // Client errors (4xx)
358 /// assert_eq!(Error::Http("Bad request".to_string()).status_code(), 400);
359 /// assert_eq!(Error::Validation("Invalid input".to_string()).status_code(), 400);
360 /// assert_eq!(Error::Authentication("No token".to_string()).status_code(), 401);
361 /// assert_eq!(Error::Authorization("No access".to_string()).status_code(), 403);
362 /// assert_eq!(Error::NotFound("Resource missing".to_string()).status_code(), 404);
363 ///
364 // Server errors (5xx)
365 /// assert_eq!(Error::Database("Connection failed".to_string()).status_code(), 500);
366 /// assert_eq!(Error::Internal("Crash".to_string()).status_code(), 500);
367 /// assert_eq!(Error::ImproperlyConfigured("Bad config".to_string()).status_code(), 500);
368 ///
369 // Edge cases
370 /// assert_eq!(Error::BodyAlreadyConsumed.status_code(), 400);
371 /// assert_eq!(Error::ParseError("Invalid data".to_string()).status_code(), 400);
372 /// ```
373 ///
374 /// # Using with anyhow errors
375 ///
376 /// ```
377 /// use reinhardt_core::exception::Error;
378 /// use anyhow::anyhow;
379 ///
380 /// let anyhow_error = anyhow!("Unexpected error");
381 /// let error: Error = anyhow_error.into();
382 /// assert_eq!(error.status_code(), 500);
383 /// ```
384 pub fn status_code(&self) -> u16 {
385 match self {
386 Error::Http(_) => 400,
387 Error::Database(_) => 500,
388 Error::Serialization(_) => 400,
389 Error::Validation(_) => 400,
390 Error::Authentication(_) => 401,
391 Error::Authorization(_) => 403,
392 Error::NotFound(_) => 404,
393 Error::TemplateNotFound(_) => 404,
394 Error::MethodNotAllowed(_) => 405,
395 Error::Conflict(_) => 409,
396 Error::Internal(_) => 500,
397 Error::ImproperlyConfigured(_) => 500,
398 Error::BodyAlreadyConsumed => 400,
399 Error::ParseError(_) => 400,
400 Error::MissingContentType => 400,
401 Error::InvalidPage(_) => 400,
402 Error::InvalidCursor(_) => 400,
403 Error::InvalidLimit(_) => 400,
404 Error::MissingParameter(_) => 400,
405 Error::ParamValidation(_) => 400,
406 Error::Other(_) => 500,
407 }
408 }
409
410 /// Returns the categorical `ErrorKind` for this error.
411 pub fn kind(&self) -> ErrorKind {
412 match self {
413 Error::Http(_) => ErrorKind::Http,
414 Error::Database(_) => ErrorKind::Database,
415 Error::Serialization(_) => ErrorKind::Serialization,
416 Error::Validation(_) => ErrorKind::Validation,
417 Error::Authentication(_) => ErrorKind::Authentication,
418 Error::Authorization(_) => ErrorKind::Authorization,
419 Error::NotFound(_) => ErrorKind::NotFound,
420 Error::TemplateNotFound(_) => ErrorKind::NotFound,
421 Error::MethodNotAllowed(_) => ErrorKind::MethodNotAllowed,
422 Error::Conflict(_) => ErrorKind::Conflict,
423 Error::Internal(_) => ErrorKind::Internal,
424 Error::ImproperlyConfigured(_) => ErrorKind::ImproperlyConfigured,
425 Error::BodyAlreadyConsumed => ErrorKind::BodyAlreadyConsumed,
426 Error::ParseError(_) => ErrorKind::Parse,
427 Error::MissingContentType => ErrorKind::Http,
428 Error::InvalidPage(_) => ErrorKind::Validation,
429 Error::InvalidCursor(_) => ErrorKind::Validation,
430 Error::InvalidLimit(_) => ErrorKind::Validation,
431 Error::MissingParameter(_) => ErrorKind::Validation,
432 Error::ParamValidation(_) => ErrorKind::ParamValidation,
433 Error::Other(_) => ErrorKind::Other,
434 }
435 }
436}
437
438// Common conversions to the unified Error without introducing cross-crate deps.
439impl From<serde_json::Error> for Error {
440 fn from(err: serde_json::Error) -> Self {
441 Error::Serialization(err.to_string())
442 }
443}
444
445impl From<std::io::Error> for Error {
446 fn from(err: std::io::Error) -> Self {
447 Error::Internal(format!("IO error: {}", err))
448 }
449}
450
451impl From<http::Error> for Error {
452 fn from(err: http::Error) -> Self {
453 Error::Http(err.to_string())
454 }
455}
456
457impl From<String> for Error {
458 fn from(msg: String) -> Self {
459 Error::Internal(msg)
460 }
461}
462
463impl From<&str> for Error {
464 fn from(msg: &str) -> Self {
465 Error::Internal(msg.to_string())
466 }
467}
468
469impl From<validator::ValidationErrors> for Error {
470 fn from(err: validator::ValidationErrors) -> Self {
471 Error::Validation(format!("Validation failed: {}", err))
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn test_error_kind_mapping() {
481 // HTTP errors
482 assert_eq!(Error::Http("test".to_string()).kind(), ErrorKind::Http);
483 assert_eq!(Error::MissingContentType.kind(), ErrorKind::Http);
484
485 // Database errors
486 assert_eq!(
487 Error::Database("test".to_string()).kind(),
488 ErrorKind::Database
489 );
490
491 // Serialization errors
492 assert_eq!(
493 Error::Serialization("test".to_string()).kind(),
494 ErrorKind::Serialization
495 );
496
497 // Validation errors
498 assert_eq!(
499 Error::Validation("test".to_string()).kind(),
500 ErrorKind::Validation
501 );
502 assert_eq!(
503 Error::InvalidPage("test".to_string()).kind(),
504 ErrorKind::Validation
505 );
506 assert_eq!(
507 Error::InvalidCursor("test".to_string()).kind(),
508 ErrorKind::Validation
509 );
510 assert_eq!(
511 Error::InvalidLimit("test".to_string()).kind(),
512 ErrorKind::Validation
513 );
514 assert_eq!(
515 Error::MissingParameter("test".to_string()).kind(),
516 ErrorKind::Validation
517 );
518
519 // Authentication errors
520 assert_eq!(
521 Error::Authentication("test".to_string()).kind(),
522 ErrorKind::Authentication
523 );
524
525 // Authorization errors
526 assert_eq!(
527 Error::Authorization("test".to_string()).kind(),
528 ErrorKind::Authorization
529 );
530
531 // NotFound errors
532 assert_eq!(
533 Error::NotFound("test".to_string()).kind(),
534 ErrorKind::NotFound
535 );
536 assert_eq!(
537 Error::TemplateNotFound("test".to_string()).kind(),
538 ErrorKind::NotFound
539 );
540
541 // MethodNotAllowed errors
542 assert_eq!(
543 Error::MethodNotAllowed("test".to_string()).kind(),
544 ErrorKind::MethodNotAllowed
545 );
546
547 // Internal errors
548 assert_eq!(
549 Error::Internal("test".to_string()).kind(),
550 ErrorKind::Internal
551 );
552
553 // ImproperlyConfigured errors
554 assert_eq!(
555 Error::ImproperlyConfigured("test".to_string()).kind(),
556 ErrorKind::ImproperlyConfigured
557 );
558
559 // BodyAlreadyConsumed errors
560 assert_eq!(
561 Error::BodyAlreadyConsumed.kind(),
562 ErrorKind::BodyAlreadyConsumed
563 );
564
565 // Parse errors
566 assert_eq!(
567 Error::ParseError("test".to_string()).kind(),
568 ErrorKind::Parse
569 );
570
571 // Other errors
572 assert_eq!(
573 Error::Other(anyhow::anyhow!("test")).kind(),
574 ErrorKind::Other
575 );
576 }
577
578 #[test]
579 fn test_from_serde_json_error() {
580 let json_error = serde_json::from_str::<i32>("invalid").unwrap_err();
581 let error: Error = json_error.into();
582
583 assert_eq!(error.status_code(), 400);
584 assert_eq!(error.kind(), ErrorKind::Serialization);
585 assert!(error.to_string().contains("Serialization error"));
586 }
587
588 #[test]
589 fn test_from_io_error() {
590 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
591 let error: Error = io_error.into();
592
593 assert_eq!(error.status_code(), 500);
594 assert_eq!(error.kind(), ErrorKind::Internal);
595 assert!(error.to_string().contains("IO error"));
596 }
597
598 #[test]
599 fn test_status_codes_comprehensive() {
600 // 400 errors
601 assert_eq!(Error::Http("test".to_string()).status_code(), 400);
602 assert_eq!(Error::Serialization("test".to_string()).status_code(), 400);
603 assert_eq!(Error::Validation("test".to_string()).status_code(), 400);
604 assert_eq!(Error::BodyAlreadyConsumed.status_code(), 400);
605 assert_eq!(Error::ParseError("test".to_string()).status_code(), 400);
606 assert_eq!(Error::MissingContentType.status_code(), 400);
607 assert_eq!(Error::InvalidPage("test".to_string()).status_code(), 400);
608 assert_eq!(Error::InvalidCursor("test".to_string()).status_code(), 400);
609 assert_eq!(Error::InvalidLimit("test".to_string()).status_code(), 400);
610 assert_eq!(
611 Error::MissingParameter("test".to_string()).status_code(),
612 400
613 );
614
615 // 401 error
616 assert_eq!(Error::Authentication("test".to_string()).status_code(), 401);
617
618 // 403 error
619 assert_eq!(Error::Authorization("test".to_string()).status_code(), 403);
620
621 // 404 errors
622 assert_eq!(Error::NotFound("test".to_string()).status_code(), 404);
623 assert_eq!(
624 Error::TemplateNotFound("test".to_string()).status_code(),
625 404
626 );
627
628 // 405 error
629 assert_eq!(
630 Error::MethodNotAllowed("test".to_string()).status_code(),
631 405
632 );
633
634 // 500 errors
635 assert_eq!(Error::Database("test".to_string()).status_code(), 500);
636 assert_eq!(Error::Internal("test".to_string()).status_code(), 500);
637 assert_eq!(
638 Error::ImproperlyConfigured("test".to_string()).status_code(),
639 500
640 );
641 assert_eq!(Error::Other(anyhow::anyhow!("test")).status_code(), 500);
642 }
643
644 #[test]
645 fn test_template_not_found_error() {
646 let error = Error::TemplateNotFound("index.html".to_string());
647 assert_eq!(error.status_code(), 404);
648 assert_eq!(error.kind(), ErrorKind::NotFound);
649 assert!(error.to_string().contains("Template not found"));
650 assert!(error.to_string().contains("index.html"));
651 }
652
653 #[test]
654 fn test_pagination_errors() {
655 let page_error = Error::InvalidPage("page must be positive".to_string());
656 assert_eq!(page_error.status_code(), 400);
657 assert_eq!(page_error.kind(), ErrorKind::Validation);
658
659 let cursor_error = Error::InvalidCursor("invalid base64".to_string());
660 assert_eq!(cursor_error.status_code(), 400);
661 assert_eq!(cursor_error.kind(), ErrorKind::Validation);
662
663 let limit_error = Error::InvalidLimit("limit too large".to_string());
664 assert_eq!(limit_error.status_code(), 400);
665 assert_eq!(limit_error.kind(), ErrorKind::Validation);
666 }
667
668 #[test]
669 fn test_missing_parameter_error() {
670 let error = Error::MissingParameter("user_id".to_string());
671 assert_eq!(error.status_code(), 400);
672 assert_eq!(error.kind(), ErrorKind::Validation);
673 assert!(error.to_string().contains("Missing parameter"));
674 assert!(error.to_string().contains("user_id"));
675 }
676}