use std::sync::Arc;
use derive_builder::Builder;
use mangadex_api_schema::v5::oauth::ClientInfo;
use mangadex_api_schema::ApiResult;
use reqwest::{Client, Response, StatusCode};
use serde::de::DeserializeOwned;
use tokio::sync::RwLock;
use url::Url;
use crate::error::Error;
use crate::rate_limit::Limited;
use crate::v5::AuthTokens;
use crate::{
traits::{Endpoint, FromResponse, UrlSerdeQS},
Result,
};
use crate::{API_DEV_URL, API_URL};
pub type HttpClientRef = Arc<RwLock<HttpClient>>;
#[derive(Debug, Builder, Clone)]
#[builder(
setter(into, strip_option),
default,
build_fn(error = "crate::error::BuilderError")
)]
pub struct HttpClient {
pub client: Client,
pub base_url: Url,
auth_tokens: Option<AuthTokens>,
captcha: Option<String>,
#[cfg(feature = "oauth")]
client_info: Option<ClientInfo>,
}
impl Default for HttpClient {
fn default() -> Self {
Self {
client: crate::get_default_client_api(),
base_url: Url::parse(API_URL).expect("error parsing the base url"),
auth_tokens: None,
captcha: None,
#[cfg(feature = "oauth")]
client_info: None,
}
}
}
impl HttpClient {
pub fn new(client: Client) -> Self {
Self {
client,
base_url: Url::parse(API_URL).expect("error parsing the base url"),
..Default::default()
}
}
pub fn builder() -> HttpClientBuilder {
HttpClientBuilder::default()
.client(crate::get_default_client_api())
.base_url(Url::parse(API_URL).expect("error parsing the base url"))
.clone()
}
pub(crate) async fn send_request_without_deserializing_with_other_base_url<E>(
&self,
endpoint: &E,
base_url: &url::Url,
) -> Result<reqwest::Response>
where
E: Endpoint,
{
let mut endpoint_url = base_url.join(&endpoint.path())?;
if let Some(query) = endpoint.query() {
endpoint_url = endpoint_url.query_qs(query);
}
let mut req = self.client.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 endpoint.require_auth() {
let tokens = self.get_tokens().ok_or(Error::MissingTokens)?;
req = req.bearer_auth(&tokens.session);
}
if let Some(captcha) = self.get_captcha() {
req = req.header("X-Captcha-Result", captcha);
}
Ok(req.send().await?)
}
pub(crate) async fn send_request_without_deserializing<E>(
&self,
endpoint: &E,
) -> Result<reqwest::Response>
where
E: Endpoint,
{
self.send_request_without_deserializing_with_other_base_url(endpoint, &self.base_url)
.await
}
pub(crate) async fn send_request_with_checks<E>(
&self,
endpoint: &E,
) -> Result<reqwest::Response>
where
E: Endpoint,
{
let res = self.send_request_without_deserializing(endpoint).await?;
let status_code = res.status();
if status_code.as_u16() == 429 {
return Err(Error::RateLimitExcedeed);
}
if status_code == StatusCode::SERVICE_UNAVAILABLE {
return Err(Error::ServiceUnavailable(res.text().await.ok()));
}
if status_code.is_server_error() {
return Err(Error::ServerError(status_code.as_u16(), res.text().await?));
}
Ok(res)
}
pub(crate) async fn handle_result<T>(&self, res: Response) -> Result<T>
where
T: DeserializeOwned,
{
Ok(res.json::<ApiResult<T>>().await?.into_result()?)
}
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 res = self.send_request_with_checks(endpoint).await?;
let res = res
.json::<<E::Response as FromResponse>::Response>()
.await?;
Ok(FromResponse::from_response(res))
}
pub(crate) async fn send_request_with_rate_limit<E>(
&self,
endpoint: &E,
) -> Result<Limited<E::Response>>
where
E: Endpoint,
<<E as Endpoint>::Response as FromResponse>::Response: DeserializeOwned,
{
use crate::rate_limit::RateLimit;
let resp = self.send_request_with_checks(endpoint).await?;
let some_rate_limit = <RateLimit as TryFrom<&Response>>::try_from(&resp);
let res = self
.handle_result::<<E::Response as FromResponse>::Response>(resp)
.await?;
Ok(Limited {
rate_limit: some_rate_limit?,
body: FromResponse::from_response(res),
})
}
pub fn get_tokens(&self) -> Option<&AuthTokens> {
self.auth_tokens.as_ref()
}
pub fn set_auth_tokens(&mut self, auth_tokens: &AuthTokens) {
self.auth_tokens = Some(auth_tokens.clone());
}
pub fn clear_auth_tokens(&mut self) {
self.auth_tokens = None;
}
pub fn get_captcha(&self) -> Option<&String> {
self.captcha.as_ref()
}
pub fn set_captcha<T: Into<String>>(&mut self, captcha: T) {
self.captcha = Some(captcha.into());
}
pub fn clear_captcha(&mut self) {
self.captcha = None;
}
#[cfg(feature = "oauth")]
#[cfg_attr(docsrs, doc(cfg(feature = "oauth")))]
pub fn set_client_info(&mut self, client_info: &ClientInfo) {
self.client_info = Some(client_info.clone());
}
#[cfg(feature = "oauth")]
#[cfg_attr(docsrs, doc(cfg(feature = "oauth")))]
pub fn get_client_info(&self) -> Option<&ClientInfo> {
self.client_info.as_ref()
}
#[cfg(feature = "oauth")]
#[cfg_attr(docsrs, doc(cfg(feature = "oauth")))]
pub fn clear_client_info(&mut self) {
self.client_info = None;
}
pub fn api_dev_client() -> Self {
Self {
client: Client::new(),
base_url: Url::parse(API_DEV_URL).expect("error parsing the base url"),
auth_tokens: None,
captcha: None,
#[cfg(feature = "oauth")]
client_info: None,
}
}
}
macro_rules! builder_send {
{
#[$builder:ident] $typ:ty,
$(#[$out_res:ident])? $out_type:ty
} => {
builder_send! { @send $(:$out_res)?, $typ, $out_type }
};
{ @send, $typ:ty, $out_type:ty } => {
impl $typ {
pub async fn send(&self) -> crate::Result<$out_type>{
self.build()?.send().await
}
}
};
{ @send:discard_result, $typ:ty, $out_type:ty } => {
impl $typ {
pub async fn send(&self) -> crate::Result<()>{
self.build()?.send().await?;
Ok(())
}
}
};
{ @send:flatten_result, $typ:ty, $out_type:ty } => {
impl $typ {
pub async fn send(&self) -> $out_type{
self.build()?.send().await
}
}
};
{ @send:rate_limited, $typ:ty, $out_type:ty } => {
impl $typ {
pub async fn send(&self) -> crate::Result<crate::rate_limit::Limited<$out_type>>{
self.build()?.send().await
}
}
};
{ @send:no_send, $typ:ty, $out_type:ty } => {
impl $typ {
pub async fn send(&self) -> $out_type{
self.build()?.send().await
}
}
};
}
macro_rules! endpoint {
{
$method:ident $path:tt,
#[$payload:ident $($auth:ident $(=> $field:ident)?)?] $typ:ty,
$(#[$out_res:ident])? $out:ty
$(,$builder_ty:ty)?
} => {
impl crate::traits::Endpoint for $typ {
type Response = $out;
fn method(&self) -> reqwest::Method {
reqwest::Method::$method
}
endpoint! { @path $path }
endpoint! { @payload $payload }
$(endpoint! { @$auth $(, $field)? })?
}
endpoint! { @send $(:$out_res)?, $typ, $out $(,$builder_ty)? }
};
{ @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
}
};
{ @auth, $field:ident } => {
fn require_auth(&self) -> bool {
self.$field
}
};
{ @send, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
impl $typ {
pub async fn send(&self) -> crate::Result<$out> {
#[cfg(all(not(feature = "multi-thread"), not(feature = "tokio-multi-thread"), not(feature = "rw-multi-thread")))]
{
self.http_client.try_borrow()?.send_request(self).await
}
#[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
{
self.http_client.lock().await.send_request(self).await
}
#[cfg(feature = "rw-multi-thread")]
{
self.http_client.read().await.send_request(self).await
}
}
}
$(
builder_send! {
#[builder] $builder_ty,
$out
}
)?
};
{ @send:rate_limited, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
impl $typ {
pub async fn send(&self) -> crate::Result<crate::rate_limit::Limited<$out>> {
self.http_client.read().await.send_request_with_rate_limit(self).await
}
}
$(
builder_send! {
#[builder] $builder_ty,
#[rate_limited] $out
}
)?
};
{ @send:flatten_result, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
impl $typ {
#[allow(dead_code)]
pub async fn send(&self) -> $out {
self.http_client.read().await.send_request(self).await?
}
}
$(
builder_send! {
#[builder] $builder_ty,
#[flatten_result] $out
}
)?
};
{ @send:discard_result, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
impl $typ {
#[allow(dead_code)]
pub async fn send(&self) -> crate::Result<()> {
self.http_client.read().await.send_request(self).await??;
Ok(())
}
}
$(
builder_send! {
#[builder] $builder_ty,
#[discard_result] $out
}
)?
};
{ @send:no_send, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
$(
builder_send! {
#[builder] $builder_ty,
#[no_send] $out
}
)?
};
}
macro_rules! create_endpoint_node {
{
#[$name:ident] $sname:ident $tname:ident,
#[$args:ident] {$($arg_name:ident: $arg_ty:ty,)+},
#[$methods:ident] {$($func:ident($($farg_name:ident: $farg_ty:ty,)*) -> $output:ty;)*}
} => {
#[derive(Debug)]
pub struct $sname {
$( $arg_name: $arg_ty, )+
}
trait $tname {
$(
fn $func(&self, $( $farg_name: $farg_ty, )*) -> $output;
)*
}
impl $sname {
pub fn new($( $arg_name: $arg_ty, )+) -> Self {
Self {
$( $arg_name, )+
}
}
$(
pub fn $func(&self, $( $farg_name: $farg_ty, )*) -> $output {
<Self as $tname>::$func(&self, $( $farg_name,)*)
}
)*
}
$(
impl From<&$sname> for $arg_ty {
fn from(value: &$sname) -> Self {
value.$arg_name.clone()
}
}
)+
}
}