Skip to main content

cloudreve_sdk_api/
error.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use thiserror::Error;
4
5/// Standard API response wrapper
6#[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/// Error codes used by the Cloudreve API
39#[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    /// Check if this error code indicates an authentication/credential issue
81    pub fn is_credential_error(&self) -> bool {
82        matches!(
83            self,
84            Self::CredentialInvalid | Self::LoginRequired | Self::SessionExpired
85        )
86    }
87}
88
89/// Main error type for the Cloudreve API client
90#[derive(Error, Debug)]
91pub enum ApiError {
92    /// API returned an error response
93    #[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    /// Lock conflict error (40073)
103    #[error("Lock conflict: {message}")]
104    LockConflict {
105        message: String,
106        detail: Option<LockConflictDetail>,
107    },
108
109    /// Batch operation not fully completed (40081)
110    #[error("Batch operation not fully completed: {message}")]
111    BatchError {
112        message: String,
113        aggregated_errors: Option<HashMap<String, String>>,
114    },
115
116    /// Login required or credential invalid (401, 40020)
117    #[error("Login required: {0}")]
118    LoginRequired(String),
119
120    /// Access token expired and needs refresh
121    #[error("Access token expired")]
122    AccessTokenExpired,
123
124    /// Refresh token expired, need to login again
125    #[error("Refresh token expired, please login again")]
126    RefreshTokenExpired,
127
128    /// HTTP request error
129    #[error("HTTP request error: {0}")]
130    RequestError(#[from] reqwest::Error),
131
132    /// JSON serialization/deserialization error
133    #[error("JSON error: {0}")]
134    JsonError(#[from] serde_json::Error),
135
136    /// Invalid URL
137    #[error("Invalid URL: {0}")]
138    InvalidUrl(#[from] url::ParseError),
139
140    /// No tokens available
141    #[error("No authentication tokens available")]
142    NoTokensAvailable,
143
144    /// Invalid JWT token
145    #[error("Invalid token: {0}")]
146    InvalidToken(String),
147
148    /// SSE connection returned non-SSE response (server returned error before upgrading)
149    #[error("SSE connection failed (code {code}): {message}")]
150    SseNotUpgraded { code: i32, message: String },
151
152    /// SSE stream error
153    #[error("SSE stream error: {0}")]
154    SseStreamError(String),
155
156    /// Generic error
157    #[error("{0}")]
158    Other(String),
159}
160
161impl ApiError {
162    /// Create an ApiError from an API response
163    pub fn from_response<T>(response: ApiResponse<T>) -> Self {
164        let code = response.code;
165
166        // Handle specific error codes
167        match ErrorCode::from_code(code) {
168            Some(ErrorCode::LockConflict) => ApiError::LockConflict {
169                message: response.msg,
170                detail: None, // Will be populated by the client when parsing raw response
171            },
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    /// Check if this error is recoverable by retrying with a refreshed token
197    pub fn is_token_expired(&self) -> bool {
198        matches!(self, ApiError::AccessTokenExpired)
199    }
200
201    /// Check if this error requires login
202    pub fn requires_login(&self) -> bool {
203        matches!(
204            self,
205            ApiError::LoginRequired(_) | ApiError::RefreshTokenExpired
206        )
207    }
208}
209
210/// Result type alias for API operations
211pub type ApiResult<T> = Result<T, ApiError>;