use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; use serde_json::json; /// Every handler returns `Result<_, AppError>`; errors become /// `{"error": "..."}` with a sensible status code. #[derive(Debug)] pub struct AppError { pub status: StatusCode, pub message: String, } pub type AppResult = Result; impl AppError { pub fn new(status: StatusCode, message: impl Into) -> Self { Self { status, message: message.into() } } pub fn bad_request(m: impl Into) -> Self { Self::new(StatusCode::BAD_REQUEST, m) } pub fn unauthorized(m: impl Into) -> Self { Self::new(StatusCode::UNAUTHORIZED, m) } pub fn forbidden(m: impl Into) -> Self { Self::new(StatusCode::FORBIDDEN, m) } pub fn not_found(m: impl Into) -> Self { Self::new(StatusCode::NOT_FOUND, m) } pub fn conflict(m: impl Into) -> Self { Self::new(StatusCode::CONFLICT, m) } } impl IntoResponse for AppError { fn into_response(self) -> Response { if self.status.is_server_error() { tracing::error!("{}", self.message); } (self.status, Json(json!({ "error": self.message }))).into_response() } } impl From for AppError { fn from(e: anyhow::Error) -> Self { Self::new(StatusCode::BAD_GATEWAY, format!("{e:#}")) } } impl From for AppError { fn from(e: sqlx::Error) -> Self { Self::new(StatusCode::INTERNAL_SERVER_ERROR, format!("database error: {e}")) } } impl From for AppError { fn from(e: std::io::Error) -> Self { Self::new(StatusCode::INTERNAL_SERVER_ERROR, format!("io error: {e}")) } } impl From for AppError { fn from(e: serde_json::Error) -> Self { Self::bad_request(format!("invalid JSON: {e}")) } } impl From for AppError { fn from(e: axum::extract::multipart::MultipartError) -> Self { Self::bad_request(format!("upload failed: {e}")) } }