use crate::api::Meme; use anyhow::bail; use reqwest::Client; use std::str::FromStr; #[macro_export] macro_rules! init_once_cell { ($cell:ident, $init_fn:expr) => { match $cell.get() { Some(x) => x, None => { let x = $init_fn; $cell.get_or_init(|| x) }, } }; } pub mod consts { pub const API_ENDPOINT: &str = "https://api.tilera.xyz/jensmemes/v1"; pub const NO_SUCH_CATEGORY_ERROR: &str = "The given Category does not exist!"; pub const NO_SUCH_USER_ERROR: &str = "The given User does not exist!"; } /// ways to communicyte with the JM API pub mod api { use super::consts; use crate::api::{Category, CatsResp, Meme, MemesResp, User, UsersResp}; use anyhow::Result; use lazy_static::lazy_static; use log::info; use once_cell::sync::OnceCell; use reqwest::Client; use std::{ collections::HashMap, sync::{Arc, Mutex}, }; use thiserror::Error; use url::Url; // cached api responses static USERS: OnceCell> = OnceCell::new(); static CATS: OnceCell> = OnceCell::new(); lazy_static! { // is this type long enough yet? static ref MEMES: Mutex, Option), Arc>>> = Mutex::new(HashMap::new()); } #[derive(Error, Debug)] #[error("Error Deserializing JSON api response: \n{json}\nError: {error}")] pub struct ApiDeserializeError { pub json: String, #[source] pub error: serde_json::Error, } pub async fn cats(http: &Client) -> Result<&Vec> { Ok(init_once_cell!(CATS, { info!("Requesting categories from server"); let res = http .get(&format!("{}/categories", consts::API_ENDPOINT)) .send() .await?; let cats = try_deserialize_api_reponse::(&res.bytes().await?)?; cats.categories.into_iter().collect() })) } pub async fn memes<'a>( http: &Client, cat_filter: Option, usr_filter: Option, ) -> Result>> { let filters = (cat_filter, usr_filter); if let Some(m) = MEMES.lock().unwrap().get(&filters) { return Ok(m.clone()); } let mut url = Url::options().parse(&format!("{}/memes", consts::API_ENDPOINT))?; let mut pairs = url.query_pairs_mut(); if let Some(cat) = filters.0.as_ref() { pairs.append_pair("category", cat); } if let Some(usr) = filters.1.as_ref() { pairs.append_pair("user", usr); } // drop required in order to move the URL into the request drop(pairs); info!("Requesting memes from server"); let res = http.get(url).send().await?; let memes = try_deserialize_api_reponse::(&res.bytes().await?)?; Ok(MEMES .lock() .unwrap() .entry(filters) .or_insert(Arc::new(memes.memes)) .clone()) } pub async fn users(http: &Client) -> Result<&Vec> { Ok(init_once_cell!(USERS, { info!("Requesting users from server"); let res = http .get(&format!("{}/users", consts::API_ENDPOINT)) .send() .await?; let users = try_deserialize_api_reponse::(&res.bytes().await?)?; users.users })) } /// tries to deserialize `json` as `T`, and if it fails converts the error /// to a `ApiDeserializeError` pub fn try_deserialize_api_reponse<'a, T: serde::Deserialize<'a>>(json: &'a [u8]) -> Result { let result = serde_json::from_slice::(&json); result.map_err(|error| match std::str::from_utf8(json) { Err(e) => e.into(), Ok(json) => ApiDeserializeError { json: json.into(), error, } .into(), }) } #[cfg(test)] mod tests { use super::*; #[test] fn api_deserialize_error() { let incorrect_json = r#" { "foo": "bar" } "#; let result = try_deserialize_api_reponse::(incorrect_json.as_bytes()); println!("{}", result.unwrap_err()); } } } pub enum MemeSorting { Id, Link, Category, User, Timestamp, } impl MemeSorting { pub fn sort_with(&self, memes: &mut [&Meme]) { macro_rules! sort { ($list:ident, $field:ident) => { $list.sort_by(|a, b| { a.$field .to_ascii_lowercase() .cmp(&b.$field.to_ascii_lowercase()) }); }; } match self { Self::Id => sort!(memes, id), Self::Link => sort!(memes, link), Self::Category => sort!(memes, category), Self::User => sort!(memes, user), Self::Timestamp => memes.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)), } } } impl FromStr for MemeSorting { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s.to_lowercase().as_ref() { "id" => Ok(Self::Id), "link" => Ok(Self::Link), "category" => Ok(Self::Category), "user" => Ok(Self::User), "timestamp" => Ok(Self::Timestamp), _ => bail!("Invalid Meme sorting! options are id, link, category and user!"), } } } pub async fn assert_user_exists(http: &Client, user: &str) -> anyhow::Result<()> { if !api::users(http).await?.iter().any(|u| u.name == user) { bail!(consts::NO_SUCH_USER_ERROR); } Ok(()) } pub async fn assert_category_exists(http: &Client, cat: &str) -> anyhow::Result<()> { if !api::cats(http).await?.iter().any(|c| c.id == cat) { bail!(consts::NO_SUCH_CATEGORY_ERROR); } Ok(()) }