awa-ui 0.6.2

Web UI and JSON API for the Awa job queue
Documentation
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde_json::json;

/// API error wrapper around AwaError, mapping to HTTP status codes.
#[derive(Debug)]
pub enum ApiError {
    Awa(awa_model::AwaError),
    ReadOnly,
    NotFound(String),
    Unauthorized(String),
}

impl From<awa_model::AwaError> for ApiError {
    fn from(err: awa_model::AwaError) -> Self {
        ApiError::Awa(err)
    }
}

impl From<sqlx::Error> for ApiError {
    fn from(err: sqlx::Error) -> Self {
        ApiError::Awa(err.into())
    }
}

impl From<serde_json::Error> for ApiError {
    fn from(err: serde_json::Error) -> Self {
        ApiError::Awa(awa_model::AwaError::Validation(err.to_string()))
    }
}

impl From<std::sync::Arc<crate::cache::CacheError>> for ApiError {
    fn from(err: std::sync::Arc<crate::cache::CacheError>) -> Self {
        // Moka wraps try_get_with errors in Arc. Extract the inner AwaError
        // if we're the only holder, otherwise create a database-level error
        // from the description.
        let cache_err = std::sync::Arc::try_unwrap(err).unwrap_or_else(|arc| (*arc).clone());
        ApiError::Awa(
            std::sync::Arc::try_unwrap(cache_err.0).unwrap_or_else(|arc| {
                awa_model::AwaError::Database(sqlx::Error::Protocol(arc.to_string()))
            }),
        )
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            ApiError::ReadOnly => (
                StatusCode::SERVICE_UNAVAILABLE,
                "awa serve is connected to a read-only database; admin actions are disabled"
                    .to_string(),
            ),
            ApiError::NotFound(message) => (StatusCode::NOT_FOUND, message),
            ApiError::Unauthorized(message) => (StatusCode::UNAUTHORIZED, message),
            ApiError::Awa(err) => match &err {
                awa_model::AwaError::JobNotFound { .. } => (StatusCode::NOT_FOUND, err.to_string()),
                awa_model::AwaError::CallbackNotFound { .. } => {
                    (StatusCode::NOT_FOUND, err.to_string())
                }
                awa_model::AwaError::Validation(_) => (StatusCode::BAD_REQUEST, err.to_string()),
                awa_model::AwaError::UniqueConflict { .. } => {
                    (StatusCode::CONFLICT, err.to_string())
                }
                awa_model::AwaError::Database(_) | awa_model::AwaError::Serialization(_) => {
                    tracing::error!(error = %err, "internal error");
                    (
                        StatusCode::INTERNAL_SERVER_ERROR,
                        "internal server error".to_string(),
                    )
                }
                _ => {
                    tracing::error!(error = %err, "internal error");
                    (
                        StatusCode::INTERNAL_SERVER_ERROR,
                        "internal server error".to_string(),
                    )
                }
            },
        };

        let body = axum::Json(json!({ "error": message }));
        (status, body).into_response()
    }
}

impl ApiError {
    pub fn read_only() -> Self {
        ApiError::ReadOnly
    }

    pub fn unauthorized(message: impl Into<String>) -> Self {
        ApiError::Unauthorized(message.into())
    }

    pub fn not_found(message: impl Into<String>) -> Self {
        ApiError::NotFound(message.into())
    }
}