use crate::error::{Error, Result};
#[cfg(feature = "cache")]
use moka::future::Cache;
use reqwest::header;
use reqwest::Client;
use serde::de::DeserializeOwned;
#[allow(unused_imports)]
use std::time::Duration;
#[cfg(feature = "cache")]
const DEFAULT_CACHE_TTL: Duration = Duration::from_secs(60 * 10);
#[cfg(feature = "cache")]
const DEFAULT_CACHE_CAPACITY: u64 = 10_000;
#[derive(Clone, Default)]
pub(crate) struct ApiCore {
client: Client,
#[cfg(feature = "cache")]
cache: Option<Cache<String, Vec<u8>>>,
}
impl ApiCore {
#[cfg(feature = "cache")]
fn default_cache() -> Cache<String, Vec<u8>> {
Cache::builder()
.max_capacity(DEFAULT_CACHE_CAPACITY)
.time_to_live(DEFAULT_CACHE_TTL)
.build()
}
fn new() -> Self {
let client = Client::builder()
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.default_headers({
let mut h = header::HeaderMap::new();
h.insert(
header::ACCEPT,
header::HeaderValue::from_static("application/json"),
);
h
})
.build()
.unwrap_or_else(|_| Client::new());
Self {
client,
#[cfg(feature = "cache")]
cache: Some(Self::default_cache()),
}
}
fn new_with_client(client: Client) -> Self {
Self {
client,
#[cfg(feature = "cache")]
cache: Some(Self::default_cache()),
}
}
#[cfg(feature = "cache")]
fn set_cache(&mut self, capacity: u64, time_to_live: Duration) {
self.cache = Some(
Cache::builder()
.max_capacity(capacity)
.time_to_live(time_to_live)
.build(),
);
}
async fn send_request(&self, url: &str) -> Result<Vec<u8>> {
let response = self
.client
.get(url)
.timeout(Duration::from_secs(30))
.send()
.await?;
let status = response.status();
let body = response.bytes().await?.to_vec();
if body.is_empty() {
return Err(Error::EmptyBody);
}
if !status.is_success() {
let msg = String::from_utf8_lossy(&body).chars().take(2048).collect();
return Err(Error::HttpStatus { status, body: msg });
}
Ok(body)
}
pub async fn _generator<T>(&self, url: &str) -> Result<T>
where
T: DeserializeOwned + Send + Sync + 'static,
{
#[cfg(feature = "cache")]
{
self._generator_cached(url).await
}
#[cfg(not(feature = "cache"))]
{
self._generator_no_cache(url).await
}
}
#[cfg(feature = "cache")]
async fn _generator_cached<T>(&self, url: &str) -> Result<T>
where
T: DeserializeOwned + Send + Sync + 'static,
{
let type_name = std::any::type_name::<T>();
let cache_key = format!("{}:{}", type_name, url);
match &self.cache {
Some(cache) => {
if let Some(value) = cache.get(&cache_key).await {
self.parse_response::<T>(value.as_slice())
} else {
let body = self.send_request(url).await?;
cache.insert(cache_key, body.clone()).await;
self.parse_response::<T>(body.as_slice())
}
}
None => self._generator_no_cache(url).await,
}
}
pub async fn _generator_no_cache<T>(&self, url: &str) -> Result<T>
where
T: DeserializeOwned,
{
let body = self.send_request(url).await?;
self.parse_response::<T>(body.as_slice())
}
fn parse_response<T>(&self, body: &[u8]) -> Result<T>
where
T: DeserializeOwned,
{
#[cfg(feature = "ddnet")]
{
let trimmed = trim_ascii(body);
if trimmed == b"{}" {
return Err(Error::NotFound);
}
}
#[cfg(feature = "ddstats")]
{
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum MaybeError<T> {
Err { error: String },
Ok(T),
}
match serde_json::from_slice::<MaybeError<T>>(body)? {
MaybeError::Err { error } => {
if error.eq_ignore_ascii_case("player not found") {
Err(Error::NotFound)
} else {
Err(Error::RemoteMessage(error))
}
}
MaybeError::Ok(v) => Ok(v),
}
}
#[cfg(not(feature = "ddstats"))]
{
Ok(serde_json::from_slice(body)?)
}
}
}
fn trim_ascii(mut s: &[u8]) -> &[u8] {
while let Some((&b, rest)) = s.split_first() {
if !b.is_ascii_whitespace() {
break;
}
s = rest;
}
while let Some((&b, rest)) = s.split_last() {
if !b.is_ascii_whitespace() {
break;
}
s = rest;
}
s
}
pub trait HasApiCore {
fn core(&self) -> &ApiCore;
}
#[derive(Clone, Default)]
pub struct DDApi {
core: ApiCore,
}
impl HasApiCore for DDApi {
fn core(&self) -> &ApiCore {
&self.core
}
}
impl DDApi {
pub fn new() -> Self {
DDApi {
core: ApiCore::new(),
}
}
pub fn new_with_client(client: Client) -> Self {
DDApi {
core: ApiCore::new_with_client(client),
}
}
#[cfg(feature = "cache")]
pub fn set_cache(&mut self, capacity: u64, time_to_live: Duration) {
self.core.set_cache(capacity, time_to_live);
}
pub async fn _generator<T>(&self, url: &str) -> Result<T>
where
T: DeserializeOwned + Send + Sync + 'static,
{
self.core._generator(url).await
}
pub async fn _generator_no_cache<T>(&self, url: &str) -> Result<T>
where
T: DeserializeOwned,
{
self.core._generator_no_cache(url).await
}
}
#[derive(Clone, Default)]
pub struct DDnetClient {
core: ApiCore,
}
impl HasApiCore for DDnetClient {
fn core(&self) -> &ApiCore {
&self.core
}
}
impl DDnetClient {
pub fn new() -> Self {
Self {
core: ApiCore::new(),
}
}
pub fn new_with_client(client: Client) -> Self {
Self {
core: ApiCore::new_with_client(client),
}
}
#[cfg(feature = "cache")]
pub fn set_cache(&mut self, capacity: u64, time_to_live: Duration) {
self.core.set_cache(capacity, time_to_live);
}
}
#[derive(Clone, Default)]
pub struct DDstatsClient {
core: ApiCore,
}
impl HasApiCore for DDstatsClient {
fn core(&self) -> &ApiCore {
&self.core
}
}
impl DDstatsClient {
pub fn new() -> Self {
Self {
core: ApiCore::new(),
}
}
pub fn new_with_client(client: Client) -> Self {
Self {
core: ApiCore::new_with_client(client),
}
}
#[cfg(feature = "cache")]
pub fn set_cache(&mut self, capacity: u64, time_to_live: Duration) {
self.core.set_cache(capacity, time_to_live);
}
}
#[cfg(feature = "ddnet")]
pub mod ddnet;
#[cfg(feature = "ddstats")]
pub mod ddstats;