use std::collections::HashMap;
use std::fmt::Write as _;
use std::sync::Mutex;
use std::thread::sleep;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use reqwest::blocking::Client as ReqwestClient;
use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION, USER_AGENT};
use reqwest::Method;
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::error::{build_api_error, Error};
use crate::params::Params;
#[derive(Debug, Clone, Default)]
pub struct Quota {
pub limit: Option<i64>,
pub remaining: Option<i64>,
pub reset: Option<i64>,
pub raw: HashMap<String, String>,
}
pub(crate) struct HttpClient {
api_key: String,
base_url: String,
user_agent: String,
max_retries: u32,
client: ReqwestClient,
quota: Mutex<Option<Quota>>,
}
impl HttpClient {
pub(crate) fn new(
api_key: String,
base_url: String,
user_agent: String,
max_retries: u32,
client: ReqwestClient,
) -> Self {
HttpClient {
api_key,
base_url,
user_agent,
max_retries,
client,
quota: Mutex::new(None),
}
}
pub(crate) fn quota(&self) -> Option<Quota> {
self.quota.lock().unwrap().clone()
}
pub(crate) fn get<T: DeserializeOwned>(&self, path: &str, params: &Params) -> Result<T, Error> {
let value = self.send(Method::GET, path, Some(params), None)?;
from_value(unwrap(value))
}
pub(crate) fn post_value(&self, path: &str, body: Option<Value>) -> Result<Value, Error> {
let value = self.send(Method::POST, path, None, body)?;
Ok(unwrap(value))
}
pub(crate) fn delete_value(&self, path: &str) -> Result<Value, Error> {
let value = self.send(Method::DELETE, path, None, None)?;
Ok(unwrap(value))
}
pub(crate) fn send(
&self,
method: Method,
path: &str,
params: Option<&Params>,
body: Option<Value>,
) -> Result<Value, Error> {
let url = format!("{}{}", self.base_url, path);
let auth = format!("Bearer {}", self.api_key);
let is_idempotent = matches!(method, Method::GET | Method::DELETE);
let mut attempt: u32 = 0;
loop {
let mut req = self
.client
.request(method.clone(), &url)
.header(AUTHORIZATION, auth.as_str())
.header(ACCEPT, "application/json")
.header(USER_AGENT, self.user_agent.as_str());
if let Some(p) = params {
req = req.query(p.pairs());
}
if let Some(b) = &body {
req = req.json(b);
}
match req.send() {
Ok(resp) => {
let status = resp.status();
*self.quota.lock().unwrap() = Some(parse_quota(resp.headers()));
if status.is_success() {
let bytes = resp.bytes().map_err(Error::from)?;
if bytes.is_empty() {
return Ok(Value::Null);
}
return serde_json::from_slice(&bytes)
.map_err(|e| Error::Decode(e.to_string()));
}
let headers = resp.headers().clone();
let bytes = resp.bytes().map_err(Error::from)?;
let code = status.as_u16();
if (code == 429 || (status.is_server_error() && is_idempotent))
&& attempt < self.max_retries
{
sleep(retry_delay(&headers, attempt));
attempt += 1;
continue;
}
let value: Value = serde_json::from_slice(&bytes).unwrap_or(Value::Null);
return Err(Error::Api(build_api_error(code, &value, &headers)));
}
Err(e) => {
let retryable =
is_idempotent && (e.is_timeout() || e.is_connect() || e.is_request());
if retryable && attempt < self.max_retries {
sleep(backoff(attempt));
attempt += 1;
continue;
}
return Err(Error::from(e));
}
}
}
}
}
pub(crate) fn enc(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
out.push(b as char)
}
_ => {
let _ = write!(out, "%{b:02X}");
}
}
}
out
}
fn unwrap(value: Value) -> Value {
if let Value::Object(mut map) = value {
if let Some(data) = map.remove("data") {
return data;
}
return Value::Object(map);
}
value
}
fn from_value<T: DeserializeOwned>(value: Value) -> Result<T, Error> {
serde_json::from_value(value).map_err(|e| Error::Decode(e.to_string()))
}
fn parse_quota(headers: &HeaderMap) -> Quota {
let mut raw = HashMap::new();
for (name, value) in headers.iter() {
let key = name.as_str();
if key.starts_with("x-quota-") {
if let Ok(v) = value.to_str() {
raw.insert(key.to_string(), v.to_string());
}
}
}
let num = |name: &str| -> Option<i64> { raw.get(name).and_then(|v| v.parse::<i64>().ok()) };
Quota {
limit: num("x-quota-limit"),
remaining: num("x-quota-remaining"),
reset: num("x-quota-reset"),
raw,
}
}
fn retry_delay(headers: &HeaderMap, attempt: u32) -> Duration {
let retry_after = headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.filter(|&secs| secs > 0);
if let Some(secs) = retry_after {
return Duration::from_secs(secs.min(60));
}
backoff(attempt)
}
fn backoff(attempt: u32) -> Duration {
let ms = (250u64.saturating_mul(1u64 << attempt.min(20))).min(8000);
let jitter = u64::from(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0)
% 100,
);
Duration::from_millis(ms + jitter)
}