# Error Handling — `errors.rs`

Inspired by **AdonisJS's typed exception system** (`E_ROUTE_NOT_FOUND`, `E_UNAUTHORIZED_ACCESS`, `E_HTTP_EXCEPTION`, etc.) and **Laravel's `HttpException`**. Every error variant carries a machine-readable code string matching the AdonisJS convention.

## 5.1 Core Error Enum — `errors/kinds.rs`

```rust
use thiserror::Error;

/// All rok-utils errors. Each variant maps to an AdonisJS-style error code.
#[derive(Debug, Error)]
pub enum RokError {
    // ── String errors ──────────────────────────────────────────────
    #[error("[E_INVALID_UTF8] Invalid UTF-8 input: {0}")]
    InvalidUtf8(String),

    #[error("[E_INVALID_BASE64] Cannot decode base64 string: {0}")]
    InvalidBase64(String),

    // ── Parse errors ───────────────────────────────────────────────
    #[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 },

    // ── Data errors ────────────────────────────────────────────────
    #[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),

    // ── HTTP-style errors (AdonisJS parity) ────────────────────────
    #[error("[E_NOT_FOUND] Resource not found: {0}")]
    NotFound(String),                             // HTTP 404

    #[error("[E_UNAUTHORIZED] Unauthorized: {0}")]
    Unauthorized(String),                         // HTTP 401

    #[error("[E_FORBIDDEN] Forbidden: {0}")]
    Forbidden(String),                            // HTTP 403

    #[error("[E_VALIDATION_FAILURE] Validation failed: {field} — {reason}")]
    ValidationFailure { field: String, reason: String },  // HTTP 422

    #[error("[E_TOO_MANY_REQUESTS] Rate limit exceeded")]
    TooManyRequests,                              // HTTP 429

    #[error("[E_INTERNAL] Internal error: {0}")]
    Internal(String),                             // HTTP 500

    // ── Generic wrapped error ──────────────────────────────────────
    #[error("[E_WRAPPED] {message}: {source}")]
    Wrapped {
        message: String,
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
    },
}

impl RokError {
    /// AdonisJS-style error code string
    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",
        }
    }

    /// HTTP status code — compatible with AdonisJS status semantics
    pub fn status(&self) -> u16 {
        match self {
            Self::NotFound(_)            => 404,
            Self::Unauthorized(_)        => 401,
            Self::Forbidden(_)           => 403,
            Self::ValidationFailure{..}  => 422,
            Self::TooManyRequests        => 429,
            _                            => 500,
        }
    }

    /// Whether this error is recoverable / self-handled
    pub fn is_self_handled(&self) -> bool {
        matches!(self,
            Self::NotFound(_)
            | Self::Unauthorized(_)
            | Self::Forbidden(_)
            | Self::ValidationFailure { .. }
            | Self::TooManyRequests
        )
    }
}
```

## 5.2 Context & Wrapping — `errors/context.rs`

```rust
/// Add context to any error (AdonisJS: error.help / error.cause pattern)
pub fn wrap_error<E>(error: E, message: impl Into<String>) -> RokError
where E: std::error::Error + Send + Sync + 'static

/// Extension trait on Result — Laravel-style ergonomics
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()))
    }
}
```

### Usage

```rust
use rok_utils::errors::{RokError, ResultExt};

fn find_user(id: u64) -> Result<User, RokError> {
    db::find(id)
        .context("Failed to query user table")
        .or_not_found(&format!("User #{id}"))
}

// Error codes match AdonisJS convention:
let err = RokError::NotFound("User #42".into());
assert_eq!(err.code(), "E_NOT_FOUND");
assert_eq!(err.status(), 404);
assert!(err.is_self_handled());
```

## 5.3 Error Macros — `errors/macros.rs`

```rust
/// Quickly build a validation error
macro_rules! validation_err {
    ($field:expr, $reason:expr) => {
        RokError::ValidationFailure {
            field: $field.to_string(),
            reason: $reason.to_string(),
        }
    };
}

/// Bail early from a function with a RokError
macro_rules! rok_bail {
    ($variant:ident, $msg:expr) => {
        return Err(RokError::$variant($msg.to_string()))
    };
}
```