use std::fmt::Debug;
use super::auth::AuthenticationError;
use super::response::output::Output;
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
pub enum ApiClientError {
ReqwestError(reqwest::Error),
UrlError(url::ParseError),
HeadersError(headers::Error),
HttpError(http::Error),
InvalidHeaderName(http::header::InvalidHeaderName),
InvalidHeaderValue(http::header::InvalidHeaderValue),
JsonValueError(serde_json::Error),
QuerySerializationError(serde_urlencoded::ser::Error),
AuthenticationError(AuthenticationError),
#[display("Invalid state: expected a call result")]
CallResultRequired,
#[display("Invalid base path: {error}")]
InvalidBasePath {
error: String,
},
#[display("Failed to deserialize JSON at '{path}': {error}\n{body}")]
#[from(skip)]
JsonError {
path: String,
error: serde_json::Error,
body: String,
},
#[display("Unsupported output for {name} as JSON:\n{output:?}")]
#[from(skip)]
UnsupportedJsonOutput {
output: Output,
name: &'static str,
},
#[display("Unsupported output for text:\n{output:?}")]
#[from(skip)]
UnsupportedTextOutput {
output: Output,
},
#[display("Unsupported output for bytes:\n{output:?}")]
#[from(skip)]
UnsupportedBytesOutput {
output: Output,
},
#[display("Path '{path}' is missing required arguments: {missings:?}")]
#[from(skip)]
PathUnresolved {
path: String,
missings: Vec<String>,
},
#[display(
"Unsupported query parameter value: objects are not supported for query parameters. Got: {value}"
)]
#[from(skip)]
UnsupportedQueryParameterValue {
value: serde_json::Value,
},
#[display("Unsupported parameter value: {message}. Got: {value}")]
#[from(skip)]
UnsupportedParameterValue {
message: String,
value: serde_json::Value,
},
#[display("Missing operation: {id}")]
#[from(skip)]
MissingOperation {
id: String,
},
#[display("Server error (500) with response body: {raw_body}")]
#[from(skip)]
ServerFailure {
raw_body: String,
},
#[display("Serialization error: {message}")]
#[from(skip)]
SerializationError {
message: String,
},
#[display("Unexpected status code {status_code}: {body}")]
#[from(skip)]
UnexpectedStatusCode {
status_code: u16,
body: String,
},
#[cfg(feature = "redaction")]
#[display("Redaction error: {message}")]
#[from(skip)]
RedactionError {
message: String,
},
#[display("Expected output type '{expected}' but got '{actual}'")]
#[from(skip)]
UnexpectedOutputType {
expected: String,
actual: String,
},
#[cfg(feature = "oauth2")]
#[display("OAuth2 error: {message}")]
#[from(skip)]
OAuth2Error {
message: String,
},
}
#[cfg(feature = "oauth2")]
impl ApiClientError {
pub fn oauth2_error(error: impl std::fmt::Display) -> Self {
Self::OAuth2Error {
message: error.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::response::output::Output;
#[test]
fn test_api_client_error_is_send_and_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<ApiClientError>();
assert_sync::<ApiClientError>();
}
#[test]
fn test_call_result_required_error() {
let error = ApiClientError::CallResultRequired;
assert_eq!(error.to_string(), "Invalid state: expected a call result");
}
#[test]
fn test_invalid_base_path_error() {
let error = ApiClientError::InvalidBasePath {
error: "contains invalid characters".to_string(),
};
assert_eq!(
error.to_string(),
"Invalid base path: contains invalid characters"
);
}
#[test]
fn test_json_error() {
let json_error = serde_json::from_str::<serde_json::Value>("{ invalid json").unwrap_err();
let error = ApiClientError::JsonError {
path: "/api/users".to_string(),
error: json_error,
body: "{ invalid json }".to_string(),
};
let error_str = error.to_string();
assert!(error_str.contains("Failed to deserialize JSON at '/api/users'"));
assert!(error_str.contains("{ invalid json }"));
}
#[test]
fn test_unsupported_json_output_error() {
let output = Output::Bytes(vec![0xFF, 0xFE, 0xFD]);
let error = ApiClientError::UnsupportedJsonOutput {
output,
name: "User",
};
let error_str = error.to_string();
assert!(error_str.contains("Unsupported output for User as JSON"));
assert!(error_str.contains("Bytes"));
}
#[test]
fn test_unsupported_text_output_error() {
let output = Output::Bytes(vec![0xFF, 0xFE, 0xFD]);
let error = ApiClientError::UnsupportedTextOutput { output };
let error_str = error.to_string();
assert!(error_str.contains("Unsupported output for text"));
assert!(error_str.contains("Bytes"));
}
#[test]
fn test_unsupported_bytes_output_error() {
let output = Output::Empty;
let error = ApiClientError::UnsupportedBytesOutput { output };
let error_str = error.to_string();
assert!(error_str.contains("Unsupported output for bytes"));
assert!(error_str.contains("Empty"));
}
#[test]
fn test_path_unresolved_error() {
let error = ApiClientError::PathUnresolved {
path: "/users/{id}/posts/{post_id}".to_string(),
missings: vec!["id".to_string(), "post_id".to_string()],
};
let error_str = error.to_string();
assert!(
error_str.contains("Path '/users/{id}/posts/{post_id}' is missing required arguments")
);
assert!(error_str.contains("id"));
assert!(error_str.contains("post_id"));
}
#[test]
fn test_unsupported_query_parameter_value_error() {
let value = serde_json::json!({"nested": {"object": "not supported"}});
let error = ApiClientError::UnsupportedQueryParameterValue {
value: value.clone(),
};
let error_str = error.to_string();
assert!(error_str.contains("Unsupported query parameter value"));
assert!(error_str.contains("objects are not supported"));
}
#[test]
fn test_unsupported_parameter_value_error() {
let value = serde_json::json!({"complex": "object"});
let error = ApiClientError::UnsupportedParameterValue {
message: "nested objects not allowed".to_string(),
value: value.clone(),
};
let error_str = error.to_string();
assert!(error_str.contains("Unsupported parameter value: nested objects not allowed"));
assert!(error_str.contains("complex"));
}
#[test]
fn test_missing_operation_error() {
let error = ApiClientError::MissingOperation {
id: "get-users-by-id".to_string(),
};
assert_eq!(error.to_string(), "Missing operation: get-users-by-id");
}
#[test]
fn test_server_failure_error() {
let error = ApiClientError::ServerFailure {
raw_body: "Internal Server Error: Database connection failed".to_string(),
};
let error_str = error.to_string();
assert!(error_str.contains("Server error (500)"));
assert!(error_str.contains("Database connection failed"));
}
#[test]
fn test_serialization_error() {
let error = ApiClientError::SerializationError {
message: "Cannot serialize circular reference".to_string(),
};
assert_eq!(
error.to_string(),
"Serialization error: Cannot serialize circular reference"
);
}
#[test]
fn test_unexpected_status_code_error() {
let error = ApiClientError::UnexpectedStatusCode {
status_code: 418,
body: "I'm a teapot".to_string(),
};
let error_str = error.to_string();
assert!(error_str.contains("Unexpected status code 418"));
assert!(error_str.contains("I'm a teapot"));
}
#[test]
fn test_from_reqwest_error() {
let url_error = url::ParseError::InvalidPort;
let api_error: ApiClientError = url_error.into();
match api_error {
ApiClientError::UrlError(_) => {} _ => panic!("Should convert to UrlError"),
}
}
#[test]
fn test_from_url_parse_error() {
let url_error = url::ParseError::InvalidPort;
let api_error: ApiClientError = url_error.into();
match api_error {
ApiClientError::UrlError(url::ParseError::InvalidPort) => {} _ => panic!("Should convert to UrlError"),
}
}
#[test]
fn test_from_json_error() {
let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let api_error: ApiClientError = json_error.into();
match api_error {
ApiClientError::JsonValueError(_) => {} _ => panic!("Should convert to JsonValueError"),
}
}
#[test]
fn test_from_http_error() {
let invalid_header = http::HeaderName::from_bytes(b"invalid\0header").unwrap_err();
let api_error: ApiClientError = invalid_header.into();
match api_error {
ApiClientError::InvalidHeaderName(_) => {} _ => panic!("Should convert to InvalidHeaderName"),
}
}
#[test]
fn test_from_invalid_header_name() {
let header_error = http::HeaderName::from_bytes(b"invalid\0header").unwrap_err();
let api_error: ApiClientError = header_error.into();
match api_error {
ApiClientError::InvalidHeaderName(_) => {} _ => panic!("Should convert to InvalidHeaderName"),
}
}
#[test]
fn test_from_invalid_header_value() {
let header_error = http::HeaderValue::from_bytes(&[0x00]).unwrap_err();
let api_error: ApiClientError = header_error.into();
match api_error {
ApiClientError::InvalidHeaderValue(_) => {} _ => panic!("Should convert to InvalidHeaderValue"),
}
}
#[test]
fn test_from_authentication_error() {
let auth_error = AuthenticationError::InvalidBearerToken {
message: "contains null byte".to_string(),
};
let api_error: ApiClientError = auth_error.into();
match api_error {
ApiClientError::AuthenticationError(_) => {} _ => panic!("Should convert to AuthenticationError"),
}
}
#[test]
fn test_error_debug_implementation() {
let error = ApiClientError::CallResultRequired;
let debug_str = format!("{error:?}");
assert!(debug_str.contains("CallResultRequired"));
let error = ApiClientError::InvalidBasePath {
error: "test".to_string(),
};
let debug_str = format!("{error:?}");
assert!(debug_str.contains("InvalidBasePath"));
assert!(debug_str.contains("test"));
}
#[test]
fn test_error_trait_implementation() {
use std::error::Error;
let error = ApiClientError::CallResultRequired;
assert!(error.source().is_none());
let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let error = ApiClientError::JsonValueError(json_error);
assert!(error.source().is_some());
}
#[test]
fn test_error_equality() {
let error1 = ApiClientError::CallResultRequired;
let error2 = ApiClientError::CallResultRequired;
assert_eq!(error1.to_string(), error2.to_string());
let error1 = ApiClientError::InvalidBasePath {
error: "same error".to_string(),
};
let error2 = ApiClientError::InvalidBasePath {
error: "same error".to_string(),
};
assert_eq!(error1.to_string(), error2.to_string());
}
#[test]
fn test_error_context_preservation() {
let path = "/complex/path/{id}";
let missings = vec!["id".to_string(), "user_id".to_string()];
let error = ApiClientError::PathUnresolved {
path: path.to_string(),
missings: missings.clone(),
};
let error_string = error.to_string();
assert!(error_string.contains(path));
for missing in &missings {
assert!(error_string.contains(missing));
}
}
#[test]
fn test_json_error_with_large_body() {
let large_body = "x".repeat(2000);
let json_error = serde_json::from_str::<serde_json::Value>("{ invalid").unwrap_err();
let error = ApiClientError::JsonError {
path: "/api/data".to_string(),
error: json_error,
body: large_body.clone(),
};
let error_str = error.to_string();
assert!(error_str.contains("/api/data"));
assert!(error_str.contains(&large_body));
}
#[test]
fn test_status_code_error_edge_cases() {
let error = ApiClientError::UnexpectedStatusCode {
status_code: 999, body: "unknown status".to_string(),
};
assert!(error.to_string().contains("999"));
let error = ApiClientError::UnexpectedStatusCode {
status_code: 0, body: "".to_string(),
};
assert!(error.to_string().contains("0"));
}
#[test]
fn test_output_errors_with_all_output_types() {
let text_output = Output::Text("some text".to_string());
let error = ApiClientError::UnsupportedBytesOutput {
output: text_output,
};
assert!(error.to_string().contains("Text"));
let json_output =
Output::Json(serde_json::to_string(&serde_json::json!({"key": "value"})).unwrap());
let error = ApiClientError::UnsupportedTextOutput {
output: json_output,
};
assert!(error.to_string().contains("Json"));
let empty_output = Output::Empty;
let error = ApiClientError::UnsupportedJsonOutput {
output: empty_output,
name: "TestType",
};
assert!(error.to_string().contains("Empty"));
assert!(error.to_string().contains("TestType"));
}
}