use crate::pagination::ResultPagination;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
#[non_exhaustive]
pub enum ErrorCode {
Success = 0,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
Conflict = 409,
TooManyRequests = 429,
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::TooManyRequests => "Too Many Requests",
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),
429 => Some(ErrorCode::TooManyRequests),
422 => Some(ErrorCode::ValidationError),
500 => Some(ErrorCode::InternalError),
501 => Some(ErrorCode::NotImplemented),
502 => Some(ErrorCode::BadGateway),
503 => Some(ErrorCode::ServiceUnavailable),
_ => None,
}
}
}
#[derive(Error, Debug, Clone, PartialEq)]
#[error("[{}] {}", self.code.as_str(), self.message)]
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 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 not_found(message: impl Into<String>) -> Self {
Self::new(ErrorCode::NotFound, message)
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Conflict, message)
}
pub fn too_many_requests(message: impl Into<String>) -> Self {
Self::new(ErrorCode::TooManyRequests, message)
}
pub fn validation(message: impl Into<String>) -> Self {
Self::new(ErrorCode::ValidationError, 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
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
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(rename_all = "camelCase")]
pub struct FieldError {
pub field: String,
pub message: String,
}
impl FieldError {
pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
Self {
field: field.into(),
message: message.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
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),
}
}
}
impl<T> From<T> for ResponseData<T> {
fn from(v: T) -> Self {
ResponseData::Single(v)
}
}
impl<T> From<Vec<T>> for ResponseData<T> {
fn from(v: Vec<T>) -> Self {
ResponseData::Multiple(v)
}
}
#[derive(Debug, Clone, 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 errors: Option<Vec<FieldError>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace_id: Option<String>,
#[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,
errors: None,
trace_id: None,
extra: None,
}
}
pub fn list(v: Vec<T>) -> Self {
ApiResult {
success: true,
data: Some(ResponseData::multiple(v)),
message: None,
code: None,
pagination: None,
errors: None,
trace_id: 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,
errors: None,
trace_id: None,
extra: None,
}
}
pub fn success() -> Self {
Self::ok()
}
pub fn ok() -> Self {
ApiResult {
success: true,
data: None,
message: None,
code: None,
pagination: None,
errors: None,
trace_id: None,
extra: None,
}
}
pub fn validation_errors(errors: Vec<FieldError>) -> Self {
ApiResult {
success: false,
data: None,
message: Some("Validation failed".to_string()),
code: Some(ErrorCode::ValidationError as i32),
pagination: None,
errors: Some(errors),
trace_id: None,
extra: None,
}
}
pub fn from_result<E: std::fmt::Display>(result: Result<T, E>) -> Self {
match result {
Ok(data) => ApiResult::value(data),
Err(err) => ApiResult {
success: false,
data: None,
message: Some(err.to_string()),
code: Some(ErrorCode::InternalError as i32),
pagination: None,
errors: None,
trace_id: 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
}
pub fn with_errors(mut self, errors: Vec<FieldError>) -> Self {
self.errors = Some(errors);
self
}
pub fn with_error(mut self, field: impl Into<String>, message: impl Into<String>) -> Self {
match self.errors {
Some(ref mut v) => v.push(FieldError::new(field, message)),
None => self.errors = Some(vec![FieldError::new(field, message)]),
}
self
}
pub fn with_trace_id(mut self, id: impl Into<String>) -> Self {
self.trace_id = Some(id.into());
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,
errors: None,
trace_id: 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,
errors: None,
trace_id: 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,
errors: None,
trace_id: 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,
errors: None,
trace_id: 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<()>;