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("I/O error: {0}")]
129 IoError(String),
130
131 #[error("URL parse error: {0}")]
133 UrlParseError(#[from] url::ParseError),
134}
135
136impl FilesError {
137 pub fn not_found(message: impl Into<String>) -> Self {
139 FilesError::NotFound {
140 message: message.into(),
141 resource_type: None,
142 path: None,
143 }
144 }
145
146 pub fn not_found_resource(
148 message: impl Into<String>,
149 resource_type: impl Into<String>,
150 path: impl Into<String>,
151 ) -> Self {
152 FilesError::NotFound {
153 message: message.into(),
154 resource_type: Some(resource_type.into()),
155 path: Some(path.into()),
156 }
157 }
158
159 pub fn bad_request(message: impl Into<String>) -> Self {
161 FilesError::BadRequest {
162 message: message.into(),
163 field: None,
164 }
165 }
166
167 pub fn bad_request_field(message: impl Into<String>, field: impl Into<String>) -> Self {
169 FilesError::BadRequest {
170 message: message.into(),
171 field: Some(field.into()),
172 }
173 }
174
175 pub fn validation_failed(
177 message: impl Into<String>,
178 field: impl Into<String>,
179 value: impl Into<String>,
180 ) -> Self {
181 FilesError::UnprocessableEntity {
182 message: message.into(),
183 field: Some(field.into()),
184 value: Some(value.into()),
185 }
186 }
187
188 pub fn rate_limited(message: impl Into<String>, retry_after: Option<u64>) -> Self {
190 FilesError::RateLimited {
191 message: message.into(),
192 retry_after,
193 }
194 }
195
196 pub fn with_resource_type(mut self, resource_type: impl Into<String>) -> Self {
198 if let FilesError::NotFound {
199 resource_type: rt, ..
200 } = &mut self
201 {
202 *rt = Some(resource_type.into());
203 }
204 self
205 }
206
207 pub fn with_path(mut self, path: impl Into<String>) -> Self {
209 if let FilesError::NotFound { path: p, .. } = &mut self {
210 *p = Some(path.into());
211 }
212 self
213 }
214
215 pub fn with_field(mut self, field: impl Into<String>) -> Self {
217 if let FilesError::BadRequest { field: f, .. } = &mut self {
218 *f = Some(field.into());
219 }
220 self
221 }
222
223 pub fn status_code(&self) -> Option<u16> {
225 match self {
226 FilesError::BadRequest { .. } => Some(400),
227 FilesError::AuthenticationFailed { .. } => Some(401),
228 FilesError::Forbidden { .. } => Some(403),
229 FilesError::NotFound { .. } => Some(404),
230 FilesError::Conflict { .. } => Some(409),
231 FilesError::PreconditionFailed { .. } => Some(412),
232 FilesError::UnprocessableEntity { .. } => Some(422),
233 FilesError::Locked { .. } => Some(423),
234 FilesError::RateLimited { .. } => Some(429),
235 FilesError::InternalServerError { .. } => Some(500),
236 FilesError::ServiceUnavailable { .. } => Some(503),
237 FilesError::ApiError { code, .. } => Some(*code),
238 _ => None,
239 }
240 }
241
242 pub fn is_retryable(&self) -> bool {
244 matches!(
245 self,
246 FilesError::RateLimited { .. }
247 | FilesError::ServiceUnavailable { .. }
248 | FilesError::InternalServerError { .. }
249 )
250 }
251
252 pub fn retry_after(&self) -> Option<u64> {
254 match self {
255 FilesError::RateLimited { retry_after, .. } => *retry_after,
256 FilesError::ServiceUnavailable { retry_after, .. } => *retry_after,
257 _ => None,
258 }
259 }
260}
261
262pub type Result<T> = std::result::Result<T, FilesError>;
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_not_found_with_context() {
271 let error = FilesError::not_found_resource("File not found", "file", "/path/to/file.txt");
272 assert!(matches!(error, FilesError::NotFound { .. }));
273 assert!(error.to_string().contains("Not Found"));
274 }
275
276 #[test]
277 fn test_bad_request_with_field() {
278 let error = FilesError::bad_request_field("Invalid value", "username");
279 if let FilesError::BadRequest { field, .. } = error {
280 assert_eq!(field, Some("username".to_string()));
281 } else {
282 panic!("Expected BadRequest error");
283 }
284 }
285
286 #[test]
287 fn test_validation_failed() {
288 let error = FilesError::validation_failed("Invalid email format", "email", "not-an-email");
289 if let FilesError::UnprocessableEntity { field, value, .. } = error {
290 assert_eq!(field, Some("email".to_string()));
291 assert_eq!(value, Some("not-an-email".to_string()));
292 } else {
293 panic!("Expected UnprocessableEntity error");
294 }
295 }
296
297 #[test]
298 fn test_rate_limited_with_retry() {
299 let error = FilesError::rate_limited("Too many requests", Some(60));
300 assert_eq!(error.retry_after(), Some(60));
301 assert!(error.is_retryable());
302 }
303
304 #[test]
305 fn test_status_code_extraction() {
306 assert_eq!(FilesError::not_found("test").status_code(), Some(404));
307 assert_eq!(FilesError::bad_request("test").status_code(), Some(400));
308 assert_eq!(
309 FilesError::rate_limited("test", None).status_code(),
310 Some(429)
311 );
312 }
313
314 #[test]
315 fn test_is_retryable() {
316 assert!(FilesError::rate_limited("test", None).is_retryable());
317 assert!(
318 FilesError::InternalServerError {
319 message: "test".to_string(),
320 request_id: None
321 }
322 .is_retryable()
323 );
324 assert!(!FilesError::not_found("test").is_retryable());
325 }
326
327 #[test]
328 fn test_builder_pattern() {
329 let error = FilesError::not_found("File not found")
330 .with_resource_type("file")
331 .with_path("/test.txt");
332
333 if let FilesError::NotFound {
334 resource_type,
335 path,
336 ..
337 } = error
338 {
339 assert_eq!(resource_type, Some("file".to_string()));
340 assert_eq!(path, Some("/test.txt".to_string()));
341 } else {
342 panic!("Expected NotFound error");
343 }
344 }
345}