use std::collections::HashMap;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("API error ({status}): {message}")]
Api {
status: u16,
message: String,
detail: Option<String>,
},
#[error("Rate limited. Retry after {retry_after} seconds")]
RateLimit {
retry_after: u64,
message: String,
},
#[error("Validation error: {message}")]
Validation {
message: String,
errors: HashMap<String, Vec<String>>,
},
#[error("Authentication failed: {0}")]
Authentication(String),
#[error("Access forbidden: {0}")]
Forbidden(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Unsupported API version {api_version}. This SDK requires >= {min_version}")]
UnsupportedApiVersion {
api_version: String,
min_version: String,
max_known_version: String,
},
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Configuration error: {0}")]
Config(String),
#[error("Request timed out")]
Timeout,
}
impl Error {
pub(crate) async fn from_response(response: reqwest::Response) -> Self {
let status = response.status().as_u16();
let retry_after = response
.headers()
.get("Retry-After")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse().ok())
.unwrap_or(60);
let body: std::result::Result<ErrorResponse, _> = response.json().await;
let (message, detail, errors) = match body {
Ok(err) => (
err.error.unwrap_or_else(|| "Unknown error".into()),
err.detail,
err.errors,
),
Err(_) => ("Unknown error".into(), None, None),
};
match status {
400 => Error::Validation {
message,
errors: errors.unwrap_or_default(),
},
401 => Error::Authentication(message),
403 => Error::Forbidden(message),
404 => Error::NotFound(message),
429 => Error::RateLimit {
retry_after,
message,
},
_ => Error::Api {
status,
message,
detail,
},
}
}
}
#[derive(serde::Deserialize)]
struct ErrorResponse {
error: Option<String>,
detail: Option<String>,
errors: Option<HashMap<String, Vec<String>>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_error_display() {
let err = Error::Api {
status: 500,
message: "Internal server error".into(),
detail: Some("Something went wrong".into()),
};
assert!(err.to_string().contains("500"));
assert!(err.to_string().contains("Internal server error"));
}
#[test]
fn test_rate_limit_error_display() {
let err = Error::RateLimit {
retry_after: 30,
message: "Too many requests".into(),
};
assert!(err.to_string().contains("30"));
assert!(err.to_string().contains("Rate limited"));
}
#[test]
fn test_validation_error_display() {
let mut errors = HashMap::new();
errors.insert("url".to_string(), vec!["URL is required".to_string()]);
let err = Error::Validation {
message: "Invalid input".into(),
errors,
};
assert!(err.to_string().contains("Validation error"));
}
#[test]
fn test_authentication_error_display() {
let err = Error::Authentication("Invalid API key".into());
assert!(err.to_string().contains("Authentication failed"));
assert!(err.to_string().contains("Invalid API key"));
}
#[test]
fn test_forbidden_error_display() {
let err = Error::Forbidden("Insufficient permissions".into());
assert!(err.to_string().contains("Access forbidden"));
}
#[test]
fn test_not_found_error_display() {
let err = Error::NotFound("Job not found".into());
assert!(err.to_string().contains("Not found"));
}
#[test]
fn test_unsupported_api_version_error_display() {
let err = Error::UnsupportedApiVersion {
api_version: "0.5.0".into(),
min_version: "1.0.0".into(),
max_known_version: "1.1.0".into(),
};
assert!(err.to_string().contains("0.5.0"));
assert!(err.to_string().contains("1.0.0"));
}
#[test]
fn test_config_error_display() {
let err = Error::Config("API key is required".into());
assert!(err.to_string().contains("Configuration error"));
assert!(err.to_string().contains("API key is required"));
}
#[test]
fn test_timeout_error_display() {
let err = Error::Timeout;
assert!(err.to_string().contains("timed out"));
}
#[test]
fn test_error_is_debug() {
let err = Error::Api {
status: 404,
message: "Not found".into(),
detail: None,
};
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("Api"));
}
}