use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use thiserror::Error;
use crate::api::dto::ErrorBody;
use crate::error::JobError;
#[derive(Debug, Error)]
pub enum ApiError {
#[error("not found")]
NotFound,
#[error("bad request: {0}")]
BadRequest(String),
#[error("conflict: {0}")]
Conflict(String),
#[error("validation: {0}")]
Unprocessable(String),
#[error(transparent)]
Internal(#[from] anyhow::Error),
}
impl From<JobError> for ApiError {
fn from(e: JobError) -> Self {
match e {
JobError::NotFound(_) => ApiError::NotFound,
JobError::InvalidTransition { .. } => ApiError::Conflict(e.to_string()),
JobError::PayloadInvalid { .. } => ApiError::Unprocessable(e.to_string()),
JobError::IdempotencyConflict(_) => ApiError::Conflict(e.to_string()),
JobError::Db(err) => ApiError::Internal(anyhow::anyhow!(err)),
JobError::Serde(err) => ApiError::Unprocessable(err.to_string()),
}
}
}
impl From<sqlx::Error> for ApiError {
fn from(e: sqlx::Error) -> Self {
ApiError::Internal(anyhow::anyhow!(e))
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, slug, message) = match self {
ApiError::NotFound => (
StatusCode::NOT_FOUND,
"not_found",
"resource not found".into(),
),
ApiError::BadRequest(m) => (StatusCode::BAD_REQUEST, "bad_request", m),
ApiError::Conflict(m) => (StatusCode::CONFLICT, "conflict", m),
ApiError::Unprocessable(m) => (StatusCode::UNPROCESSABLE_ENTITY, "validation", m),
ApiError::Internal(err) => {
tracing::error!(error = %err, "internal server error");
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
"internal server error".into(),
)
}
};
(
status,
Json(ErrorBody {
error: slug,
message,
}),
)
.into_response()
}
}