use crate::pagination::ResultPagination;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum ErrorCode {
Success = 0,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
Conflict = 409,
ValidationError = 422,
InternalError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
}
impl ErrorCode {
pub fn as_i32(self) -> i32 {
self as i32
}
pub fn as_str(self) -> &'static str {
match self {
ErrorCode::Success => "Success",
ErrorCode::BadRequest => "Bad Request",
ErrorCode::Unauthorized => "Unauthorized",
ErrorCode::Forbidden => "Forbidden",
ErrorCode::NotFound => "Not Found",
ErrorCode::Conflict => "Conflict",
ErrorCode::ValidationError => "Unprocessable Entity",
ErrorCode::InternalError => "Internal Server Error",
ErrorCode::NotImplemented => "Not Implemented",
ErrorCode::BadGateway => "Bad Gateway",
ErrorCode::ServiceUnavailable => "Service Unavailable",
}
}
pub fn from_i32(value: i32) -> Option<Self> {
match value {
0 => Some(ErrorCode::Success),
400 => Some(ErrorCode::BadRequest),
401 => Some(ErrorCode::Unauthorized),
403 => Some(ErrorCode::Forbidden),
404 => Some(ErrorCode::NotFound),
409 => Some(ErrorCode::Conflict),
422 => Some(ErrorCode::ValidationError),
500 => Some(ErrorCode::InternalError),
501 => Some(ErrorCode::NotImplemented),
502 => Some(ErrorCode::BadGateway),
503 => Some(ErrorCode::ServiceUnavailable),
_ => None,
}
}
#[cfg(feature = "axum")]
pub fn to_axum_status(self) -> axum::http::StatusCode {
match self {
ErrorCode::Success => axum::http::StatusCode::OK,
ErrorCode::BadRequest => axum::http::StatusCode::BAD_REQUEST,
ErrorCode::Unauthorized => axum::http::StatusCode::UNAUTHORIZED,
ErrorCode::Forbidden => axum::http::StatusCode::FORBIDDEN,
ErrorCode::NotFound => axum::http::StatusCode::NOT_FOUND,
ErrorCode::Conflict => axum::http::StatusCode::CONFLICT,
ErrorCode::ValidationError => axum::http::StatusCode::UNPROCESSABLE_ENTITY,
ErrorCode::InternalError => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
ErrorCode::NotImplemented => axum::http::StatusCode::NOT_IMPLEMENTED,
ErrorCode::BadGateway => axum::http::StatusCode::BAD_GATEWAY,
ErrorCode::ServiceUnavailable => axum::http::StatusCode::SERVICE_UNAVAILABLE,
}
}
#[cfg(feature = "actix")]
pub fn to_actix_status(self) -> actix_web::http::StatusCode {
match self {
ErrorCode::Success => actix_web::http::StatusCode::OK,
ErrorCode::BadRequest => actix_web::http::StatusCode::BAD_REQUEST,
ErrorCode::Unauthorized => actix_web::http::StatusCode::UNAUTHORIZED,
ErrorCode::Forbidden => actix_web::http::StatusCode::FORBIDDEN,
ErrorCode::NotFound => actix_web::http::StatusCode::NOT_FOUND,
ErrorCode::Conflict => actix_web::http::StatusCode::CONFLICT,
ErrorCode::ValidationError => actix_web::http::StatusCode::UNPROCESSABLE_ENTITY,
ErrorCode::InternalError => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
ErrorCode::NotImplemented => actix_web::http::StatusCode::NOT_IMPLEMENTED,
ErrorCode::BadGateway => actix_web::http::StatusCode::BAD_GATEWAY,
ErrorCode::ServiceUnavailable => actix_web::http::StatusCode::SERVICE_UNAVAILABLE,
}
}
}
#[derive(Error, Debug)]
pub struct ApiError {
pub code: ErrorCode,
pub message: String,
}
impl ApiError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(ErrorCode::NotFound, message)
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(ErrorCode::BadRequest, message)
}
pub fn unauthorized(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Unauthorized, message)
}
pub fn forbidden(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Forbidden, message)
}
pub fn validation(message: impl Into<String>) -> Self {
Self::new(ErrorCode::ValidationError, message)
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Conflict, message)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InternalError, message)
}
pub fn code(&self) -> ErrorCode {
self.code
}
pub fn message(&self) -> &str {
&self.message
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.code.as_str(), self.message)
}
}
#[derive(Error, Debug)]
pub enum AppError {
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Forbidden: {0}")]
Forbidden(String),
#[error("Conflict: {0}")]
Conflict(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Internal error: {0}")]
Internal(#[from] anyhow::Error),
#[error("Unknown error: {0}")]
Unknown(String),
}
impl AppError {
pub fn to_error_code(&self) -> ErrorCode {
match self {
AppError::NotFound(_) => ErrorCode::NotFound,
AppError::BadRequest(_) => ErrorCode::BadRequest,
AppError::Unauthorized(_) => ErrorCode::Unauthorized,
AppError::Forbidden(_) => ErrorCode::Forbidden,
AppError::Conflict(_) => ErrorCode::Conflict,
AppError::Validation(_) => ErrorCode::ValidationError,
AppError::Internal(_) => ErrorCode::InternalError,
AppError::Unknown(_) => ErrorCode::InternalError,
}
}
}
impl From<ApiError> for AppError {
fn from(err: ApiError) -> Self {
match err.code {
ErrorCode::NotFound => AppError::NotFound(err.message),
ErrorCode::BadRequest => AppError::BadRequest(err.message),
ErrorCode::Unauthorized => AppError::Unauthorized(err.message),
ErrorCode::Forbidden => AppError::Forbidden(err.message),
ErrorCode::Conflict => AppError::Conflict(err.message),
ErrorCode::ValidationError => AppError::Validation(err.message),
_ => AppError::Internal(anyhow::anyhow!(err.message)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ResponseData<T> {
Single(T),
Multiple(Vec<T>),
}
impl<T> ResponseData<T> {
pub fn single(v: T) -> Self {
ResponseData::Single(v)
}
pub fn multiple(v: Vec<T>) -> Self {
ResponseData::Multiple(v)
}
pub fn is_single(&self) -> bool {
matches!(self, ResponseData::Single(_))
}
pub fn is_multiple(&self) -> bool {
matches!(self, ResponseData::Multiple(_))
}
pub fn as_single(&self) -> Option<&T> {
match self {
ResponseData::Single(v) => Some(v),
ResponseData::Multiple(_) => None,
}
}
pub fn as_multiple(&self) -> Option<&Vec<T>> {
match self {
ResponseData::Single(_) => None,
ResponseData::Multiple(v) => Some(v),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiResult<T> {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<ResponseData<T>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pagination: Option<ResultPagination>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<Value>,
}
impl<T> ApiResult<T> {
pub fn value(v: T) -> Self {
ApiResult {
success: true,
data: Some(ResponseData::single(v)),
message: None,
code: None,
pagination: None,
extra: None,
}
}
pub fn list(v: Vec<T>) -> Self {
ApiResult {
success: true,
data: Some(ResponseData::multiple(v)),
message: None,
code: None,
pagination: None,
extra: None,
}
}
pub fn fail(message: &str) -> Self {
Self::failure(message)
}
pub fn failure(message: &str) -> Self {
ApiResult {
success: false,
data: None,
message: Some(message.to_string()),
code: None,
pagination: None,
extra: None,
}
}
pub fn success() -> Self {
Self::ok()
}
pub fn ok() -> Self {
ApiResult {
success: true,
data: None,
message: None,
code: None,
pagination: None,
extra: None,
}
}
pub fn with_extra(mut self, key: &str, value: Value) -> Self {
match self.extra {
Some(ref mut v) => {
v[key] = value;
}
None => {
let mut v = serde_json::Map::new();
v.insert(key.to_string(), value);
self.extra = Some(v.into());
}
}
self
}
pub fn with_code(mut self, code: i32) -> Self {
self.code = Some(code);
self
}
pub fn with_pagination(mut self, pagination: ResultPagination) -> Self {
self.pagination = Some(pagination);
self
}
pub fn with_message(mut self, message: &str) -> Self {
self.message = Some(message.to_string());
self
}
}
impl<T, E> Into<Result<ApiResult<T>, E>> for ApiResult<T> {
fn into(self) -> Result<ApiResult<T>, E> {
Ok(self)
}
}
impl<T> From<ApiError> for ApiResult<T> {
fn from(err: ApiError) -> Self {
ApiResult {
success: false,
data: None,
message: Some(err.message),
code: Some(err.code.as_i32()),
pagination: None,
extra: None,
}
}
}
impl<T> From<AppError> for ApiResult<T> {
fn from(err: AppError) -> Self {
ApiResult {
success: false,
data: None,
message: Some(err.to_string()),
code: Some(err.to_error_code().as_i32()),
pagination: None,
extra: None,
}
}
}
impl<T> From<anyhow::Error> for ApiResult<T> {
fn from(err: anyhow::Error) -> Self {
ApiResult {
success: false,
data: None,
message: Some(err.to_string()),
code: Some(ErrorCode::InternalError.as_i32()),
pagination: None,
extra: None,
}
}
}
impl<T, E: std::fmt::Display + std::fmt::Debug> From<Result<T, E>> for ApiResult<T> {
fn from(result: Result<T, E>) -> Self {
match result {
Ok(data) => ApiResult::value(data),
Err(err) => ApiResult {
success: false,
data: None,
message: Some(format!("{}", err)),
code: Some(ErrorCode::InternalError.as_i32()),
pagination: None,
extra: None,
},
}
}
}
pub trait IntoApiResult<T> {
fn into_api_result_with<F>(self, f: F) -> ApiResult<T>
where
F: FnOnce() -> ApiError;
fn into_api_result(self) -> ApiResult<T>;
}
impl<T, E: Into<ApiError>> IntoApiResult<T> for Result<T, E> {
fn into_api_result_with<F>(self, f: F) -> ApiResult<T>
where
F: FnOnce() -> ApiError,
{
match self {
Ok(data) => ApiResult::value(data),
Err(_) => ApiResult::from(f()),
}
}
fn into_api_result(self) -> ApiResult<T> {
match self {
Ok(data) => ApiResult::value(data),
Err(err) => ApiResult::from(err.into()),
}
}
}
pub type DefaultResult<T> = Result<T, Box<dyn std::error::Error>>;
pub type AnyhowResult<T> = Result<T, anyhow::Error>;
pub type EmptyResult = ApiResult<()>;