use axum::{ http::StatusCode, response::{IntoResponse, Response}, Json, }; use serde::Serialize; use serde_json::Value; use std::fmt::{self, Display}; use utoipa::ToSchema; pub type AppResult = Result; #[derive(Debug)] pub struct AppError { status: StatusCode, message: String, code: Option, details: Option, } impl AppError { pub fn new(status: StatusCode, message: impl Into) -> Self { Self { status, message: message.into(), code: None, details: None, } } pub fn bad_request(message: impl Into) -> Self { Self::new(StatusCode::BAD_REQUEST, message) } pub fn conflict(message: impl Into) -> Self { Self::new(StatusCode::CONFLICT, message) } pub fn unauthorized() -> Self { Self::new(StatusCode::UNAUTHORIZED, "unauthorized") } pub fn forbidden(message: impl Into) -> Self { Self::new(StatusCode::FORBIDDEN, message) } pub fn not_found() -> Self { Self::new(StatusCode::NOT_FOUND, "resource not found") } pub fn internal(error: E) -> Self { Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()) } pub fn with_code(mut self, code: impl Into) -> Self { self.code = Some(code.into()); self } pub fn with_details(mut self, details: Value) -> Self { self.details = Some(details); self } } impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}: {}", self.status, self.message) } } impl IntoResponse for AppError { fn into_response(self) -> Response { let status = self.status; let body = Json(ApiErrorResponse { error: self.message, code: self.code, details: self.details, }); (status, body).into_response() } } #[derive(Serialize, ToSchema)] pub struct ApiErrorResponse { error: String, #[serde(skip_serializing_if = "Option::is_none")] code: Option, #[serde(skip_serializing_if = "Option::is_none")] details: Option, } impl From for AppError { fn from(value: diesel::result::Error) -> Self { match value { diesel::result::Error::NotFound => AppError::not_found(), other => { let message = format!("database operation failed: {other}"); tracing::error!(error = ?other, message); AppError::internal(message) } } } } impl From for AppError { fn from(value: jsonwebtoken::errors::Error) -> Self { AppError::internal(value) } } impl From for AppError { fn from(value: anyhow::Error) -> Self { AppError::internal(value) } } impl From for AppError { fn from(value: std::io::Error) -> Self { AppError::internal(value) } } impl From for AppError { fn from(value: serde_json::Error) -> Self { AppError::internal(value) } }