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
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}