clawspec_core/client/
error.rs

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