islands-actions 0.1.2

Server-side typed action plumbing for islands.rs: ActionContext, ActionError, ActionRegistry, and the /_action/:name router.
Documentation
use axum::response::{IntoResponse, Response};
use axum::http::StatusCode;
use axum::Json;
use serde_json::json;

/// Typed error enum for server actions.
///
/// `IntoResponse` maps each variant to its appropriate HTTP status + JSON body.
/// Per project rule `use-match.md`, the status mapping uses a `match` block.
#[derive(Debug)]
pub enum ActionError {
    BadRequest(String),
    Unauthorized,
    Forbidden,
    NotFound,
    Conflict(String),
    Internal(String),
}

impl std::fmt::Display for ActionError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ActionError::BadRequest(msg) => write!(f, "bad request: {msg}"),
            ActionError::Unauthorized => f.write_str("unauthorized"),
            ActionError::Forbidden => f.write_str("forbidden"),
            ActionError::NotFound => f.write_str("not found"),
            ActionError::Conflict(msg) => write!(f, "conflict: {msg}"),
            ActionError::Internal(msg) => write!(f, "internal error: {msg}"),
        }
    }
}

impl std::error::Error for ActionError {}

impl IntoResponse for ActionError {
    fn into_response(self) -> Response {
        let (status, code, message) = match &self {
            ActionError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "BAD_REQUEST", msg.as_str()),
            ActionError::Unauthorized => (StatusCode::UNAUTHORIZED, "UNAUTHORIZED", "unauthorized"),
            ActionError::Forbidden => (StatusCode::FORBIDDEN, "FORBIDDEN", "forbidden"),
            ActionError::NotFound => (StatusCode::NOT_FOUND, "NOT_FOUND", "not found"),
            ActionError::Conflict(msg) => (StatusCode::CONFLICT, "CONFLICT", msg.as_str()),
            ActionError::Internal(msg) => {
                tracing::error!(error = %msg, "action internal error");
                (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL", "internal server error")
            }
        };
        (status, Json(json!({ "error": message, "code": code }))).into_response()
    }
}