use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde_json::json;
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("not found: {0}")]
NotFound(String),
#[error("validation error: {0}")]
Validation(String),
#[error("conflict: {0}")]
Conflict(String),
#[error(transparent)]
Database(#[from] sqlx::Error),
#[error(transparent)]
Serialization(#[from] serde_json::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl AppError {
pub fn code(&self) -> &'static str {
match self {
AppError::NotFound(_) => "not_found",
AppError::Validation(_) => "validation_error",
AppError::Conflict(_) => "conflict",
AppError::Database(_) => "database_error",
AppError::Serialization(_) | AppError::Other(_) => "internal_error",
}
}
fn status(&self) -> StatusCode {
match self {
AppError::NotFound(_) => StatusCode::NOT_FOUND,
AppError::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
AppError::Conflict(_) => StatusCode::CONFLICT,
AppError::Database(_) | AppError::Serialization(_) | AppError::Other(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = self.status();
let message = match &self {
AppError::Database(_) | AppError::Serialization(_) | AppError::Other(_) => {
tracing::error!(error = %self, "internal error");
"internal server error".to_string()
}
other => other.to_string(),
};
let body = Json(json!({
"error": {
"code": self.code(),
"message": message,
}
}));
(status, body).into_response()
}
}
pub type AppResult<T> = Result<T, AppError>;