use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug, Clone, PartialEq)]
pub enum ApiError {
#[error("Network error: {0}")]
Network(String),
#[error("HTTP {status}: {message}")]
Http { status: u16, message: String },
#[error("Not found: {0}")]
NotFound(String),
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Unauthorized")]
Unauthorized,
#[error("Forbidden")]
Forbidden,
#[error("Server error: {0}")]
Server(String),
#[error("Request timed out")]
Timeout,
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Deserialization error: {0}")]
Deserialization(String),
#[error("Validation error: {0}")]
Validation(String),
}
impl ApiError {
pub fn from_status(status: u16, message: String) -> Self {
match status {
400 => Self::BadRequest(message),
401 => Self::Unauthorized,
403 => Self::Forbidden,
404 => Self::NotFound(message),
408 => Self::Timeout,
500..=599 => Self::Server(message),
_ => Self::Http { status, message },
}
}
pub fn is_recoverable(&self) -> bool {
matches!(
self,
Self::Network(_) | Self::Timeout | Self::Server(_)
)
}
pub fn is_client_error(&self) -> bool {
matches!(
self,
Self::NotFound(_)
| Self::BadRequest(_)
| Self::Unauthorized
| Self::Forbidden
| Self::Validation(_)
)
}
pub fn status_code(&self) -> Option<u16> {
match self {
Self::Http { status, .. } => Some(*status),
Self::NotFound(_) => Some(404),
Self::BadRequest(_) => Some(400),
Self::Unauthorized => Some(401),
Self::Forbidden => Some(403),
Self::Server(_) => Some(500),
Self::Timeout => Some(408),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiErrorResponse {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
impl From<ApiErrorResponse> for ApiError {
fn from(resp: ApiErrorResponse) -> Self {
match resp.code.as_str() {
"NOT_FOUND" => Self::NotFound(resp.message),
"BAD_REQUEST" => Self::BadRequest(resp.message),
"VALIDATION_ERROR" => Self::Validation(resp.message),
"UNAUTHORIZED" => Self::Unauthorized,
"FORBIDDEN" => Self::Forbidden,
"INTERNAL_ERROR" => Self::Server(resp.message),
_ => Self::Http {
status: 0,
message: resp.message,
},
}
}
}
pub type ApiResult<T> = Result<T, ApiError>;