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