jensmemesclient/jm_client_core/src/client.rs

148 lines
3.7 KiB
Rust

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<Cache>,
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<R, T, F>(
&self,
cache: &OnceCell<Arc<T>>,
endpoint: &str,
res_to_data: F,
) -> Result<Arc<T>, 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<Arc<Vec<Category>>, JMClientError> {
self.get_api_json(
&self.cache.lock().await.cats,
"categories",
|r: CatsResp| r.categories,
)
.await
}
pub async fn get_memes(&self) -> Result<Arc<Vec<Meme>>, JMClientError> {
self.get_api_json(&self.cache.lock().await.memes, "memes", |r: MemesResp| {
r.memes
})
.await
}
pub async fn get_users(&self) -> Result<Arc<Vec<User>>, 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<Arc<Vec<User>>>,
cats: OnceCell<Arc<Vec<Category>>>,
memes: OnceCell<Arc<Vec<Meme>>>,
}
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<reqwest::Client>,
endpoint: Option<Url>,
}
impl JMClientBuilder {
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()),
}
}
pub fn client(mut self, client: reqwest::Client) -> Self {
self.client = Some(client);
self
}
pub fn endpoint(mut self, endpoint: impl Into<Url>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
}