#![allow(clippy::missing_errors_doc)]
use std::{
any::{
Any,
TypeId,
type_name,
},
sync::Arc,
time::Duration,
};
use moka::future::Cache;
use super::{
Queryable,
error::Error,
language::Language,
models::items::Item,
};
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub nested_listener_sleep: Duration,
pub listener_sleep_timeout: Duration,
pub cache_404_item_requests: bool,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
nested_listener_sleep: Duration::from_mins(5),
listener_sleep_timeout: Duration::from_mins(5),
cache_404_item_requests: false,
}
}
}
type TypeCache = Cache<(Language, TypeId), Arc<dyn Any + Send + Sync>>;
type ItemCache = Cache<(Language, Box<str>), Option<Item>>;
#[derive(Debug, Clone)]
pub struct Client {
pub(crate) http: reqwest::Client,
pub(crate) base_url: Arc<str>,
pub(crate) config: Arc<ClientConfig>,
type_cache: TypeCache,
items_cache: ItemCache,
}
impl Default for Client {
fn default() -> Self {
Self {
http: reqwest::Client::new(),
base_url: "https://api.warframestat.us".into(),
config: Arc::new(ClientConfig::default()),
type_cache: Cache::builder()
.time_to_live(Duration::from_mins(5))
.build(),
items_cache: Cache::builder()
.time_to_live(Duration::from_hours(12))
.build(),
}
}
}
impl Client {
#[must_use]
pub fn new(
reqwest_client: reqwest::Client,
base_url: &str,
config: ClientConfig,
type_cache: TypeCache,
items_cache: ItemCache,
) -> Self {
Self {
http: reqwest_client,
base_url: base_url.into(),
config: Arc::new(config),
type_cache,
items_cache,
}
}
async fn type_cached<T, F>(&self, language: Language, fallback: F) -> Result<T::Return, Error>
where
T: Queryable,
F: AsyncFnOnce() -> Result<T::Return, Error>,
{
let type_id = TypeId::of::<T::Return>();
if let Some(item) = self
.type_cache
.get(&(language, type_id))
.await
.and_then(|any| any.downcast_ref::<T::Return>().cloned())
{
tracing::debug!("cache hit for type {}", type_name::<T::Return>());
return Ok(item);
}
let item = fallback().await?;
self.type_cache
.insert((language, type_id), Arc::new(item.clone()))
.await;
Ok(item)
}
pub async fn fetch<T>(&self) -> Result<T::Return, Error>
where
T: Queryable,
{
self.fetch_using_lang::<T>(Language::EN).await
}
pub async fn fetch_using_lang<T>(&self, language: Language) -> Result<T::Return, Error>
where
T: Queryable,
{
self.type_cached::<T, _>(language, async || {
T::query(&self.base_url.clone(), &self.http.clone(), language).await
})
.await
}
async fn cached_item<F>(
&self,
language: Language,
query: &str,
fallback: F,
) -> Result<Option<Item>, Error>
where
F: AsyncFnOnce() -> Result<Option<Item>, Error>,
{
let key = (language, Box::from(query));
if let Some(item) = self.items_cache.get(&key).await {
tracing::debug!("cache hit for {key:?}");
return Ok(item);
}
let maybe_item = fallback().await?;
if maybe_item.is_some() || self.config.cache_404_item_requests {
self.items_cache.insert(key, maybe_item.clone()).await;
}
Ok(maybe_item)
}
pub async fn query_item(&self, query: &str) -> Result<Option<Item>, Error> {
self.query_item_using_lang(query, Language::EN).await
}
pub async fn query_item_using_lang(
&self,
query: &str,
language: Language,
) -> Result<Option<Item>, Error> {
self.cached_item(language, query, async move || {
Item::query(
self.http.clone(),
format!(
"{}/items/{}/?language={}",
self.base_url,
urlencoding::encode(query),
language
),
)
.await
})
.await
}
}