use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use serde::de::DeserializeOwned;
use crate::error::{Error, Result};
use crate::ratelimit::{RateLimit, SharedRateLimit};
const DEFAULT_BASE_URL: &str = "https://api.hypixel.net";
const API_KEY_HEADER: &str = "API-Key";
#[derive(Debug, Clone)]
pub struct HypixelClient {
http: reqwest::Client,
base_url: String,
api_key: Option<String>,
rate_limit: SharedRateLimit,
retry_rate_limited: u32,
cache: Option<ResponseCache>,
}
type CacheEntries = Arc<Mutex<HashMap<String, (Instant, Arc<[u8]>)>>>;
#[derive(Debug, Clone)]
struct ResponseCache {
ttl: Duration,
entries: CacheEntries,
}
impl ResponseCache {
fn get(&self, key: &str) -> Option<Arc<[u8]>> {
let entries = self.entries.lock().ok()?;
let (stored_at, bytes) = entries.get(key)?;
(stored_at.elapsed() < self.ttl).then(|| bytes.clone())
}
fn put(&self, key: String, bytes: Arc<[u8]>) {
if let Ok(mut entries) = self.entries.lock() {
entries.retain(|_, (stored_at, _)| stored_at.elapsed() < self.ttl);
entries.insert(key, (Instant::now(), bytes));
}
}
}
impl HypixelClient {
pub fn new(api_key: impl Into<String>) -> Self {
Self::builder().api_key(api_key).build()
}
pub fn unauthenticated() -> Self {
Self::builder().build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn rate_limit(&self) -> RateLimit {
self.rate_limit.load()
}
pub(crate) async fn request<T: DeserializeOwned>(
&self,
path: &str,
query: &[(&str, &str)],
requires_key: bool,
) -> Result<T> {
if requires_key && self.api_key.is_none() {
return Err(Error::MissingApiKey);
}
let cache_key = self.cache.as_ref().map(|cache| {
let mut key = path.to_string();
for (name, value) in query {
key.push_str(&format!("&{name}={value}"));
}
(cache, key)
});
if let Some((cache, key)) = &cache_key {
if let Some(bytes) = cache.get(key) {
return Ok(serde_json::from_slice(&bytes)?);
}
}
let mut attempt = 0;
let bytes = loop {
match self.send(path, query).await {
Ok(bytes) => break bytes,
Err(Error::RateLimited { retry_after }) if attempt < self.retry_rate_limited => {
attempt += 1;
tokio::time::sleep(retry_after.unwrap_or(Duration::from_secs(1))).await;
}
Err(err) => return Err(err),
}
};
if let Some((cache, key)) = cache_key {
cache.put(key, bytes.clone());
}
Ok(serde_json::from_slice(&bytes)?)
}
async fn send(&self, path: &str, query: &[(&str, &str)]) -> Result<Arc<[u8]>> {
let url = format!("{}{}", self.base_url, path);
let mut request = self.http.get(url);
if let Some(key) = &self.api_key {
request = request.header(API_KEY_HEADER, key);
}
if !query.is_empty() {
request = request.query(query);
}
let response = request.send().await?;
let status = response.status();
let headers = response.headers().clone();
self.rate_limit.store(RateLimit::from_headers(&headers));
if status.is_success() {
let bytes = response.bytes().await?;
return Ok(Arc::from(bytes.as_ref()));
}
match status.as_u16() {
403 => Err(Error::InvalidApiKey),
429 => {
let retry_after = headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.trim().parse().ok())
.map(Duration::from_secs);
Err(Error::RateLimited { retry_after })
}
code => {
let cause = response
.json::<ErrorBody>()
.await
.ok()
.and_then(|b| b.cause)
.unwrap_or_else(|| "unknown error".to_string());
Err(Error::Api {
status: code,
cause,
})
}
}
}
}
#[derive(serde::Deserialize)]
struct ErrorBody {
cause: Option<String>,
}
#[derive(Debug, Default)]
pub struct ClientBuilder {
api_key: Option<String>,
base_url: Option<String>,
http: Option<reqwest::Client>,
timeout: Option<Duration>,
retry_rate_limited: u32,
cache_ttl: Option<Duration>,
}
impl ClientBuilder {
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn http_client(mut self, http: reqwest::Client) -> Self {
self.http = Some(http);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn retry_on_rate_limit(mut self, retries: u32) -> Self {
self.retry_rate_limited = retries;
self
}
pub fn cache_ttl(mut self, ttl: Duration) -> Self {
self.cache_ttl = Some(ttl);
self
}
pub fn build(self) -> HypixelClient {
let http = self.http.unwrap_or_else(|| {
let mut builder = reqwest::Client::builder();
if let Some(timeout) = self.timeout {
builder = builder.timeout(timeout);
}
builder.build().unwrap_or_default()
});
HypixelClient {
http,
base_url: self
.base_url
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
api_key: self.api_key,
rate_limit: SharedRateLimit::default(),
retry_rate_limited: self.retry_rate_limited,
cache: self.cache_ttl.map(|ttl| ResponseCache {
ttl,
entries: Arc::default(),
}),
}
}
}