Skip to main content

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}