//! Structured API errors → JSON responses. use axum::Json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use kez_core::KezError; use serde_json::json; use thiserror::Error; #[derive(Debug, Error)] pub enum ApiError { #[error("not found")] NotFound, #[error("bad request: {0}")] BadRequest(String), #[error("conflict: {0}")] Conflict(String), #[error("forbidden: {0}")] Forbidden(String), #[error("internal: {0}")] Internal(String), } impl ApiError { fn status(&self) -> StatusCode { match self { ApiError::NotFound => StatusCode::NOT_FOUND, ApiError::BadRequest(_) => StatusCode::BAD_REQUEST, ApiError::Conflict(_) => StatusCode::CONFLICT, ApiError::Forbidden(_) => StatusCode::FORBIDDEN, ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, } } fn code(&self) -> &'static str { match self { ApiError::NotFound => "not_found", ApiError::BadRequest(_) => "bad_request", ApiError::Conflict(_) => "conflict", ApiError::Forbidden(_) => "forbidden", ApiError::Internal(_) => "internal", } } } impl IntoResponse for ApiError { fn into_response(self) -> Response { let status = self.status(); let body = Json(json!({ "error": { "code": self.code(), "message": self.to_string(), } })); (status, body).into_response() } } impl From for ApiError { fn from(e: KezError) -> Self { ApiError::BadRequest(e.to_string()) } } impl From for ApiError { fn from(e: rusqlite::Error) -> Self { ApiError::Internal(format!("db: {e}")) } } impl From for ApiError { fn from(e: serde_json::Error) -> Self { ApiError::BadRequest(format!("json: {e}")) } }