use crate::{
api::auth::{Login, Logout, RefreshToken},
errors::{Errors, Result},
schema::auth::{AuthTokens, RefreshTokenResponse},
Endpoint, FromResponse, UrlSerdeQS,
};
use reqwest::Url;
use serde::de::DeserializeOwned;
#[cfg(not(target_arch = "wasm32"))]
static APP_USER_AGENT: &str = concat!(
env!("CARGO_PKG_NAME"),
"-rs",
"/",
env!("CARGO_PKG_VERSION"),
);
#[derive(Debug, Clone)]
pub struct Client {
http: reqwest::Client,
base_url: Url,
tokens: Option<AuthTokens>,
}
impl Default for Client {
fn default() -> Self {
Self::new("https://api.mangadex.org/").expect("Error creating default API client")
}
}
impl Client {
pub fn new(base_url: &str) -> Result<Self> {
let client = reqwest::Client::builder();
#[cfg(not(target_arch = "wasm32"))]
let client = client.user_agent(APP_USER_AGENT);
let client = client.build()?;
Ok(Self {
http: client,
base_url: Url::parse(base_url)?,
tokens: None,
})
}
pub(crate) async fn send_request<E>(&self, endpoint: &E) -> Result<E::Response>
where
E: Endpoint,
<<E as Endpoint>::Response as FromResponse>::Response: DeserializeOwned,
{
let mut endpoint_url = self.base_url.join(&endpoint.path())?;
if let Some(query) = endpoint.query() {
endpoint_url = endpoint_url.query_qs(query);
}
let mut req = self.http.request(endpoint.method(), endpoint_url);
if let Some(body) = endpoint.body() {
req = req.json(body);
}
if let Some(multipart) = endpoint.multipart() {
req = req.multipart(multipart);
}
if let Some(tokens) = self.get_tokens() {
req = req.bearer_auth(&tokens.session);
} else if endpoint.require_auth() {
return Err(Errors::MissingTokens);
}
let res = req.send().await?;
let res = res
.json::<<E::Response as FromResponse>::Response>()
.await?;
Ok(FromResponse::from_response(res))
}
pub async fn login(&mut self, username: &str, password: &str) -> Result<&AuthTokens> {
let tokens = Login { username, password }.send(self).await?.tokens;
self.set_tokens(Some(tokens));
Ok(self.get_tokens().unwrap())
}
pub fn get_tokens(&self) -> Option<&AuthTokens> {
self.tokens.as_ref()
}
pub fn set_tokens(&mut self, tokens: Option<AuthTokens>) {
self.tokens = tokens;
}
pub async fn logout(&mut self) -> Result<()> {
Logout.send(self).await?;
self.set_tokens(None);
Ok(())
}
pub async fn refresh_tokens(&mut self) -> Result<RefreshTokenResponse> {
let refresh_token = &self.get_tokens().ok_or(Errors::MissingTokens)?.refresh;
let res = RefreshToken { refresh_token }.send(self).await?;
self.set_tokens(Some(res.tokens.clone()));
Ok(res)
}
pub async fn ping(&self) -> Result<()> {
let endpoint = self.base_url.join("/ping")?;
let res = self.http.get(endpoint).send().await?;
if res.text().await? == "pong" {
Ok(())
} else {
Err(Errors::PingError)
}
}
}
macro_rules! impl_endpoint {
{
$method:ident $path:tt,
#[$payload:ident $($auth:ident)?] $typ:ty,
$(#[$out_res:ident])? $out:ty
} => {
impl $crate::common::Endpoint for $typ {
type Response = $out;
fn method(&self) -> reqwest::Method {
reqwest::Method::$method
}
impl_endpoint! { @path $path }
impl_endpoint! { @payload $payload }
$(impl_endpoint! { @$auth })?
}
impl_endpoint! { @send $(:$out_res)?, $typ, $out }
};
{ @path ($path:expr, $($arg:ident),+) } => {
fn path(&self) -> std::borrow::Cow<str> {
std::borrow::Cow::Owned(format!($path, $(self.$arg),+))
}
};
{ @path $path:expr } => {
fn path(&self) -> std::borrow::Cow<str> {
std::borrow::Cow::Borrowed($path)
}
};
{ @payload query } => {
type Query = Self;
type Body = ();
fn query(&self) -> Option<&Self::Query> {
Some(&self)
}
};
{ @payload body } => {
type Query = ();
type Body = Self;
fn body(&self) -> Option<&Self::Body> {
Some(&self)
}
};
{ @payload no_data } => {
type Query = ();
type Body = ();
};
{ @auth } => {
fn require_auth(&self) -> bool {
true
}
};
{ @send, $typ:ty, $out:ty } => {
impl $typ {
pub async fn send(&self, client: &$crate::Client) -> $crate::Result<$out> {
client.send_request(self).await
}
}
};
{ @send:flatten_result, $typ:ty, $out:ty } => {
impl $typ {
pub async fn send(&self, client: &$crate::Client) -> $out {
client.send_request(self).await?
}
}
};
{ @send:discard_result, $typ:ty, $out:ty } => {
impl $typ {
pub async fn send(&self, client: &$crate::Client) -> $crate::Result<()> {
client.send_request(self).await??;
Ok(())
}
}
};
{ @send:no_send, $typ:ty, $out:ty } => { };
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_new() {
Client::default();
}
#[tokio::test]
async fn ping_server() {
let client = Client::default();
client.ping().await.unwrap();
}
}