use thiserror::Error;
use crate::errors::context::wrap_error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RokError {
#[error("[E_INVALID_UTF8] Invalid UTF-8 input: {0}")]
InvalidUtf8(String),
#[error("[E_INVALID_BASE64] Cannot decode base64 string: {0}")]
InvalidBase64(String),
#[error("[E_PARSE_BYTES] Cannot parse byte expression '{expr}': {reason}")]
ParseBytes { expr: String, reason: String },
#[error("[E_PARSE_DURATION] Cannot parse duration expression '{expr}': {reason}")]
ParseDuration { expr: String, reason: String },
#[error("[E_INVALID_JSON] JSON parse failed: {0}")]
InvalidJson(String),
#[error("[E_INVALID_UUID] '{0}' is not a valid UUID")]
InvalidUuid(String),
#[error("[E_INVALID_DATE] Cannot parse date '{0}'")]
InvalidDate(String),
#[error("[E_NOT_FOUND] Resource not found: {0}")]
NotFound(String),
#[error("[E_UNAUTHORIZED] Unauthorized: {0}")]
Unauthorized(String),
#[error("[E_FORBIDDEN] Forbidden: {0}")]
Forbidden(String),
#[error("[E_VALIDATION_FAILURE] Validation failed: {field} — {reason}")]
ValidationFailure { field: String, reason: String },
#[error("[E_TOO_MANY_REQUESTS] Rate limit exceeded")]
TooManyRequests,
#[error("[E_INTERNAL] Internal error: {0}")]
Internal(String),
#[error("[E_WRAPPED] {message}: {source}")]
Wrapped {
message: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
}
impl RokError {
pub fn code(&self) -> &'static str {
match self {
Self::InvalidUtf8(_) => "E_INVALID_UTF8",
Self::InvalidBase64(_) => "E_INVALID_BASE64",
Self::ParseBytes { .. } => "E_PARSE_BYTES",
Self::ParseDuration { .. } => "E_PARSE_DURATION",
Self::InvalidJson(_) => "E_INVALID_JSON",
Self::InvalidUuid(_) => "E_INVALID_UUID",
Self::InvalidDate(_) => "E_INVALID_DATE",
Self::NotFound(_) => "E_NOT_FOUND",
Self::Unauthorized(_) => "E_UNAUTHORIZED",
Self::Forbidden(_) => "E_FORBIDDEN",
Self::ValidationFailure { .. } => "E_VALIDATION_FAILURE",
Self::TooManyRequests => "E_TOO_MANY_REQUESTS",
Self::Internal(_) => "E_INTERNAL",
Self::Wrapped { .. } => "E_WRAPPED",
}
}
pub fn status(&self) -> u16 {
match self {
Self::NotFound(_) => 404,
Self::Unauthorized(_) => 401,
Self::Forbidden(_) => 403,
Self::ValidationFailure { .. } => 422,
Self::TooManyRequests => 429,
_ => 500,
}
}
pub fn is_self_handled(&self) -> bool {
matches!(
self,
Self::NotFound(_)
| Self::Unauthorized(_)
| Self::Forbidden(_)
| Self::ValidationFailure { .. }
| Self::TooManyRequests
)
}
}
pub trait ResultExt<T> {
fn context(self, msg: &str) -> Result<T, RokError>;
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, RokError>;
fn map_rok_err(self, variant: fn(String) -> RokError) -> Result<T, RokError>;
fn or_not_found(self, resource: &str) -> Result<T, RokError>;
}
impl<T, E: std::error::Error + Send + Sync + 'static> ResultExt<T> for Result<T, E> {
fn context(self, msg: &str) -> Result<T, RokError> {
self.map_err(|e| wrap_error(e, msg))
}
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, RokError> {
self.map_err(|e| wrap_error(e, f()))
}
fn map_rok_err(self, variant: fn(String) -> RokError) -> Result<T, RokError> {
self.map_err(|e| variant(e.to_string()))
}
fn or_not_found(self, resource: &str) -> Result<T, RokError> {
self.map_err(|_| RokError::NotFound(resource.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_code() {
let err = RokError::NotFound("User #42".into());
assert_eq!(err.code(), "E_NOT_FOUND");
}
#[test]
fn error_status() {
let err = RokError::NotFound("User #42".into());
assert_eq!(err.status(), 404);
}
#[test]
fn is_self_handled() {
assert!(RokError::NotFound("test".into()).is_self_handled());
assert!(!RokError::Internal("test".into()).is_self_handled());
}
}