move client core to seperate crate

This commit is contained in:
LordMZTE 2021-04-01 19:12:23 +02:00
commit f7f5dcb1b3
4 changed files with 287 additions and 0 deletions

20
Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "jm_client_core"
version = "0.1.0"
authors = ["LordMZTE <lord@mzte.de>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.34"
env_logger = "0.8.2"
lazy_static = "1.4.0"
log = "0.4.11"
once_cell = "1.5.2"
reqwest = "0.10.9"
serde = { version = "1.0.117", features = ["derive"] }
serde_json = "1.0.60"
thiserror = "1.0.23"
tokio = { version = "0.2.23", features = ["macros", "fs", "process"] }
url = "2.2.0"

64
src/api.rs Normal file
View file

@ -0,0 +1,64 @@
use anyhow::{anyhow, Result};
use serde::Deserialize;
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct UpResp {
pub files: Vec<String>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct CatsResp {
pub categories: Vec<Category>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct UsersResp {
pub users: Vec<User>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct MemesResp {
pub memes: Vec<Meme>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Category {
pub id: String,
pub name: String,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Meme {
pub id: String,
pub link: String,
pub path: String,
pub category: String,
pub user: String,
}
impl Meme {
pub fn file_name(&self) -> Result<&str> {
self.path
.split('/')
.last()
.ok_or_else(|| anyhow!("failed to get file name. server response invalid"))
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct User {
pub name: String,
pub id: Option<String>,
pub tokenhash: Option<String>,
pub userdir: Option<String>,
pub dayuploads: String,
}
impl User {
pub fn get_id(&self) -> Option<&String> {
self.id
.as_ref()
.or(self.tokenhash.as_ref())
.or(self.userdir.as_ref())
}
}

2
src/lib.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod api;
pub mod util;

201
src/util.rs Normal file
View file

@ -0,0 +1,201 @@
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 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 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<Vec<User>> = OnceCell::new();
static CATS: OnceCell<Vec<Category>> = OnceCell::new();
lazy_static! {
// is this type long enough yet?
static ref MEMES: Mutex<HashMap<(Option<String>, Option<String>), Arc<Vec<Meme>>>> =
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<Category>> {
Ok(init_once_cell!(CATS, {
info!("Requesting categories from server");
let res = http
.get("https://data.tilera.xyz/api/jensmemes/categories")
.send()
.await?;
let cats = try_deserialize_api_reponse::<CatsResp>(&res.bytes().await?)?;
cats.categories.into_iter().collect()
}))
}
pub async fn memes<'a>(
http: &Client,
cat_filter: Option<String>,
usr_filter: Option<String>,
) -> Result<Arc<Vec<Meme>>> {
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("https://data.tilera.xyz/api/jensmemes/memes")?;
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::<MemesResp>(&res.bytes().await?)?;
Ok(MEMES
.lock()
.unwrap()
.entry(filters)
.or_insert(Arc::new(memes.memes))
.clone())
}
pub async fn users(http: &Client) -> Result<&Vec<User>> {
Ok(init_once_cell!(USERS, {
info!("Requesting users from server");
let res = http
.get("https://data.tilera.xyz/api/jensmemes/users")
.send()
.await?;
let users = try_deserialize_api_reponse::<UsersResp>(&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<T> {
let result = serde_json::from_slice::<T>(&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::<UsersResp>(incorrect_json.as_bytes());
println!("{}", result.unwrap_err());
}
}
}
pub enum MemeSorting {
Id,
Link,
Category,
User,
}
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),
}
}
}
impl FromStr for MemeSorting {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_ref() {
"id" => Ok(Self::Id),
"link" => Ok(Self::Link),
"category" => Ok(Self::Category),
"user" => Ok(Self::User),
_ => 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(())
}