1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use thiserror::Error;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ApiResponse<T> {
8 pub data: Option<T>,
9 pub code: i32,
10 pub msg: String,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 pub error: Option<String>,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub correlation_id: Option<String>,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub aggregated_error: Option<HashMap<String, ApiResponse<T>>>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct LockConflictDetail {
21 pub path: String,
22 #[serde(rename = "type")]
23 pub lock_type: i32,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct LockOwner {
28 pub owner: String,
29 pub application: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct LockApplication {
34 #[serde(rename = "type")]
35 pub application_type: String,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum ErrorCode {
41 Success = 0,
42 Continue = 203,
43 ObjectExisted = 40004,
44 ParentNotExist = 40016,
45 CredentialInvalid = 40020,
46 IncorrectPassword = 40069,
47 LockConflict = 40073,
48 StaleVersion = 40076,
49 BatchOperationNotFullyCompleted = 40081,
50 DomainNotLicensed = 40087,
51 AnonymousAccessDenied = 40088,
52 SessionExpired = 40089,
53 PurchaseRequired = 40083,
54 LoginRequired = 401,
55 PermissionDenied = 403,
56 NotFound = 404,
57}
58
59impl ErrorCode {
60 pub fn from_code(code: i32) -> Option<Self> {
61 match code {
62 0 => Some(Self::Success),
63 203 => Some(Self::Continue),
64 40020 => Some(Self::CredentialInvalid),
65 40069 => Some(Self::IncorrectPassword),
66 40073 => Some(Self::LockConflict),
67 40076 => Some(Self::StaleVersion),
68 40081 => Some(Self::BatchOperationNotFullyCompleted),
69 40087 => Some(Self::DomainNotLicensed),
70 40088 => Some(Self::AnonymousAccessDenied),
71 40089 => Some(Self::SessionExpired),
72 40083 => Some(Self::PurchaseRequired),
73 401 => Some(Self::LoginRequired),
74 403 => Some(Self::PermissionDenied),
75 404 => Some(Self::NotFound),
76 _ => None,
77 }
78 }
79
80 pub fn is_credential_error(&self) -> bool {
82 matches!(
83 self,
84 Self::CredentialInvalid | Self::LoginRequired | Self::SessionExpired
85 )
86 }
87}
88
89#[derive(Error, Debug)]
91pub enum ApiError {
92 #[error("API error (code {code}): {message}")]
94 ApiError {
95 code: i32,
96 message: String,
97 error_detail: Option<String>,
98 correlation_id: Option<String>,
99 aggregated_errors: Option<HashMap<String, String>>,
100 },
101
102 #[error("Lock conflict: {message}")]
104 LockConflict {
105 message: String,
106 detail: Option<LockConflictDetail>,
107 },
108
109 #[error("Batch operation not fully completed: {message}")]
111 BatchError {
112 message: String,
113 aggregated_errors: Option<HashMap<String, String>>,
114 },
115
116 #[error("Login required: {0}")]
118 LoginRequired(String),
119
120 #[error("Access token expired")]
122 AccessTokenExpired,
123
124 #[error("Refresh token expired, please login again")]
126 RefreshTokenExpired,
127
128 #[error("HTTP request error: {0}")]
130 RequestError(#[from] reqwest::Error),
131
132 #[error("JSON error: {0}")]
134 JsonError(#[from] serde_json::Error),
135
136 #[error("Invalid URL: {0}")]
138 InvalidUrl(#[from] url::ParseError),
139
140 #[error("No authentication tokens available")]
142 NoTokensAvailable,
143
144 #[error("Invalid token: {0}")]
146 InvalidToken(String),
147
148 #[error("SSE connection failed (code {code}): {message}")]
150 SseNotUpgraded { code: i32, message: String },
151
152 #[error("SSE stream error: {0}")]
154 SseStreamError(String),
155
156 #[error("{0}")]
158 Other(String),
159}
160
161impl ApiError {
162 pub fn from_response<T>(response: ApiResponse<T>) -> Self {
164 let code = response.code;
165
166 match ErrorCode::from_code(code) {
168 Some(ErrorCode::LockConflict) => ApiError::LockConflict {
169 message: response.msg,
170 detail: None, },
172 Some(ErrorCode::BatchOperationNotFullyCompleted) => {
173 let aggregated = response
174 .aggregated_error
175 .map(|errors| errors.into_iter().map(|(k, v)| (k, v.msg)).collect());
176 ApiError::BatchError {
177 message: response.msg,
178 aggregated_errors: aggregated,
179 }
180 }
181 Some(ErrorCode::LoginRequired)
182 | Some(ErrorCode::CredentialInvalid)
183 | Some(ErrorCode::SessionExpired) => ApiError::LoginRequired(response.msg),
184 _ => ApiError::ApiError {
185 code,
186 message: response.msg,
187 error_detail: response.error,
188 correlation_id: response.correlation_id,
189 aggregated_errors: response
190 .aggregated_error
191 .map(|errors| errors.into_iter().map(|(k, v)| (k, v.msg)).collect()),
192 },
193 }
194 }
195
196 pub fn is_token_expired(&self) -> bool {
198 matches!(self, ApiError::AccessTokenExpired)
199 }
200
201 pub fn requires_login(&self) -> bool {
203 matches!(
204 self,
205 ApiError::LoginRequired(_) | ApiError::RefreshTokenExpired
206 )
207 }
208}
209
210pub type ApiResult<T> = Result<T, ApiError>;