use axum::{ Json, extract::rejection::{JsonRejection, PathRejection, QueryRejection}, http::StatusCode, response::{IntoResponse, Response}, }; use serde_json::json; pub enum AppError { BadRequest(&'static str), Conflict, Csv(csv::Error), Database(sqlx::Error), MethodNotAllowed, NotFound, Rejection(StatusCode, String), } impl From for AppError { fn from(e: sqlx::Error) -> Self { match e { sqlx::Error::Database(e) if e.is_unique_violation() => AppError::Conflict, sqlx::Error::RowNotFound => AppError::NotFound, e => AppError::Database(e), } } } impl From for AppError { fn from(e: csv::Error) -> Self { AppError::Csv(e) } } impl From for AppError { fn from(rejection: JsonRejection) -> Self { AppError::Rejection(rejection.status(), rejection.body_text()) } } impl From for AppError { fn from(rejection: QueryRejection) -> Self { AppError::Rejection(rejection.status(), rejection.body_text()) } } impl From for AppError { fn from(rejection: PathRejection) -> Self { AppError::Rejection(rejection.status(), rejection.body_text()) } } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match self { AppError::BadRequest(message) => (StatusCode::BAD_REQUEST, message.to_owned()), AppError::Conflict => (StatusCode::CONFLICT, "already exists".to_owned()), AppError::Csv(e) => { tracing::error!("csv error: {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, "internal server error".to_owned(), ) } AppError::Database(e) => { tracing::error!("database error: {e}"); ( StatusCode::INTERNAL_SERVER_ERROR, "internal server error".to_owned(), ) } AppError::MethodNotAllowed => ( StatusCode::METHOD_NOT_ALLOWED, "method not allowed".to_owned(), ), AppError::NotFound => (StatusCode::NOT_FOUND, "not found".to_owned()), AppError::Rejection(status, message) => (status, message), }; if status.is_client_error() { tracing::debug!(%status, error = %message, "request failed"); } (status, Json(json!({ "error": message }))).into_response() } }