//! API errors: a stable machine code the interface translates, never a stack. use axum::Json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde_json::json; #[derive(Debug, thiserror::Error)] pub enum AppError { #[error("unauthenticated")] Unauthenticated, #[error("forbidden")] Forbidden, #[error("not found")] NotFound, #[error("invalid: {0}")] Invalid(&'static str), #[error("conflict: {0}")] Conflict(&'static str), #[error("unavailable: {0}")] Unavailable(&'static str), #[error(transparent)] Db(#[from] sqlx::Error), #[error(transparent)] Other(#[from] anyhow::Error), } pub type AppResult = Result; impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, code) = match &self { Self::Unauthenticated => (StatusCode::UNAUTHORIZED, "unauthenticated"), Self::Forbidden => (StatusCode::FORBIDDEN, "forbidden"), Self::NotFound => (StatusCode::NOT_FOUND, "not_found"), Self::Invalid(code) => (StatusCode::BAD_REQUEST, *code), Self::Conflict(code) => (StatusCode::CONFLICT, *code), Self::Unavailable(code) => (StatusCode::SERVICE_UNAVAILABLE, *code), Self::Db(e) => { tracing::error!(error = %e, "database"); (StatusCode::INTERNAL_SERVER_ERROR, "internal") } Self::Other(e) => { tracing::error!(error = %e, "internal"); (StatusCode::INTERNAL_SERVER_ERROR, "internal") } }; (status, Json(json!({ "error": code }))).into_response() } }