rok-utils 0.2.4

Laravel/AdonisJS-inspired utility helpers for the Rok ecosystem
Documentation
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());
    }
}