rok_utils/errors/
kinds.rs1use thiserror::Error;
2
3use crate::errors::context::wrap_error;
4
5#[derive(Debug, Error)]
6#[non_exhaustive]
7pub enum RokError {
8 #[error("[E_INVALID_UTF8] Invalid UTF-8 input: {0}")]
9 InvalidUtf8(String),
10
11 #[error("[E_INVALID_BASE64] Cannot decode base64 string: {0}")]
12 InvalidBase64(String),
13
14 #[error("[E_PARSE_BYTES] Cannot parse byte expression '{expr}': {reason}")]
15 ParseBytes { expr: String, reason: String },
16
17 #[error("[E_PARSE_DURATION] Cannot parse duration expression '{expr}': {reason}")]
18 ParseDuration { expr: String, reason: String },
19
20 #[error("[E_INVALID_JSON] JSON parse failed: {0}")]
21 InvalidJson(String),
22
23 #[error("[E_INVALID_UUID] '{0}' is not a valid UUID")]
24 InvalidUuid(String),
25
26 #[error("[E_INVALID_DATE] Cannot parse date '{0}'")]
27 InvalidDate(String),
28
29 #[error("[E_NOT_FOUND] Resource not found: {0}")]
30 NotFound(String),
31
32 #[error("[E_UNAUTHORIZED] Unauthorized: {0}")]
33 Unauthorized(String),
34
35 #[error("[E_FORBIDDEN] Forbidden: {0}")]
36 Forbidden(String),
37
38 #[error("[E_VALIDATION_FAILURE] Validation failed: {field} — {reason}")]
39 ValidationFailure { field: String, reason: String },
40
41 #[error("[E_TOO_MANY_REQUESTS] Rate limit exceeded")]
42 TooManyRequests,
43
44 #[error("[E_INTERNAL] Internal error: {0}")]
45 Internal(String),
46
47 #[error("[E_WRAPPED] {message}: {source}")]
48 Wrapped {
49 message: String,
50 #[source]
51 source: Box<dyn std::error::Error + Send + Sync>,
52 },
53}
54
55impl RokError {
56 pub fn code(&self) -> &'static str {
57 match self {
58 Self::InvalidUtf8(_) => "E_INVALID_UTF8",
59 Self::InvalidBase64(_) => "E_INVALID_BASE64",
60 Self::ParseBytes { .. } => "E_PARSE_BYTES",
61 Self::ParseDuration { .. } => "E_PARSE_DURATION",
62 Self::InvalidJson(_) => "E_INVALID_JSON",
63 Self::InvalidUuid(_) => "E_INVALID_UUID",
64 Self::InvalidDate(_) => "E_INVALID_DATE",
65 Self::NotFound(_) => "E_NOT_FOUND",
66 Self::Unauthorized(_) => "E_UNAUTHORIZED",
67 Self::Forbidden(_) => "E_FORBIDDEN",
68 Self::ValidationFailure { .. } => "E_VALIDATION_FAILURE",
69 Self::TooManyRequests => "E_TOO_MANY_REQUESTS",
70 Self::Internal(_) => "E_INTERNAL",
71 Self::Wrapped { .. } => "E_WRAPPED",
72 }
73 }
74
75 pub fn status(&self) -> u16 {
76 match self {
77 Self::NotFound(_) => 404,
78 Self::Unauthorized(_) => 401,
79 Self::Forbidden(_) => 403,
80 Self::ValidationFailure { .. } => 422,
81 Self::TooManyRequests => 429,
82 _ => 500,
83 }
84 }
85
86 pub fn is_self_handled(&self) -> bool {
87 matches!(
88 self,
89 Self::NotFound(_)
90 | Self::Unauthorized(_)
91 | Self::Forbidden(_)
92 | Self::ValidationFailure { .. }
93 | Self::TooManyRequests
94 )
95 }
96}
97
98pub trait ResultExt<T> {
99 fn context(self, msg: &str) -> Result<T, RokError>;
100 fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, RokError>;
101 fn map_rok_err(self, variant: fn(String) -> RokError) -> Result<T, RokError>;
102 fn or_not_found(self, resource: &str) -> Result<T, RokError>;
103}
104
105impl<T, E: std::error::Error + Send + Sync + 'static> ResultExt<T> for Result<T, E> {
106 fn context(self, msg: &str) -> Result<T, RokError> {
107 self.map_err(|e| wrap_error(e, msg))
108 }
109
110 fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, RokError> {
111 self.map_err(|e| wrap_error(e, f()))
112 }
113
114 fn map_rok_err(self, variant: fn(String) -> RokError) -> Result<T, RokError> {
115 self.map_err(|e| variant(e.to_string()))
116 }
117
118 fn or_not_found(self, resource: &str) -> Result<T, RokError> {
119 self.map_err(|_| RokError::NotFound(resource.to_string()))
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn error_code() {
129 let err = RokError::NotFound("User #42".into());
130 assert_eq!(err.code(), "E_NOT_FOUND");
131 }
132
133 #[test]
134 fn error_status() {
135 let err = RokError::NotFound("User #42".into());
136 assert_eq!(err.status(), 404);
137 }
138
139 #[test]
140 fn is_self_handled() {
141 assert!(RokError::NotFound("test".into()).is_self_handled());
142 assert!(!RokError::Internal("test".into()).is_self_handled());
143 }
144}