use crate::{
API_KEY, DEFAULT_BASE_URL,
client_builder::OpenFIGIClientBuilder,
error::{OpenFIGIError, Result},
model::response::ResponseResult,
request_builder::OpenFIGIRequestBuilder,
};
use reqwest_middleware::ClientWithMiddleware;
use serde::de::DeserializeOwned;
use url::Url;
#[derive(Clone, Debug)]
pub struct OpenFIGIClient {
client: ClientWithMiddleware,
base_url: Url,
api_key: Option<String>,
}
impl Default for OpenFIGIClient {
fn default() -> Self {
let api_key = API_KEY.as_ref().map(std::string::ToString::to_string);
Self {
client: ClientWithMiddleware::default(),
base_url: DEFAULT_BASE_URL.clone(),
api_key,
}
}
}
impl OpenFIGIClient {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn new_with_components(
client: ClientWithMiddleware,
base_url: Url,
api_key: Option<String>,
) -> Self {
Self {
client,
base_url,
api_key,
}
}
#[must_use]
pub fn builder() -> OpenFIGIClientBuilder {
OpenFIGIClientBuilder::new()
}
#[must_use]
pub fn client(&self) -> &ClientWithMiddleware {
&self.client
}
#[must_use]
pub fn base_url(&self) -> &Url {
&self.base_url
}
#[must_use]
pub fn api_key(&self) -> Option<&str> {
self.api_key.as_deref()
}
#[must_use]
pub fn has_api_key(&self) -> bool {
self.api_key.is_some()
}
pub(crate) fn request(&self, path: &str, method: reqwest::Method) -> OpenFIGIRequestBuilder {
OpenFIGIRequestBuilder::new(self.clone(), method, path)
}
pub(crate) async fn parse_single_response<T: DeserializeOwned>(
&self,
response: reqwest::Response,
) -> Result<T> {
let status = response.status();
if status.is_success() {
let parsed_response: ResponseResult<T> =
response.json().await.map_err(OpenFIGIError::from)?;
match parsed_response {
ResponseResult::Success(data) => return Ok(data),
ResponseResult::Error(err) => {
return Err(OpenFIGIError::response_error(
status,
format!("OpenFIGI API error: {}", err.error),
String::new(),
));
}
}
}
return Err(self.handle_error_response(response).await);
}
pub(crate) async fn parse_list_response<T: DeserializeOwned>(
&self,
response: reqwest::Response,
) -> Result<Vec<Result<T>>> {
let status = response.status();
if response.status().is_success() {
let parsed_list: Vec<ResponseResult<T>> =
response.json().await.map_err(OpenFIGIError::from)?;
let results: Vec<Result<T>> = parsed_list
.into_iter()
.map(|item| match item {
ResponseResult::Success(data) => Ok(data),
ResponseResult::Error(err) => Err(OpenFIGIError::response_error(
status,
format!("OpenFIGI API error: {}", err.error),
String::new(),
)),
})
.collect();
return Ok(results);
}
return Err(self.handle_error_response(response).await);
}
async fn handle_error_response(&self, response: reqwest::Response) -> OpenFIGIError {
let status = response.status();
let url = response.url().clone();
let rate_limit_info = if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
Self::extract_rate_limit_info(response.headers())
} else {
None
};
let error_message = Self::format_error_message(status, &url, rate_limit_info);
let resp_text = response.text().await.unwrap_or_default();
OpenFIGIError::response_error(status, error_message, resp_text)
}
fn extract_rate_limit_info(headers: &reqwest::header::HeaderMap) -> Option<String> {
let remaining = headers
.get("X-RateLimit-Remaining")
.and_then(|v| v.to_str().ok());
let reset = headers
.get("X-RateLimit-Reset")
.and_then(|v| v.to_str().ok());
match (remaining, reset) {
(Some(remaining), Some(reset)) => Some(format!(
"Rate limit: {remaining} requests remaining, resets at {reset} seconds"
)),
_ => None,
}
}
fn format_error_message(
status: reqwest::StatusCode,
url: &Url,
rate_limit_info: Option<String>,
) -> String {
match status {
reqwest::StatusCode::BAD_REQUEST => {
format!("Bad request to {url}: Invalid request body or parameters.")
}
reqwest::StatusCode::UNAUTHORIZED => {
format!("Unauthorized access to {url}: API key is missing or invalid.")
}
reqwest::StatusCode::NOT_FOUND => {
format!("Not found error from {url}: The requested resource could not be found.")
}
reqwest::StatusCode::METHOD_NOT_ALLOWED => format!(
"Method not allowed for {url}: The requested method is not supported for this endpoint."
),
reqwest::StatusCode::NOT_ACCEPTABLE => {
format!("Not acceptable request to {url}: Unsupported Accept header type.")
}
reqwest::StatusCode::PAYLOAD_TOO_LARGE => format!(
"Payload too large for {url}: Too many mapping requests in request (max 100 with API key, 5 without)."
),
reqwest::StatusCode::TOO_MANY_REQUESTS => {
let rate_msg = rate_limit_info.unwrap_or_else(|| "Rate limit exceeded".to_string());
format!("{rate_msg} for {url}. Please retry later.")
}
reqwest::StatusCode::INTERNAL_SERVER_ERROR => format!(
"Internal server error from {url}: OpenFIGI service is experiencing issues."
),
reqwest::StatusCode::BAD_GATEWAY
| reqwest::StatusCode::SERVICE_UNAVAILABLE
| reqwest::StatusCode::GATEWAY_TIMEOUT => format!(
"Service unavailable from {url}: OpenFIGI service is temporarily unavailable. Please retry later."
),
_ => format!("Unexpected HTTP status {} from {url}", status.as_u16()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_new() {
let client = OpenFIGIClient::new();
assert_eq!(client.base_url(), &*DEFAULT_BASE_URL);
}
#[test]
fn test_client_default() {
let client = OpenFIGIClient::default();
assert_eq!(client.base_url(), &*DEFAULT_BASE_URL);
}
#[test]
fn test_client_with_components() {
let client = ClientWithMiddleware::default();
let base_url = DEFAULT_BASE_URL.clone();
let api_key = Some("test_key".to_string());
let openfigi_client =
OpenFIGIClient::new_with_components(client, base_url.clone(), api_key.clone());
assert_eq!(openfigi_client.base_url(), &base_url);
assert_eq!(openfigi_client.api_key(), api_key.as_deref());
assert!(openfigi_client.has_api_key());
}
}