use std::{fmt::Display, panic::Location};
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
use thiserror::Error;
use validator::ValidationErrors;
pub type AppResult<T> = std::result::Result<T, AppError>;
#[derive(Error, Debug)]
pub enum AppError {
#[error("{0}")]
ValidationError(String), #[error("Unauthorized")]
Unauthorized, #[error("Token Expired")]
TokenExpired,
#[error("Forbidden")]
Forbidden, #[error("Resource not found: {0}")]
NotFound(String), #[error("Request conflict: {0}")]
Conflict(String), #[error("{0}")]
ClientError(String), #[error("{0}")]
ClientDataError(String),
#[error("Business rule validation failed: {0}")]
UnprocessableEntity(String), #[error("Rate limit exceeded: {0}")]
RateLimit(String), #[error("{0}")]
EasterEgg(String),
#[error("Database error: {0}")]
DbError(String), #[error("Redis error: {0}")]
RedisError(String), #[error("Message queue error: {0}")]
MqError(String), #[error("External service error: {0}")]
ExternalError(String), #[error("Internal server error")]
Internal(String),
#[error("{1}")]
DataError(u32, String),
#[error("{0}")]
JsonError(String), }
#[derive(Serialize)]
pub struct ApiResponse<T> {
pub code: u32, pub message: String, pub data: Option<T>, }
impl AppError {
const HTTP_BAD_REQUEST: StatusCode = StatusCode::BAD_REQUEST; const HTTP_UNAUTHORIZED: StatusCode = StatusCode::UNAUTHORIZED; const HTTP_FORBIDDEN: StatusCode = StatusCode::FORBIDDEN; const HTTP_NOT_FOUND: StatusCode = StatusCode::NOT_FOUND; const HTTP_CONFLICT: StatusCode = StatusCode::CONFLICT; const HTTP_UNPROCESSABLE_ENTITY: StatusCode = StatusCode::UNPROCESSABLE_ENTITY; const HTTP_TOO_MANY_REQUESTS: StatusCode = StatusCode::TOO_MANY_REQUESTS; const HTTP_IM_A_TEAPOT: StatusCode = StatusCode::IM_A_TEAPOT; const EXPECTATION_FAILED: StatusCode = StatusCode::EXPECTATION_FAILED; const HTTP_INTERNAL_ERROR: StatusCode = StatusCode::INTERNAL_SERVER_ERROR;
const BIZ_VALIDATION_ERROR: u32 = 400001;
const BIZ_UNAUTHORIZED: u32 = 400002;
const BIZ_FORBIDDEN: u32 = 400003;
const BIZ_NOT_FOUND: u32 = 400004;
const BIZ_CONFLICT: u32 = 400005;
const BIZ_CLIENT_ERROR: u32 = 400006;
const BIZ_DATA_ERROR: u32 = 400007;
const BIZ_TOKEN_EXPIRED: u32 = 400008;
const BIZ_DB_ERROR: u32 = 500001;
const BIZ_REDIS_ERROR: u32 = 500002;
const BIZ_MQ_ERROR: u32 = 500003;
const BIZ_EXTERNAL_ERROR: u32 = 500004;
const BIZ_INTERNAL_ERROR: u32 = 500000;
const BIZ_UNPROCESSABLE_ENTITY: u32 = 400100; const BIZ_RATE_LIMIT: u32 = 400101; const BIZ_EASTER_EGG: u32 = 400102;
pub const BIZ_DATA_EXISTS: u32 = 410000; pub const BIZ_DATA_DUPLICATE: u32 = 410001; pub const BIZ_DATA_NOT_FOUND: u32 = 410002; pub const BIZ_DATA_DELETED: u32 = 410003; pub const BIZ_DATA_ARCHIVED: u32 = 410004; pub const BIZ_DATA_OUTDATED: u32 = 410005;
pub const BIZ_JSON_ERROR: u32 = 410100;
pub fn status_code(&self) -> StatusCode {
match self {
Self::ValidationError(_) => Self::HTTP_BAD_REQUEST,
Self::Unauthorized => Self::HTTP_UNAUTHORIZED,
Self::TokenExpired => Self::HTTP_UNAUTHORIZED,
Self::Forbidden => Self::HTTP_FORBIDDEN,
Self::NotFound(_) => Self::HTTP_NOT_FOUND,
Self::Conflict(_) => Self::HTTP_CONFLICT,
Self::UnprocessableEntity(_) => Self::HTTP_UNPROCESSABLE_ENTITY,
Self::RateLimit(_) => Self::HTTP_TOO_MANY_REQUESTS,
Self::EasterEgg(_) => Self::HTTP_IM_A_TEAPOT,
Self::Internal(_) => Self::HTTP_INTERNAL_ERROR,
Self::ClientError(_) => Self::EXPECTATION_FAILED,
Self::DataError(_, _) => Self::HTTP_CONFLICT, _ => Self::HTTP_BAD_REQUEST,
}
}
pub fn business_code(&self) -> u32 {
match self {
Self::ValidationError(_) => Self::BIZ_VALIDATION_ERROR,
Self::Unauthorized => Self::BIZ_UNAUTHORIZED,
Self::TokenExpired => Self::BIZ_TOKEN_EXPIRED,
Self::Forbidden => Self::BIZ_FORBIDDEN,
Self::NotFound(_) => Self::BIZ_NOT_FOUND,
Self::Conflict(_) => Self::BIZ_CONFLICT,
Self::UnprocessableEntity(_) => Self::BIZ_UNPROCESSABLE_ENTITY,
Self::RateLimit(_) => Self::BIZ_RATE_LIMIT,
Self::EasterEgg(_) => Self::BIZ_EASTER_EGG,
Self::ClientError(_) => Self::BIZ_CLIENT_ERROR,
Self::ClientDataError(_) => Self::BIZ_DATA_ERROR,
Self::DataError(code, _) => *code, Self::DbError(_) => Self::BIZ_DB_ERROR,
Self::RedisError(_) => Self::BIZ_REDIS_ERROR,
Self::MqError(_) => Self::BIZ_MQ_ERROR,
Self::ExternalError(_) => Self::BIZ_EXTERNAL_ERROR,
Self::Internal(_) => Self::BIZ_INTERNAL_ERROR,
Self::JsonError(_) => Self::BIZ_JSON_ERROR,
}
}
pub fn message(&self) -> String {
match self {
Self::UnprocessableEntity(msg) => msg.to_string(),
Self::RateLimit(msg) => format!("Rate limit exceeded: {}", msg),
Self::EasterEgg(msg) => format!("Easter egg: {}", msg),
Self::ValidationError(msg) => msg.to_string(),
Self::Unauthorized => "Unauthorized access".to_string(),
Self::TokenExpired => "Token expired".to_string(),
Self::Forbidden => "Access forbidden".to_string(),
Self::NotFound(msg) => msg.to_string(),
Self::Conflict(msg) => msg.to_string(),
Self::DbError(e) => format!("Database error: {}", e),
Self::RedisError(e) => format!("Cache error: {}", e),
Self::MqError(e) => format!("Message queue error: {}", e),
Self::ExternalError(e) => format!("External service error: {}", e),
Self::Internal(e) => format!("Internal server error: {}", e),
Self::ClientError(msg) => msg.to_string(),
Self::ClientDataError(msg) => msg.to_string(),
Self::DataError(_, msg) => msg.to_string(),
Self::JsonError(msg) => format!("JSON serialization error: {}", msg),
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = self.status_code();
let response = ApiResponse {
code: self.business_code(),
message: self.to_string(),
data: None::<()>,
};
tracing::error!(
"...App Error...: code:{:?} message:{:?} self:{:?}",
response.code,
response.message,
self
);
(status, Json(response)).into_response()
}
}
impl From<ValidationErrors> for AppError {
fn from(err: ValidationErrors) -> Self {
tracing::warn!("Parameter validation failed: {:?}", err);
let message = err
.field_errors()
.iter()
.map(|(field, errors)| {
let error_messages: Vec<String> = errors
.iter()
.filter_map(|error| error.message.as_ref().map(|m| m.to_string()))
.collect();
format!("{}: {}", field, error_messages.join(", "))
})
.collect::<Vec<String>>()
.join("; ");
AppError::ValidationError(format!("Parameter validation failed: {}", message))
}
}
#[cfg(any(feature = "diesel", feature = "full"))]
impl From<diesel::result::Error> for AppError {
fn from(err: diesel::result::Error) -> Self {
tracing::error!("Database error: {}", err);
AppError::DbError(err.to_string())
}
}
#[cfg(any(feature = "diesel", feature = "full"))]
impl From<deadpool_diesel::PoolError> for AppError {
fn from(err: deadpool_diesel::PoolError) -> Self {
tracing::error!("Deadpool_diesel Database error: {}", err);
AppError::DbError(err.to_string())
}
}
#[track_caller]
pub fn msg_with_location<M: Display>(msg: M) -> String {
let loc = Location::caller();
format!("{}:{} {}", loc.file(), loc.line(), msg)
}
impl AppError {
#[track_caller]
pub fn client_here<M: Display>(msg: M) -> Self {
AppError::ClientError(msg_with_location(msg))
}
#[track_caller]
pub fn data_here<M: Display>(msg: M) -> Self {
AppError::ClientDataError(msg_with_location(msg))
}
#[track_caller]
pub fn conflict_here<M: Display>(msg: M) -> Self {
AppError::Conflict(msg_with_location(msg))
}
#[track_caller]
pub fn not_found_here<M: Display>(msg: M) -> Self {
AppError::NotFound(msg_with_location(msg))
}
}
pub trait AppResultExt<T, E> {
#[track_caller]
fn client_context(self) -> AppResult<T>
where
E: Display;
#[track_caller]
fn context_msg(self, msg: impl Into<String>) -> AppResult<T>
where
E: Display;
}
impl<T, E> AppResultExt<T, E> for Result<T, E> {
#[track_caller]
fn client_context(self) -> AppResult<T>
where
E: Display,
{
self.map_err(|e| AppError::client_here(e))
}
#[track_caller]
fn context_msg(self, msg: impl Into<String>) -> AppResult<T>
where
E: Display,
{
self.map_err(|e| AppError::client_here(format!("{} - {}", msg.into(), e)))
}
}