1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Error, Debug, Clone, PartialEq)]
8pub enum ApiError {
9 #[error("Network error: {0}")]
11 Network(String),
12
13 #[error("HTTP {status}: {message}")]
15 Http { status: u16, message: String },
16
17 #[error("Not found: {0}")]
19 NotFound(String),
20
21 #[error("Bad request: {0}")]
23 BadRequest(String),
24
25 #[error("Unauthorized")]
27 Unauthorized,
28
29 #[error("Forbidden")]
31 Forbidden,
32
33 #[error("Server error: {0}")]
35 Server(String),
36
37 #[error("Request timed out")]
39 Timeout,
40
41 #[error("Serialization error: {0}")]
43 Serialization(String),
44
45 #[error("Deserialization error: {0}")]
47 Deserialization(String),
48
49 #[error("Validation error: {0}")]
51 Validation(String),
52}
53
54impl ApiError {
55 pub fn from_status(status: u16, message: String) -> Self {
57 match status {
58 400 => Self::BadRequest(message),
59 401 => Self::Unauthorized,
60 403 => Self::Forbidden,
61 404 => Self::NotFound(message),
62 408 => Self::Timeout,
63 500..=599 => Self::Server(message),
64 _ => Self::Http { status, message },
65 }
66 }
67
68 pub fn is_recoverable(&self) -> bool {
70 matches!(
71 self,
72 Self::Network(_) | Self::Timeout | Self::Server(_)
73 )
74 }
75
76 pub fn is_client_error(&self) -> bool {
78 matches!(
79 self,
80 Self::NotFound(_)
81 | Self::BadRequest(_)
82 | Self::Unauthorized
83 | Self::Forbidden
84 | Self::Validation(_)
85 )
86 }
87
88 pub fn status_code(&self) -> Option<u16> {
90 match self {
91 Self::Http { status, .. } => Some(*status),
92 Self::NotFound(_) => Some(404),
93 Self::BadRequest(_) => Some(400),
94 Self::Unauthorized => Some(401),
95 Self::Forbidden => Some(403),
96 Self::Server(_) => Some(500),
97 Self::Timeout => Some(408),
98 _ => None,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ApiErrorResponse {
106 pub code: String,
108 pub message: String,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub details: Option<serde_json::Value>,
113}
114
115impl From<ApiErrorResponse> for ApiError {
116 fn from(resp: ApiErrorResponse) -> Self {
117 match resp.code.as_str() {
118 "NOT_FOUND" => Self::NotFound(resp.message),
119 "BAD_REQUEST" => Self::BadRequest(resp.message),
120 "VALIDATION_ERROR" => Self::Validation(resp.message),
121 "UNAUTHORIZED" => Self::Unauthorized,
122 "FORBIDDEN" => Self::Forbidden,
123 "INTERNAL_ERROR" => Self::Server(resp.message),
124 _ => Self::Http {
125 status: 0,
126 message: resp.message,
127 },
128 }
129 }
130}
131
132pub type ApiResult<T> = Result<T, ApiError>;