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
317pub use http::StatusCode;
318
319/// Application-defined error contract for HTTP response mapping.
320///
321/// Implementations must return client-safe messages. Server-side diagnostic
322/// details should remain in logs or the original error display output.
323pub trait HttpError {
324 /// Returns the HTTP status associated with this error.
325 fn status_code(&self) -> StatusCode;
326
327 /// Returns the client-facing message associated with this error.
328 fn client_message(&self) -> std::borrow::Cow<'static, str>;
329}
330
331/// Categorical classification of `Error` variants.
332#[non_exhaustive]
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
334pub enum ErrorKind {
335 /// HTTP-related errors (400).
336 Http,
337 /// Database-related errors (500).
338 Database,
339 /// Serialization/deserialization errors (400).
340 Serialization,
341 /// Input validation errors (400).
342 Validation,
343 /// Authentication failures (401).
344 Authentication,
345 /// Authorization/permission errors (403).
346 Authorization,
347 /// Resource not found errors (404).
348 NotFound,
349 /// HTTP method not allowed (405).
350 MethodNotAllowed,
351 /// Resource conflict errors (409).
352 Conflict,
353 /// Internal server errors (500).
354 Internal,
355 /// Configuration errors (500).
356 ImproperlyConfigured,
357 /// Request body already consumed (400).
358 BodyAlreadyConsumed,
359 /// Parse errors (400).
360 Parse,
361 /// Parameter validation errors (400).
362 ParamValidation,
363 /// Catch-all for other errors (500).
364 Other,
365}
366
367impl Error {
368 /// Returns the HTTP status code associated with this error.
369 ///
370 /// Each error variant maps to an appropriate HTTP status code that can be
371 /// used when converting errors to HTTP responses.
372 ///
373 /// # Status Code Mapping
374 ///
375 /// - `Http`, `Serialization`, `Validation`, `BodyAlreadyConsumed`, `ParseError`: 400 (Bad Request)
376 /// - `Authentication`: 401 (Unauthorized)
377 /// - `Authorization`: 403 (Forbidden)
378 /// - `NotFound`, `TemplateNotFound`: 404 (Not Found)
379 /// - `MethodNotAllowed`: 405 (Method Not Allowed)
380 /// - `Conflict`: 409 (Conflict)
381 /// - `Database`, `Internal`, `ImproperlyConfigured`, `Other`: 500 (Internal Server Error)
382 ///
383 /// # Examples
384 ///
385 /// ```
386 /// use reinhardt_core::exception::Error;
387 ///
388 /// // Client errors (4xx)
389 /// assert_eq!(Error::Http("Bad request".to_string()).status_code(), 400);
390 /// assert_eq!(Error::Validation("Invalid input".to_string()).status_code(), 400);
391 /// assert_eq!(Error::Authentication("No token".to_string()).status_code(), 401);
392 /// assert_eq!(Error::Authorization("No access".to_string()).status_code(), 403);
393 /// assert_eq!(Error::NotFound("Resource missing".to_string()).status_code(), 404);
394 ///
395 /// // Server errors (5xx)
396 /// assert_eq!(Error::Database("Connection failed".to_string()).status_code(), 500);
397 /// assert_eq!(Error::Internal("Crash".to_string()).status_code(), 500);
398 /// assert_eq!(Error::ImproperlyConfigured("Bad config".to_string()).status_code(), 500);
399 ///
400 /// // Edge cases
401 /// assert_eq!(Error::BodyAlreadyConsumed.status_code(), 400);
402 /// assert_eq!(Error::ParseError("Invalid data".to_string()).status_code(), 400);
403 /// ```
404 ///
405 /// # Using with anyhow errors
406 ///
407 /// ```
408 /// use reinhardt_core::exception::Error;
409 /// use anyhow::anyhow;
410 ///
411 /// let anyhow_error = anyhow!("Unexpected error");
412 /// let error: Error = anyhow_error.into();
413 /// assert_eq!(error.status_code(), 500);
414 /// ```
415 pub fn status_code(&self) -> u16 {
416 match self {
417 Error::Http(_) => 400,
418 Error::Database(_) => 500,
419 Error::Serialization(_) => 400,
420 Error::Validation(_) => 400,
421 Error::Authentication(_) => 401,
422 Error::Authorization(_) => 403,
423 Error::NotFound(_) => 404,
424 Error::TemplateNotFound(_) => 404,
425 Error::MethodNotAllowed(_) => 405,
426 Error::Conflict(_) => 409,
427 Error::Internal(_) => 500,
428 Error::ImproperlyConfigured(_) => 500,
429 Error::BodyAlreadyConsumed => 400,
430 Error::ParseError(_) => 400,
431 Error::MissingContentType => 400,
432 Error::InvalidPage(_) => 400,
433 Error::InvalidCursor(_) => 400,
434 Error::InvalidLimit(_) => 400,
435 Error::MissingParameter(_) => 400,
436 Error::ParamValidation(_) => 400,
437 Error::Other(_) => 500,
438 }
439 }
440
441 /// Returns the categorical `ErrorKind` for this error.
442 pub fn kind(&self) -> ErrorKind {
443 match self {
444 Error::Http(_) => ErrorKind::Http,
445 Error::Database(_) => ErrorKind::Database,
446 Error::Serialization(_) => ErrorKind::Serialization,
447 Error::Validation(_) => ErrorKind::Validation,
448 Error::Authentication(_) => ErrorKind::Authentication,
449 Error::Authorization(_) => ErrorKind::Authorization,
450 Error::NotFound(_) => ErrorKind::NotFound,
451 Error::TemplateNotFound(_) => ErrorKind::NotFound,
452 Error::MethodNotAllowed(_) => ErrorKind::MethodNotAllowed,
453 Error::Conflict(_) => ErrorKind::Conflict,
454 Error::Internal(_) => ErrorKind::Internal,
455 Error::ImproperlyConfigured(_) => ErrorKind::ImproperlyConfigured,
456 Error::BodyAlreadyConsumed => ErrorKind::BodyAlreadyConsumed,
457 Error::ParseError(_) => ErrorKind::Parse,
458 Error::MissingContentType => ErrorKind::Http,
459 Error::InvalidPage(_) => ErrorKind::Validation,
460 Error::InvalidCursor(_) => ErrorKind::Validation,
461 Error::InvalidLimit(_) => ErrorKind::Validation,
462 Error::MissingParameter(_) => ErrorKind::Validation,
463 Error::ParamValidation(_) => ErrorKind::ParamValidation,
464 Error::Other(_) => ErrorKind::Other,
465 }
466 }
467}
468
469// Common conversions to the unified Error without introducing cross-crate deps.
470impl From<serde_json::Error> for Error {
471 fn from(err: serde_json::Error) -> Self {
472 Error::Serialization(err.to_string())
473 }
474}
475
476impl From<std::io::Error> for Error {
477 fn from(err: std::io::Error) -> Self {
478 Error::Internal(format!("IO error: {}", err))
479 }
480}
481
482impl From<http::Error> for Error {
483 fn from(err: http::Error) -> Self {
484 Error::Http(err.to_string())
485 }
486}
487
488impl From<String> for Error {
489 fn from(msg: String) -> Self {
490 Error::Internal(msg)
491 }
492}
493
494impl From<&str> for Error {
495 fn from(msg: &str) -> Self {
496 Error::Internal(msg.to_string())
497 }
498}
499
500#[cfg(feature = "validators")]
501impl From<crate::validators::ValidationErrors> for Error {
502 fn from(err: crate::validators::ValidationErrors) -> Self {
503 Error::Validation(format!("Validation failed: {}", err))
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn test_error_kind_mapping() {
513 // HTTP errors
514 assert_eq!(Error::Http("test".to_string()).kind(), ErrorKind::Http);
515 assert_eq!(Error::MissingContentType.kind(), ErrorKind::Http);
516
517 // Database errors
518 assert_eq!(
519 Error::Database("test".to_string()).kind(),
520 ErrorKind::Database
521 );
522
523 // Serialization errors
524 assert_eq!(
525 Error::Serialization("test".to_string()).kind(),
526 ErrorKind::Serialization
527 );
528
529 // Validation errors
530 assert_eq!(
531 Error::Validation("test".to_string()).kind(),
532 ErrorKind::Validation
533 );
534 assert_eq!(
535 Error::InvalidPage("test".to_string()).kind(),
536 ErrorKind::Validation
537 );
538 assert_eq!(
539 Error::InvalidCursor("test".to_string()).kind(),
540 ErrorKind::Validation
541 );
542 assert_eq!(
543 Error::InvalidLimit("test".to_string()).kind(),
544 ErrorKind::Validation
545 );
546 assert_eq!(
547 Error::MissingParameter("test".to_string()).kind(),
548 ErrorKind::Validation
549 );
550
551 // Authentication errors
552 assert_eq!(
553 Error::Authentication("test".to_string()).kind(),
554 ErrorKind::Authentication
555 );
556
557 // Authorization errors
558 assert_eq!(
559 Error::Authorization("test".to_string()).kind(),
560 ErrorKind::Authorization
561 );
562
563 // NotFound errors
564 assert_eq!(
565 Error::NotFound("test".to_string()).kind(),
566 ErrorKind::NotFound
567 );
568 assert_eq!(
569 Error::TemplateNotFound("test".to_string()).kind(),
570 ErrorKind::NotFound
571 );
572
573 // MethodNotAllowed errors
574 assert_eq!(
575 Error::MethodNotAllowed("test".to_string()).kind(),
576 ErrorKind::MethodNotAllowed
577 );
578
579 // Internal errors
580 assert_eq!(
581 Error::Internal("test".to_string()).kind(),
582 ErrorKind::Internal
583 );
584
585 // ImproperlyConfigured errors
586 assert_eq!(
587 Error::ImproperlyConfigured("test".to_string()).kind(),
588 ErrorKind::ImproperlyConfigured
589 );
590
591 // BodyAlreadyConsumed errors
592 assert_eq!(
593 Error::BodyAlreadyConsumed.kind(),
594 ErrorKind::BodyAlreadyConsumed
595 );
596
597 // Parse errors
598 assert_eq!(
599 Error::ParseError("test".to_string()).kind(),
600 ErrorKind::Parse
601 );
602
603 // Other errors
604 assert_eq!(
605 Error::Other(anyhow::anyhow!("test")).kind(),
606 ErrorKind::Other
607 );
608 }
609
610 #[test]
611 fn test_from_serde_json_error() {
612 let json_error = serde_json::from_str::<i32>("invalid").unwrap_err();
613 let error: Error = json_error.into();
614
615 assert_eq!(error.status_code(), 400);
616 assert_eq!(error.kind(), ErrorKind::Serialization);
617 assert!(error.to_string().contains("Serialization error"));
618 }
619
620 #[test]
621 fn test_from_io_error() {
622 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
623 let error: Error = io_error.into();
624
625 assert_eq!(error.status_code(), 500);
626 assert_eq!(error.kind(), ErrorKind::Internal);
627 assert!(error.to_string().contains("IO error"));
628 }
629
630 #[test]
631 fn test_status_codes_comprehensive() {
632 // 400 errors
633 assert_eq!(Error::Http("test".to_string()).status_code(), 400);
634 assert_eq!(Error::Serialization("test".to_string()).status_code(), 400);
635 assert_eq!(Error::Validation("test".to_string()).status_code(), 400);
636 assert_eq!(Error::BodyAlreadyConsumed.status_code(), 400);
637 assert_eq!(Error::ParseError("test".to_string()).status_code(), 400);
638 assert_eq!(Error::MissingContentType.status_code(), 400);
639 assert_eq!(Error::InvalidPage("test".to_string()).status_code(), 400);
640 assert_eq!(Error::InvalidCursor("test".to_string()).status_code(), 400);
641 assert_eq!(Error::InvalidLimit("test".to_string()).status_code(), 400);
642 assert_eq!(
643 Error::MissingParameter("test".to_string()).status_code(),
644 400
645 );
646
647 // 401 error
648 assert_eq!(Error::Authentication("test".to_string()).status_code(), 401);
649
650 // 403 error
651 assert_eq!(Error::Authorization("test".to_string()).status_code(), 403);
652
653 // 404 errors
654 assert_eq!(Error::NotFound("test".to_string()).status_code(), 404);
655 assert_eq!(
656 Error::TemplateNotFound("test".to_string()).status_code(),
657 404
658 );
659
660 // 405 error
661 assert_eq!(
662 Error::MethodNotAllowed("test".to_string()).status_code(),
663 405
664 );
665
666 // 500 errors
667 assert_eq!(Error::Database("test".to_string()).status_code(), 500);
668 assert_eq!(Error::Internal("test".to_string()).status_code(), 500);
669 assert_eq!(
670 Error::ImproperlyConfigured("test".to_string()).status_code(),
671 500
672 );
673 assert_eq!(Error::Other(anyhow::anyhow!("test")).status_code(), 500);
674 }
675
676 #[test]
677 fn test_template_not_found_error() {
678 let error = Error::TemplateNotFound("index.html".to_string());
679 assert_eq!(error.status_code(), 404);
680 assert_eq!(error.kind(), ErrorKind::NotFound);
681 assert!(error.to_string().contains("Template not found"));
682 assert!(error.to_string().contains("index.html"));
683 }
684
685 #[test]
686 fn test_pagination_errors() {
687 let page_error = Error::InvalidPage("page must be positive".to_string());
688 assert_eq!(page_error.status_code(), 400);
689 assert_eq!(page_error.kind(), ErrorKind::Validation);
690
691 let cursor_error = Error::InvalidCursor("invalid base64".to_string());
692 assert_eq!(cursor_error.status_code(), 400);
693 assert_eq!(cursor_error.kind(), ErrorKind::Validation);
694
695 let limit_error = Error::InvalidLimit("limit too large".to_string());
696 assert_eq!(limit_error.status_code(), 400);
697 assert_eq!(limit_error.kind(), ErrorKind::Validation);
698 }
699
700 #[test]
701 fn test_missing_parameter_error() {
702 let error = Error::MissingParameter("user_id".to_string());
703 assert_eq!(error.status_code(), 400);
704 assert_eq!(error.kind(), ErrorKind::Validation);
705 assert!(error.to_string().contains("Missing parameter"));
706 assert!(error.to_string().contains("user_id"));
707 }
708}