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