use crate::api::{Category, CatsResp, Meme, MemesResp, User, UsersResp}; use log::info; use once_cell::sync::OnceCell; use reqwest::Url; use serde::Deserialize; use std::sync::Arc; use thiserror::Error; use tokio::sync::Mutex; #[derive(Debug)] pub struct JMClient { pub http: reqwest::Client, cache: Mutex, endpoint: Url, } macro_rules! init_cache { ($cell:expr, $init_fn:expr) => {{ let cell = &$cell; Arc::clone(match cell.get() { Some(x) => x, None => { let x = Arc::new($init_fn); cell.get_or_init(|| x) }, }) }}; } impl JMClient { pub fn new() -> Self { Self::builder().build() } pub fn builder() -> JMClientBuilder { JMClientBuilder::default() } /// clears the cache. all requests will be made again after this has been /// called. pub async fn clear_cache(&mut self) { self.cache.lock().await.clear(); } async fn get_api_json( &self, cache: &OnceCell>, endpoint: &str, res_to_data: F, ) -> Result, JMClientError> where for<'de> R: Deserialize<'de>, F: FnOnce(R) -> T, { Ok(init_cache!(cache, { info!("Requesting {} from server", endpoint); let url = self.endpoint.join(endpoint)?; let res = self.http.get(url).send().await?; let res = serde_json::from_slice(&res.bytes().await?)?; res_to_data(res) })) } pub async fn get_cats(&self) -> Result>, JMClientError> { self.get_api_json( &self.cache.lock().await.cats, "categories", |r: CatsResp| r.categories, ) .await } pub async fn get_memes(&self) -> Result>, JMClientError> { self.get_api_json(&self.cache.lock().await.memes, "memes", |r: MemesResp| { r.memes }) .await } pub async fn get_users(&self) -> Result>, JMClientError> { self.get_api_json(&self.cache.lock().await.users, "users", |r: UsersResp| { r.users }) .await } } impl Default for JMClient { fn default() -> Self { Self::new() } } #[derive(Debug, Error)] pub enum JMClientError { #[error("Error making http request: {0}")] Http(#[from] reqwest::Error), #[error("Error deserializing JensMemes response: {0}")] Deserialize(#[from] serde_json::Error), #[error("Failed parsing URL to make request to JensMemes: {0}")] UrlParse(#[from] url::ParseError), } #[derive(Debug, Default)] struct Cache { users: OnceCell>>, cats: OnceCell>>, memes: OnceCell>>, } impl Cache { fn clear(&mut self) { self.users = OnceCell::default(); self.cats = OnceCell::default(); self.memes = OnceCell::default(); } } #[derive(Debug, Default)] pub struct JMClientBuilder { client: Option, endpoint: Option, } impl JMClientBuilder { #[must_use] pub fn build(self) -> JMClient { JMClient { http: self.client.unwrap_or_else(reqwest::Client::new), endpoint: self.endpoint.unwrap_or_else(|| { // unwrapping is fine here, as the hardcoded input is known to work. Url::parse(crate::util::consts::API_ENDPOINT).unwrap() }), cache: Mutex::new(Cache::default()), } } #[must_use] pub fn client(mut self, client: reqwest::Client) -> Self { self.client = Some(client); self } #[must_use] pub fn endpoint(mut self, endpoint: impl Into) -> Self { self.endpoint = Some(endpoint.into()); self } }