1use thiserror::Error;
7
8#[derive(Error, Debug)]
13pub enum FilesError {
14 #[error("HTTP request failed: {0}")]
16 Request(#[from] reqwest::Error),
17
18 #[error("Bad Request (400): {message}")]
20 BadRequest {
21 message: String,
22 field: Option<String>,
24 },
25
26 #[error("Authentication failed (401): {message}")]
28 AuthenticationFailed {
29 message: String,
30 auth_type: Option<String>,
32 },
33
34 #[error("Forbidden (403): {message}")]
36 Forbidden {
37 message: String,
38 resource: Option<String>,
40 },
41
42 #[error("Not Found (404): {message}")]
44 NotFound {
45 message: String,
46 resource_type: Option<String>,
48 path: Option<String>,
50 },
51
52 #[error("Conflict (409): {message}")]
54 Conflict {
55 message: String,
56 resource: Option<String>,
58 },
59
60 #[error("Precondition Failed (412): {message}")]
62 PreconditionFailed {
63 message: String,
64 condition: Option<String>,
66 },
67
68 #[error("Unprocessable Entity (422): {message}")]
70 UnprocessableEntity {
71 message: String,
72 field: Option<String>,
74 value: Option<String>,
76 },
77
78 #[error("Locked (423): {message}")]
80 Locked {
81 message: String,
82 resource: Option<String>,
84 },
85
86 #[error("Rate Limited (429): {message}")]
88 RateLimited {
89 message: String,
90 retry_after: Option<u64>,
92 },
93
94 #[error("Internal Server Error (500): {message}")]
96 InternalServerError {
97 message: String,
98 request_id: Option<String>,
100 },
101
102 #[error("Service Unavailable (503): {message}")]
104 ServiceUnavailable {
105 message: String,
106 retry_after: Option<u64>,
108 },
109
110 #[error("API error ({code}): {message}")]
112 ApiError {
113 code: u16,
114 message: String,
115 endpoint: Option<String>,
117 },
118
119 #[error("Configuration error: {0}")]
121 ConfigError(String),
122
123 #[error("JSON error: {0}")]
125 JsonError(#[from] serde_json::Error),
126
127 #[error("JSON deserialization error at '{path}': {source}")]
129 JsonPathError {
130 path: String,
131 source: serde_json::Error,
132 },
133
134 #[error("I/O error: {0}")]
136 IoError(String),
137
138 #[error("URL parse error: {0}")]
140 UrlParseError(#[from] url::ParseError),
141}
142
143impl FilesError {
144 pub fn not_found(message: impl Into<String>) -> Self {
146 FilesError::NotFound {
147 message: message.into(),
148 resource_type: None,
149 path: None,
150 }
151 }
152
153 pub fn not_found_resource(
155 message: impl Into<String>,
156 resource_type: impl Into<String>,
157 path: impl Into<String>,
158 ) -> Self {
159 FilesError::NotFound {
160 message: message.into(),
161 resource_type: Some(resource_type.into()),
162 path: Some(path.into()),
163 }
164 }
165
166 pub fn bad_request(message: impl Into<String>) -> Self {
168 FilesError::BadRequest {
169 message: message.into(),
170 field: None,
171 }
172 }
173
174 pub fn bad_request_field(message: impl Into<String>, field: impl Into<String>) -> Self {
176 FilesError::BadRequest {
177 message: message.into(),
178 field: Some(field.into()),
179 }
180 }
181
182 pub fn validation_failed(
184 message: impl Into<String>,
185 field: impl Into<String>,
186 value: impl Into<String>,
187 ) -> Self {
188 FilesError::UnprocessableEntity {
189 message: message.into(),
190 field: Some(field.into()),
191 value: Some(value.into()),
192 }
193 }
194
195 pub fn rate_limited(message: impl Into<String>, retry_after: Option<u64>) -> Self {
197 FilesError::RateLimited {
198 message: message.into(),
199 retry_after,
200 }
201 }
202
203 pub fn with_resource_type(mut self, resource_type: impl Into<String>) -> Self {
205 if let FilesError::NotFound {
206 resource_type: rt, ..
207 } = &mut self
208 {
209 *rt = Some(resource_type.into());
210 }
211 self
212 }
213
214 pub fn with_path(mut self, path: impl Into<String>) -> Self {
216 if let FilesError::NotFound { path: p, .. } = &mut self {
217 *p = Some(path.into());
218 }
219 self
220 }
221
222 pub fn with_field(mut self, field: impl Into<String>) -> Self {
224 if let FilesError::BadRequest { field: f, .. } = &mut self {
225 *f = Some(field.into());
226 }
227 self
228 }
229
230 pub fn status_code(&self) -> Option<u16> {
232 match self {
233 FilesError::BadRequest { .. } => Some(400),
234 FilesError::AuthenticationFailed { .. } => Some(401),
235 FilesError::Forbidden { .. } => Some(403),
236 FilesError::NotFound { .. } => Some(404),
237 FilesError::Conflict { .. } => Some(409),
238 FilesError::PreconditionFailed { .. } => Some(412),
239 FilesError::UnprocessableEntity { .. } => Some(422),
240 FilesError::Locked { .. } => Some(423),
241 FilesError::RateLimited { .. } => Some(429),
242 FilesError::InternalServerError { .. } => Some(500),
243 FilesError::ServiceUnavailable { .. } => Some(503),
244 FilesError::ApiError { code, .. } => Some(*code),
245 _ => None,
246 }
247 }
248
249 pub fn is_retryable(&self) -> bool {
251 matches!(
252 self,
253 FilesError::RateLimited { .. }
254 | FilesError::ServiceUnavailable { .. }
255 | FilesError::InternalServerError { .. }
256 )
257 }
258
259 pub fn retry_after(&self) -> Option<u64> {
261 match self {
262 FilesError::RateLimited { retry_after, .. } => *retry_after,
263 FilesError::ServiceUnavailable { retry_after, .. } => *retry_after,
264 _ => None,
265 }
266 }
267}
268
269pub type Result<T> = std::result::Result<T, FilesError>;
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_not_found_with_context() {
278 let error = FilesError::not_found_resource("File not found", "file", "/path/to/file.txt");
279 assert!(matches!(error, FilesError::NotFound { .. }));
280 assert!(error.to_string().contains("Not Found"));
281 }
282
283 #[test]
284 fn test_bad_request_with_field() {
285 let error = FilesError::bad_request_field("Invalid value", "username");
286 if let FilesError::BadRequest { field, .. } = error {
287 assert_eq!(field, Some("username".to_string()));
288 } else {
289 panic!("Expected BadRequest error");
290 }
291 }
292
293 #[test]
294 fn test_validation_failed() {
295 let error = FilesError::validation_failed("Invalid email format", "email", "not-an-email");
296 if let FilesError::UnprocessableEntity { field, value, .. } = error {
297 assert_eq!(field, Some("email".to_string()));
298 assert_eq!(value, Some("not-an-email".to_string()));
299 } else {
300 panic!("Expected UnprocessableEntity error");
301 }
302 }
303
304 #[test]
305 fn test_rate_limited_with_retry() {
306 let error = FilesError::rate_limited("Too many requests", Some(60));
307 assert_eq!(error.retry_after(), Some(60));
308 assert!(error.is_retryable());
309 }
310
311 #[test]
312 fn test_status_code_extraction() {
313 assert_eq!(FilesError::not_found("test").status_code(), Some(404));
314 assert_eq!(FilesError::bad_request("test").status_code(), Some(400));
315 assert_eq!(
316 FilesError::rate_limited("test", None).status_code(),
317 Some(429)
318 );
319 }
320
321 #[test]
322 fn test_is_retryable() {
323 assert!(FilesError::rate_limited("test", None).is_retryable());
324 assert!(
325 FilesError::InternalServerError {
326 message: "test".to_string(),
327 request_id: None
328 }
329 .is_retryable()
330 );
331 assert!(!FilesError::not_found("test").is_retryable());
332 }
333
334 #[test]
335 fn test_builder_pattern() {
336 let error = FilesError::not_found("File not found")
337 .with_resource_type("file")
338 .with_path("/test.txt");
339
340 if let FilesError::NotFound {
341 resource_type,
342 path,
343 ..
344 } = error
345 {
346 assert_eq!(resource_type, Some("file".to_string()));
347 assert_eq!(path, Some("/test.txt".to_string()));
348 } else {
349 panic!("Expected NotFound error");
350 }
351 }
352}