clawspec_core/client/
error.rs

1use std::fmt::Debug;
2
3use super::auth::AuthenticationError;
4use super::output::Output;
5
6/// Errors that can occur when using the ApiClient.
7///
8/// This enum covers all possible error conditions from network issues to data validation failures.
9/// All variants implement `std::error::Error` and provide detailed context for debugging.
10#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
11pub enum ApiClientError {
12    /// HTTP client error from the underlying reqwest library.
13    ///
14    /// Occurs when network requests fail, timeouts occur, or connection issues arise.
15    ReqwestError(reqwest::Error),
16
17    /// URL parsing error when constructing request URLs.
18    ///
19    /// Occurs when the base URL or path parameters create an invalid URL.
20    UrlError(url::ParseError),
21
22    /// HTTP header processing error.
23    ///
24    /// Occurs when header values cannot be processed or are malformed.
25    HeadersError(headers::Error),
26
27    /// HTTP protocol error from the http crate.
28    ///
29    /// Occurs when HTTP protocol constraints are violated.
30    HttpError(http::Error),
31
32    /// Invalid HTTP header name.
33    ///
34    /// Occurs when attempting to create headers with invalid names.
35    InvalidHeaderName(http::header::InvalidHeaderName),
36
37    /// Invalid HTTP header value.
38    ///
39    /// Occurs when header values contain invalid characters.
40    InvalidHeaderValue(http::header::InvalidHeaderValue),
41
42    /// JSON serialization/deserialization error.
43    ///
44    /// Occurs when working with JSON request bodies or responses.
45    JsonValueError(serde_json::Error),
46
47    /// Query parameter serialization error.
48    ///
49    /// Occurs when converting structures to URL query strings.
50    QuerySerializationError(serde_urlencoded::ser::Error),
51
52    /// Authentication processing error.
53    ///
54    /// Occurs when authentication credentials cannot be processed or are invalid.
55    AuthenticationError(AuthenticationError),
56
57    /// No call result available for operation.
58    ///
59    /// Occurs when attempting to access response data before making a request.
60    #[display("Invalid state: expected a call result")]
61    CallResultRequired,
62
63    /// Invalid base path configuration.
64    ///
65    /// Occurs when the provided base path cannot be used for URL construction.
66    #[display("Invalid base path: {error}")]
67    InvalidBasePath {
68        /// Description of why the base path is invalid.
69        error: String,
70    },
71
72    /// JSON response deserialization failure.
73    ///
74    /// Occurs when the response body cannot be parsed as the expected JSON structure.
75    #[display("Failed to deserialize JSON at '{path}': {error}\n{body}")]
76    #[from(skip)]
77    JsonError {
78        /// The request path where the error occurred.
79        path: String,
80        /// The underlying JSON parsing error.
81        error: serde_json::Error,
82        /// The response body that failed to parse.
83        body: String,
84    },
85
86    /// Response output type is incompatible with JSON deserialization.
87    ///
88    /// Occurs when attempting to parse non-JSON responses as JSON.
89    #[display("Unsupported output for {name} as JSON:\n{output:?}")]
90    #[from(skip)]
91    UnsupportedJsonOutput {
92        /// The actual response output received.
93        output: Output,
94        /// Name of the operation that failed.
95        name: &'static str,
96    },
97
98    /// Response output type is incompatible with text extraction.
99    ///
100    /// Occurs when attempting to extract text from binary or empty responses.
101    #[display("Unsupported output for text:\n{output:?}")]
102    #[from(skip)]
103    UnsupportedTextOutput {
104        /// The actual response output received.
105        output: Output,
106    },
107
108    /// Response output type is incompatible with byte extraction.
109    ///
110    /// Occurs when attempting to extract bytes from empty responses.
111    #[display("Unsupported output for bytes:\n{output:?}")]
112    #[from(skip)]
113    UnsupportedBytesOutput {
114        /// The actual response output received.
115        output: Output,
116    },
117
118    /// Path template contains unresolved parameters.
119    ///
120    /// Occurs when path parameters are missing for templated URLs.
121    #[display("Path '{path}' is missing required arguments: {missings:?}")]
122    #[from(skip)]
123    PathUnresolved {
124        /// The path template that couldn't be resolved.
125        path: String,
126        /// List of missing parameter names.
127        missings: Vec<String>,
128    },
129
130    /// Query parameter value type is not supported.
131    ///
132    /// Occurs when attempting to use complex objects as query parameters.
133    #[display(
134        "Unsupported query parameter value: objects are not supported for query parameters. Got: {value}"
135    )]
136    #[from(skip)]
137    UnsupportedQueryParameterValue {
138        /// The unsupported value that was provided.
139        value: serde_json::Value,
140    },
141
142    /// Parameter value cannot be converted to the required format.
143    ///
144    /// Occurs when parameter values are incompatible with their target type.
145    #[display("Unsupported parameter value: {message}. Got: {value}")]
146    #[from(skip)]
147    UnsupportedParameterValue {
148        /// Specific error message describing the conversion failure.
149        message: String,
150        /// The value that failed to convert.
151        value: serde_json::Value,
152    },
153
154    /// OpenAPI operation not found in the specification.
155    ///
156    /// Occurs when referencing operations that don't exist in the collected spec.
157    #[display("Missing operation: {id}")]
158    #[from(skip)]
159    MissingOperation {
160        /// The operation ID that was not found.
161        id: String,
162    },
163
164    /// Server returned an internal error (HTTP 500).
165    ///
166    /// Occurs when the server encounters an internal error during request processing.
167    #[display("Server error (500) with response body: {raw_body}")]
168    #[from(skip)]
169    ServerFailure {
170        /// The response body containing error details.
171        raw_body: String,
172    },
173
174    /// Data serialization failed.
175    ///
176    /// Occurs when request data cannot be converted to the required format.
177    #[display("Serialization error: {message}")]
178    #[from(skip)]
179    SerializationError {
180        /// Description of the serialization failure.
181        message: String,
182    },
183
184    /// Server returned an unexpected HTTP status code.
185    ///
186    /// Occurs when the response status code doesn't match expected values.
187    #[display("Unexpected status code {status_code}: {body}")]
188    #[from(skip)]
189    UnexpectedStatusCode {
190        /// The unexpected HTTP status code received.
191        status_code: u16,
192        /// The response body for debugging.
193        body: String,
194    },
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::client::output::Output;
201
202    #[test]
203    fn test_api_client_error_is_send_and_sync() {
204        fn assert_send<T: Send>() {}
205        fn assert_sync<T: Sync>() {}
206
207        assert_send::<ApiClientError>();
208        assert_sync::<ApiClientError>();
209    }
210
211    // Test custom error variants (those with #[from(skip)])
212    #[test]
213    fn test_call_result_required_error() {
214        let error = ApiClientError::CallResultRequired;
215        assert_eq!(error.to_string(), "Invalid state: expected a call result");
216    }
217
218    #[test]
219    fn test_invalid_base_path_error() {
220        let error = ApiClientError::InvalidBasePath {
221            error: "contains invalid characters".to_string(),
222        };
223        assert_eq!(
224            error.to_string(),
225            "Invalid base path: contains invalid characters"
226        );
227    }
228
229    #[test]
230    fn test_json_error() {
231        // Create a real JSON error by trying to parse invalid JSON
232        let json_error = serde_json::from_str::<serde_json::Value>("{ invalid json").unwrap_err();
233        let error = ApiClientError::JsonError {
234            path: "/api/users".to_string(),
235            error: json_error,
236            body: "{ invalid json }".to_string(),
237        };
238
239        let error_str = error.to_string();
240        assert!(error_str.contains("Failed to deserialize JSON at '/api/users'"));
241        assert!(error_str.contains("{ invalid json }"));
242    }
243
244    #[test]
245    fn test_unsupported_json_output_error() {
246        let output = Output::Bytes(vec![0xFF, 0xFE, 0xFD]);
247        let error = ApiClientError::UnsupportedJsonOutput {
248            output,
249            name: "User",
250        };
251
252        let error_str = error.to_string();
253        assert!(error_str.contains("Unsupported output for User as JSON"));
254        assert!(error_str.contains("Bytes"));
255    }
256
257    #[test]
258    fn test_unsupported_text_output_error() {
259        let output = Output::Bytes(vec![0xFF, 0xFE, 0xFD]);
260        let error = ApiClientError::UnsupportedTextOutput { output };
261
262        let error_str = error.to_string();
263        assert!(error_str.contains("Unsupported output for text"));
264        assert!(error_str.contains("Bytes"));
265    }
266
267    #[test]
268    fn test_unsupported_bytes_output_error() {
269        let output = Output::Empty;
270        let error = ApiClientError::UnsupportedBytesOutput { output };
271
272        let error_str = error.to_string();
273        assert!(error_str.contains("Unsupported output for bytes"));
274        assert!(error_str.contains("Empty"));
275    }
276
277    #[test]
278    fn test_path_unresolved_error() {
279        let error = ApiClientError::PathUnresolved {
280            path: "/users/{id}/posts/{post_id}".to_string(),
281            missings: vec!["id".to_string(), "post_id".to_string()],
282        };
283
284        let error_str = error.to_string();
285        assert!(
286            error_str.contains("Path '/users/{id}/posts/{post_id}' is missing required arguments")
287        );
288        assert!(error_str.contains("id"));
289        assert!(error_str.contains("post_id"));
290    }
291
292    #[test]
293    fn test_unsupported_query_parameter_value_error() {
294        let value = serde_json::json!({"nested": {"object": "not supported"}});
295        let error = ApiClientError::UnsupportedQueryParameterValue {
296            value: value.clone(),
297        };
298
299        let error_str = error.to_string();
300        assert!(error_str.contains("Unsupported query parameter value"));
301        assert!(error_str.contains("objects are not supported"));
302    }
303
304    #[test]
305    fn test_unsupported_parameter_value_error() {
306        let value = serde_json::json!({"complex": "object"});
307        let error = ApiClientError::UnsupportedParameterValue {
308            message: "nested objects not allowed".to_string(),
309            value: value.clone(),
310        };
311
312        let error_str = error.to_string();
313        assert!(error_str.contains("Unsupported parameter value: nested objects not allowed"));
314        assert!(error_str.contains("complex"));
315    }
316
317    #[test]
318    fn test_missing_operation_error() {
319        let error = ApiClientError::MissingOperation {
320            id: "get-users-by-id".to_string(),
321        };
322
323        assert_eq!(error.to_string(), "Missing operation: get-users-by-id");
324    }
325
326    #[test]
327    fn test_server_failure_error() {
328        let error = ApiClientError::ServerFailure {
329            raw_body: "Internal Server Error: Database connection failed".to_string(),
330        };
331
332        let error_str = error.to_string();
333        assert!(error_str.contains("Server error (500)"));
334        assert!(error_str.contains("Database connection failed"));
335    }
336
337    #[test]
338    fn test_serialization_error() {
339        let error = ApiClientError::SerializationError {
340            message: "Cannot serialize circular reference".to_string(),
341        };
342
343        assert_eq!(
344            error.to_string(),
345            "Serialization error: Cannot serialize circular reference"
346        );
347    }
348
349    #[test]
350    fn test_unexpected_status_code_error() {
351        let error = ApiClientError::UnexpectedStatusCode {
352            status_code: 418,
353            body: "I'm a teapot".to_string(),
354        };
355
356        let error_str = error.to_string();
357        assert!(error_str.contains("Unexpected status code 418"));
358        assert!(error_str.contains("I'm a teapot"));
359    }
360
361    // Test automatic conversions from underlying error types
362    #[test]
363    fn test_from_reqwest_error() {
364        // Create a simple URL parse error and convert it to reqwest error
365        let url_error = url::ParseError::InvalidPort;
366        let api_error: ApiClientError = url_error.into();
367
368        match api_error {
369            ApiClientError::UrlError(_) => {} // Expected - testing URL error conversion
370            _ => panic!("Should convert to UrlError"),
371        }
372    }
373
374    #[test]
375    fn test_from_url_parse_error() {
376        let url_error = url::ParseError::InvalidPort;
377        let api_error: ApiClientError = url_error.into();
378
379        match api_error {
380            ApiClientError::UrlError(url::ParseError::InvalidPort) => {} // Expected
381            _ => panic!("Should convert to UrlError"),
382        }
383    }
384
385    #[test]
386    fn test_from_json_error() {
387        let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
388        let api_error: ApiClientError = json_error.into();
389
390        match api_error {
391            ApiClientError::JsonValueError(_) => {} // Expected
392            _ => panic!("Should convert to JsonValueError"),
393        }
394    }
395
396    #[test]
397    fn test_from_http_error() {
398        // Create an HTTP error by trying to parse an invalid header name
399        let invalid_header = http::HeaderName::from_bytes(b"invalid\0header").unwrap_err();
400        let api_error: ApiClientError = invalid_header.into();
401
402        match api_error {
403            ApiClientError::InvalidHeaderName(_) => {} // Expected - testing InvalidHeaderName conversion
404            _ => panic!("Should convert to InvalidHeaderName"),
405        }
406    }
407
408    #[test]
409    fn test_from_invalid_header_name() {
410        let header_error = http::HeaderName::from_bytes(b"invalid\0header").unwrap_err();
411        let api_error: ApiClientError = header_error.into();
412
413        match api_error {
414            ApiClientError::InvalidHeaderName(_) => {} // Expected
415            _ => panic!("Should convert to InvalidHeaderName"),
416        }
417    }
418
419    #[test]
420    fn test_from_invalid_header_value() {
421        // Header values cannot contain control characters (0x00-0x1F except tab)
422        let header_error = http::HeaderValue::from_bytes(&[0x00]).unwrap_err();
423        let api_error: ApiClientError = header_error.into();
424
425        match api_error {
426            ApiClientError::InvalidHeaderValue(_) => {} // Expected
427            _ => panic!("Should convert to InvalidHeaderValue"),
428        }
429    }
430
431    #[test]
432    fn test_from_authentication_error() {
433        let auth_error = AuthenticationError::InvalidBearerToken {
434            message: "contains null byte".to_string(),
435        };
436        let api_error: ApiClientError = auth_error.into();
437
438        match api_error {
439            ApiClientError::AuthenticationError(_) => {} // Expected
440            _ => panic!("Should convert to AuthenticationError"),
441        }
442    }
443
444    // Test Debug implementation
445    #[test]
446    fn test_error_debug_implementation() {
447        let error = ApiClientError::CallResultRequired;
448        let debug_str = format!("{error:?}");
449        assert!(debug_str.contains("CallResultRequired"));
450
451        let error = ApiClientError::InvalidBasePath {
452            error: "test".to_string(),
453        };
454        let debug_str = format!("{error:?}");
455        assert!(debug_str.contains("InvalidBasePath"));
456        assert!(debug_str.contains("test"));
457    }
458
459    // Test that errors implement the Error trait properly
460    #[test]
461    fn test_error_trait_implementation() {
462        use std::error::Error;
463
464        let error = ApiClientError::CallResultRequired;
465        assert!(error.source().is_none());
466
467        let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
468        let error = ApiClientError::JsonValueError(json_error);
469        assert!(error.source().is_some());
470    }
471
472    // Test error equality (where applicable)
473    #[test]
474    fn test_error_equality() {
475        let error1 = ApiClientError::CallResultRequired;
476        let error2 = ApiClientError::CallResultRequired;
477
478        // These errors should produce the same string representation
479        assert_eq!(error1.to_string(), error2.to_string());
480
481        let error1 = ApiClientError::InvalidBasePath {
482            error: "same error".to_string(),
483        };
484        let error2 = ApiClientError::InvalidBasePath {
485            error: "same error".to_string(),
486        };
487        assert_eq!(error1.to_string(), error2.to_string());
488    }
489
490    // Test error context preservation
491    #[test]
492    fn test_error_context_preservation() {
493        let path = "/complex/path/{id}";
494        let missings = vec!["id".to_string(), "user_id".to_string()];
495        let error = ApiClientError::PathUnresolved {
496            path: path.to_string(),
497            missings: missings.clone(),
498        };
499
500        let error_string = error.to_string();
501        assert!(error_string.contains(path));
502        for missing in &missings {
503            assert!(error_string.contains(missing));
504        }
505    }
506
507    // Test complex error scenarios
508    #[test]
509    fn test_json_error_with_large_body() {
510        let large_body = "x".repeat(2000);
511        let json_error = serde_json::from_str::<serde_json::Value>("{ invalid").unwrap_err();
512        let error = ApiClientError::JsonError {
513            path: "/api/data".to_string(),
514            error: json_error,
515            body: large_body.clone(),
516        };
517
518        let error_str = error.to_string();
519        assert!(error_str.contains("/api/data"));
520        assert!(error_str.contains(&large_body));
521    }
522
523    #[test]
524    fn test_status_code_error_edge_cases() {
525        // Test various HTTP status codes
526        let error = ApiClientError::UnexpectedStatusCode {
527            status_code: 999, // Invalid status code
528            body: "unknown status".to_string(),
529        };
530        assert!(error.to_string().contains("999"));
531
532        let error = ApiClientError::UnexpectedStatusCode {
533            status_code: 0, // Edge case
534            body: "".to_string(),
535        };
536        assert!(error.to_string().contains("0"));
537    }
538
539    // Test output error variants with different Output types
540    #[test]
541    fn test_output_errors_with_all_output_types() {
542        // Test with Text output
543        let text_output = Output::Text("some text".to_string());
544        let error = ApiClientError::UnsupportedBytesOutput {
545            output: text_output,
546        };
547        assert!(error.to_string().contains("Text"));
548
549        // Test with JSON output
550        let json_output =
551            Output::Json(serde_json::to_string(&serde_json::json!({"key": "value"})).unwrap());
552        let error = ApiClientError::UnsupportedTextOutput {
553            output: json_output,
554        };
555        assert!(error.to_string().contains("Json"));
556
557        // Test with Empty output
558        let empty_output = Output::Empty;
559        let error = ApiClientError::UnsupportedJsonOutput {
560            output: empty_output,
561            name: "TestType",
562        };
563        assert!(error.to_string().contains("Empty"));
564        assert!(error.to_string().contains("TestType"));
565    }
566}