use super::data::*;
use super::error::Error;
use super::kind::NanoKind;
use std::collections::HashMap;
use std::cell::RefCell;
use reqwest::{Client, Method, StatusCode};
use serde::Serialize;
use serde::de::DeserializeOwned;
#[cfg(test)]
mod tests;
#[derive(Debug)]
pub struct NanoClient {
client: Client,
username: String,
password: String,
token: RefCell<Option<String>>,
}
impl NanoClient {
const BASE_URL: &'static str = "https://api.nanowrimo.org/";
fn new(user: &str, pass: &str) -> NanoClient {
NanoClient {
client: Client::new(),
username: user.to_string(),
password: pass.to_string(),
token: RefCell::new(None),
}
}
pub fn new_anon() -> NanoClient {
NanoClient::new("", "")
}
pub async fn new_user(user: &str, pass: &str) -> Result<NanoClient, Error> {
let client = NanoClient::new(user, pass);
client.login().await?;
Ok(client)
}
async fn make_request<T, U>(&self, path: &str, method: Method, data: &T) -> Result<U, Error>
where
T: Serialize + ?Sized,
U: DeserializeOwned + std::fmt::Debug
{
let mut query = None;
let mut json = None;
match method {
Method::GET => query = Some(data),
_ => json = Some(data)
}
let mut req = self.client.request(method, &format!("{}{}", NanoClient::BASE_URL, path));
if let Some(token) = &*self.token.borrow() {
req = req.header("Authorization", token)
}
if let Some(query) = query {
req = req.query(query);
}
if let Some(json) = json {
req = req.json(json)
}
let resp = req.send()
.await?;
let status = resp.status();
match status {
StatusCode::INTERNAL_SERVER_ERROR => return Err(
Error::NanoError(status, "Internal Server Error".to_string())
),
StatusCode::NOT_FOUND => return Err(
Error::NanoError(status, "Page Not Found".to_string())
),
_ => ()
}
let nano_resp = resp
.json()
.await?;
match nano_resp {
NanoResponse::Success(val) => Ok(val),
NanoResponse::Error(err) => Err(Error::NanoError(status, err.error)),
NanoResponse::Unknown(val) => panic!("Couldn't parse valid JSON as NanoResponse:\n{}", val)
}
}
async fn retry_request<T, U>(&self, path: &str, method: Method, data: &T) -> Result<U, Error>
where
T: Serialize + ?Sized,
U: DeserializeOwned + std::fmt::Debug
{
let res = self.make_request(path, method.clone(), data).await;
match res {
Err(Error::NanoError(code, _)) if code == StatusCode::UNAUTHORIZED && self.is_logged_in() => {
self.login().await?;
self.make_request(path, method, data).await
},
_ => res
}
}
pub fn is_logged_in(&self) -> bool {
self.token.borrow().is_none()
}
pub async fn login(&self) -> Result<(), Error> {
let mut map = HashMap::new();
map.insert("identifier", &self.username);
map.insert("password", &self.password);
let res = self.make_request::<_, LoginResponse>("users/sign_in", Method::POST, &map)
.await?;
self.token.replace(Some(res.auth_token));
Ok(())
}
pub async fn logout(&self) -> Result<(), Error> {
self.make_request::<_, ()>("users/logout", Method::POST, &()).await?;
self.token.replace(None);
Ok(())
}
pub async fn change_user(&mut self, user: Option<&str>, pass: Option<&str>) -> Result<(), Error> {
if self.is_logged_in() {
self.logout().await?;
}
if user.is_some() && pass.is_some() {
self.username = user.unwrap().to_string();
self.password = pass.unwrap().to_string();
self.login().await?;
} else if user.is_none() && pass.is_none() {
self.username = "".to_string();
self.password = "".to_string();
self.token.replace(None);
} else {
panic!("Either both user and pass must be provided, or neither")
}
Ok(())
}
pub async fn fundometer(&self) -> Result<Fundometer, Error> {
self.retry_request("fundometer", Method::GET, &()).await
}
pub async fn search(&self, name: &str) -> Result<CollectionResponse, Error> {
self.retry_request("search", Method::GET, &[("q", name)]).await
}
pub async fn random_offer(&self) -> Result<ItemResponse, Error> {
self.retry_request("random_offer", Method::GET, &()).await
}
pub async fn store_items(&self) -> Result<Vec<StoreItem>, Error> {
self.retry_request("store_items", Method::GET, &()).await
}
pub async fn offers(&self) -> Result<Vec<ItemResponse>, Error> {
self.retry_request("offers", Method::GET, &()).await
}
pub async fn current_user_includes(&self, include: &[NanoKind]) -> Result<ItemResponse, Error> {
let mut data = Vec::new();
if !include.is_empty() {
data.push(
("include".to_string(), include.iter().map(|kind| kind.api_name()).collect::<Vec<&str>>().join(","))
)
}
self.retry_request("users/current", Method::GET, &data).await
}
pub async fn current_user(&self) -> Result<ItemResponse, Error> {
self.current_user_includes(&[]).await
}
pub async fn pages(&self, page: &str) -> Result<ItemResponse, Error> {
self.retry_request(&format!("pages/{}", page), Method::GET, &()).await
}
pub async fn notifications(&self) -> Result<CollectionResponse, Error> {
self.retry_request("notifications", Method::GET, &()).await
}
pub async fn get_all_include_filtered(&self, ty: NanoKind, include: &[NanoKind], filter: &[(&str, u64)]) -> Result<CollectionResponse, Error> {
let mut data: Vec<(String, String)> = Vec::new();
for i in filter {
data.push(
(format!("filter[{}]", i.0), i.1.to_string())
)
}
if !include.is_empty() {
data.push(
("include".to_string(), include.iter().map(|kind| kind.api_name()).collect::<Vec<&str>>().join(","))
)
}
self.retry_request(ty.api_name(), Method::GET, &data).await
}
pub async fn get_all_filtered(&self, ty: NanoKind, filter: &[(&str, u64)]) -> Result<CollectionResponse, Error> {
self.get_all_include_filtered(ty, &[], filter).await
}
pub async fn get_all_include(&self, ty: NanoKind, include: &[NanoKind]) -> Result<CollectionResponse, Error> {
self.get_all_include_filtered(ty, include, &[]).await
}
pub async fn get_all(&self, ty: NanoKind) -> Result<CollectionResponse, Error> {
self.get_all_include_filtered(ty, &[], &[]).await
}
pub async fn get_id_include(&self, ty: NanoKind, id: u64, include: &[NanoKind]) -> Result<ItemResponse, Error> {
let mut data: Vec<(String, String)> = Vec::new();
if !include.is_empty() {
data.push(
("include".to_string(), include.iter().map(|kind| kind.api_name()).collect::<Vec<&str>>().join(","))
)
}
self.retry_request(&format!("{}/{}", ty.api_name(), id), Method::GET, &data).await
}
pub async fn get_id(&self, ty: NanoKind, id: u64) -> Result<ItemResponse, Error> {
self.get_id_include(ty, id, &[]).await
}
pub async fn get_all_related(&self, rel: &RelationLink) -> Result<CollectionResponse, Error> {
if !rel.related.ends_with("s") {
panic!("get_all_related can only get many-relation links")
}
self.retry_request(&rel.related, Method::GET, &()).await
}
pub async fn get_unique_related(&self, rel: &RelationLink) -> Result<ItemResponse, Error> {
if rel.related.ends_with("s") {
panic!("get_unique_related can only get single-relation links")
}
self.retry_request(&rel.related, Method::GET, &()).await
}
}