Skip to main content

rok_utils/errors/
kinds.rs

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