mangadex_api/
http_client.rs

1#[cfg(all(
2    not(feature = "multi-thread"),
3    not(feature = "tokio-multi-thread"),
4    not(feature = "rw-multi-thread")
5))]
6use std::cell::RefCell;
7#[cfg(all(
8    not(feature = "multi-thread"),
9    not(feature = "tokio-multi-thread"),
10    not(feature = "rw-multi-thread")
11))]
12use std::rc::Rc;
13#[cfg(any(
14    feature = "multi-thread",
15    feature = "tokio-multi-thread",
16    feature = "rw-multi-thread"
17))]
18use std::sync::Arc;
19
20use derive_builder::Builder;
21#[cfg(all(feature = "multi-thread", not(feature = "tokio-multi-thread")))]
22use futures::lock::Mutex;
23use mangadex_api_schema::v5::oauth::ClientInfo;
24use mangadex_api_schema::{ApiResult, Endpoint, FromResponse, Limited, UrlSerdeQS};
25use mangadex_api_types::error::Error;
26use reqwest::{Client, Response};
27use serde::de::DeserializeOwned;
28#[cfg(feature = "tokio-multi-thread")]
29use tokio::sync::Mutex;
30#[cfg(feature = "rw-multi-thread")]
31use tokio::sync::RwLock;
32use url::Url;
33
34use crate::v5::AuthTokens;
35use crate::{API_DEV_URL, API_URL};
36use mangadex_api_types::error::Result;
37
38#[cfg(all(
39    not(feature = "multi-thread"),
40    not(feature = "tokio-multi-thread"),
41    not(feature = "rw-multi-thread")
42))]
43#[cfg_attr(
44    docsrs,
45    doc(cfg(all(
46        not(feature = "multi-thread"),
47        not(feature = "tokio-multi-thread"),
48        not(feature = "rw-multi-thread")
49    )))
50)]
51pub type HttpClientRef = Rc<RefCell<HttpClient>>;
52#[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
53#[cfg_attr(
54    docsrs,
55    doc(cfg(any(feature = "multi-thread", feature = "tokio-multi-thread")))
56)]
57pub type HttpClientRef = Arc<Mutex<HttpClient>>;
58#[cfg(feature = "rw-multi-thread")]
59#[cfg_attr(docsrs, doc(cfg(feature = "rw-multi-thread")))]
60pub type HttpClientRef = Arc<RwLock<HttpClient>>;
61
62#[derive(Debug, Builder, Clone)]
63#[builder(
64    setter(into, strip_option),
65    default,
66    build_fn(error = "mangadex_api_types::error::BuilderError")
67)]
68#[cfg(not(feature = "oauth"))]
69#[cfg_attr(docsrs, doc(cfg(not(feature = "oauth"))))]
70pub struct HttpClient {
71    pub client: Client,
72    pub base_url: Url,
73    auth_tokens: Option<AuthTokens>,
74    captcha: Option<String>,
75}
76
77#[derive(Debug, Builder, Clone)]
78#[builder(
79    setter(into, strip_option),
80    default,
81    build_fn(error = "mangadex_api_types::error::BuilderError")
82)]
83#[cfg(feature = "oauth")]
84#[cfg_attr(docsrs, doc(cfg(feature = "oauth")))]
85pub struct HttpClient {
86    pub client: Client,
87    pub base_url: Url,
88    auth_tokens: Option<AuthTokens>,
89    captcha: Option<String>,
90    client_info: Option<ClientInfo>,
91}
92
93#[cfg(feature = "oauth")]
94impl Default for HttpClient {
95    fn default() -> Self {
96        Self {
97            client: crate::get_default_client_api(),
98            base_url: Url::parse(API_URL).expect("error parsing the base url"),
99            auth_tokens: None,
100            captcha: None,
101            client_info: None,
102        }
103    }
104}
105
106#[cfg(not(feature = "oauth"))]
107impl Default for HttpClient {
108    fn default() -> Self {
109        Self {
110            client: crate::get_default_client_api(),
111            base_url: Url::parse(API_URL).expect("error parsing the base url"),
112            auth_tokens: None,
113            captcha: None,
114        }
115    }
116}
117
118impl HttpClient {
119    /// Create a new `HttpClient` with a custom [`reqwest::Client`](https://docs.rs/reqwest/latest/reqwest/struct.Client.html).
120    pub fn new(client: Client) -> Self {
121        Self {
122            client,
123            base_url: Url::parse(API_URL).expect("error parsing the base url"),
124            ..Default::default()
125        }
126    }
127
128    /// Get a builder struct to customize the `HttpClient` fields.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use url::Url;
134    ///
135    /// use mangadex_api::{MangaDexClient, HttpClient};
136    ///
137    /// # async fn run() -> anyhow::Result<()> {
138    /// let http_client = HttpClient::builder()
139    ///     .base_url(Url::parse("127.0.0.1:8000")?)
140    ///     .build()?;
141    ///
142    /// let mangadex_client = MangaDexClient::new_with_http_client(http_client);
143    /// # Ok(())
144    /// # }
145    /// ```
146    pub fn builder() -> HttpClientBuilder {
147        HttpClientBuilder::default()
148            .client(crate::get_default_client_api())
149            .base_url(Url::parse(API_URL).expect("error parsing the base url"))
150            .clone()
151    }
152
153    /// Send the request to the endpoint but don't deserialize the response.
154    ///
155    /// This is useful to handle things such as response header data for more control over areas
156    /// such as rate limiting.
157    pub(crate) async fn send_request_without_deserializing_with_other_base_url<E>(
158        &self,
159        endpoint: &E,
160        base_url: &url::Url,
161    ) -> Result<reqwest::Response>
162    where
163        E: Endpoint,
164    {
165        let mut endpoint_url = base_url.join(&endpoint.path())?;
166        if let Some(query) = endpoint.query() {
167            endpoint_url = endpoint_url.query_qs(query);
168        }
169
170        let mut req = self.client.request(endpoint.method(), endpoint_url);
171
172        if let Some(body) = endpoint.body() {
173            req = req.json(body);
174        }
175
176        if let Some(multipart) = endpoint.multipart() {
177            req = req.multipart(multipart);
178        }
179        if endpoint.require_auth() {
180            let tokens = self.get_tokens().ok_or(Error::MissingTokens)?;
181            req = req.bearer_auth(&tokens.session);
182        }
183        if let Some(captcha) = self.get_captcha() {
184            req = req.header("X-Captcha-Result", captcha);
185        }
186
187        Ok(req.send().await?)
188    }
189
190    /// Send the request to the endpoint but don't deserialize the response.
191    ///
192    /// This is useful to handle things such as response header data for more control over areas
193    /// such as rate limiting.
194    pub(crate) async fn send_request_without_deserializing<E>(
195        &self,
196        endpoint: &E,
197    ) -> Result<reqwest::Response>
198    where
199        E: Endpoint,
200    {
201        self.send_request_without_deserializing_with_other_base_url(endpoint, &self.base_url)
202            .await
203    }
204
205    pub(crate) async fn send_request_with_checks<E>(
206        &self,
207        endpoint: &E,
208    ) -> Result<reqwest::Response>
209    where
210        E: Endpoint,
211    {
212        let res = self.send_request_without_deserializing(endpoint).await?;
213
214        let status_code = res.status();
215
216        if status_code.as_u16() == 429 {
217            return Err(Error::RateLimitExcedeed);
218        }
219
220        if status_code.is_server_error() {
221            return Err(Error::ServerError(status_code.as_u16(), res.text().await?));
222        }
223        Ok(res)
224    }
225
226    pub(crate) async fn handle_result<T>(&self, res: Response) -> Result<T>
227    where
228        T: DeserializeOwned,
229    {
230        /*let res_text = res.text().await?;
231        eprintln!("{}", res_text);
232        Ok(serde_json::from_str::<ApiResult<T>>(&res_text)
233        .map_err(|e| Error::UnexpectedError(anyhow::Error::msg(e.to_string())))?
234        .into_result()?)
235        */
236        Ok(res.json::<ApiResult<T>>().await?.into_result()?)
237    }
238
239    /// Send the request to the endpoint and deserialize the response body.
240    pub(crate) async fn send_request<E>(&self, endpoint: &E) -> Result<E::Response>
241    where
242        E: Endpoint,
243        <<E as Endpoint>::Response as FromResponse>::Response: DeserializeOwned,
244    {
245        let res = self.send_request_with_checks(endpoint).await?;
246
247        let res = res
248            .json::<<E::Response as FromResponse>::Response>()
249            .await?;
250
251        Ok(FromResponse::from_response(res))
252    }
253
254    /// Send the request to the endpoint and deserialize the response body.
255    #[cfg(not(feature = "serialize"))]
256    #[cfg_attr(docsrs, doc(cfg(not(feature = "serialize"))))]
257    pub(crate) async fn send_request_with_rate_limit<E>(
258        &self,
259        endpoint: &E,
260    ) -> Result<Limited<E::Response>>
261    where
262        E: Endpoint,
263        <<E as Endpoint>::Response as FromResponse>::Response: DeserializeOwned,
264        <E as mangadex_api_schema::Endpoint>::Response: Clone,
265    {
266        use mangadex_api_types::rate_limit::RateLimit;
267
268        let resp = self.send_request_with_checks(endpoint).await?;
269
270        let some_rate_limit = <RateLimit as TryFrom<&Response>>::try_from(&resp);
271
272        let res = self
273            .handle_result::<<E::Response as FromResponse>::Response>(resp)
274            .await?;
275
276        Ok(Limited {
277            rate_limit: some_rate_limit?,
278            body: FromResponse::from_response(res),
279        })
280    }
281
282    /// Send the request to the endpoint and deserialize the response body.
283    #[cfg(feature = "serialize")]
284    #[cfg_attr(docsrs, doc(cfg(feature = "serialize")))]
285    pub(crate) async fn send_request_with_rate_limit<E>(
286        &self,
287        endpoint: &E,
288    ) -> Result<Limited<E::Response>>
289    where
290        E: Endpoint,
291        <<E as Endpoint>::Response as FromResponse>::Response: DeserializeOwned,
292        <E as mangadex_api_schema::Endpoint>::Response: serde::Serialize + Clone,
293    {
294        use mangadex_api_types::rate_limit::RateLimit;
295
296        let resp = self.send_request_with_checks(endpoint).await?;
297
298        let rate_limit: RateLimit = TryFrom::try_from(&resp)?;
299
300        let res = self
301            .handle_result::<<E::Response as FromResponse>::Response>(resp)
302            .await?;
303
304        Ok(Limited {
305            rate_limit,
306            body: FromResponse::from_response(res),
307        })
308    }
309
310    /// Get the authentication tokens stored in the client.
311    pub fn get_tokens(&self) -> Option<&AuthTokens> {
312        self.auth_tokens.as_ref()
313    }
314
315    /// Set new authentication tokens into the client.
316    pub fn set_auth_tokens(&mut self, auth_tokens: &AuthTokens) {
317        self.auth_tokens = Some(auth_tokens.clone());
318    }
319
320    /// Remove all authentication tokens from the client.
321    ///
322    /// This is effectively the same as logging out, though will not remove the active session from
323    /// the MangaDex server. Be sure to call the logout endpoint to ensure your session is removed.
324    pub fn clear_auth_tokens(&mut self) {
325        self.auth_tokens = None;
326    }
327
328    /// Get the captcha solution stored in the client.
329    pub fn get_captcha(&self) -> Option<&String> {
330        self.captcha.as_ref()
331    }
332
333    /// Set a new captcha solution into the client.
334    ///
335    /// The code needed for this can be found in the "X-Captcha-Sitekey" header field,
336    /// or the `siteKey` parameter in the error context of a 403 response,
337    /// `captcha_required_exception` error code.
338    pub fn set_captcha<T: Into<String>>(&mut self, captcha: T) {
339        self.captcha = Some(captcha.into());
340    }
341
342    /// Remove the captcha solution from the client.
343    pub fn clear_captcha(&mut self) {
344        self.captcha = None;
345    }
346
347    #[cfg(feature = "oauth")]
348    pub fn set_client_info(&mut self, client_info: &ClientInfo) {
349        self.client_info = Some(client_info.clone());
350    }
351
352    #[cfg(feature = "oauth")]
353    pub fn get_client_info(&self) -> Option<&ClientInfo> {
354        self.client_info.as_ref()
355    }
356
357    #[cfg(feature = "oauth")]
358    #[cfg_attr(docsrs, doc(cfg(feature = "oauth")))]
359    pub fn clear_client_info(&mut self) {
360        self.client_info = None;
361    }
362
363    /// Create a new client of api.mangadex.dev
364    #[cfg(not(feature = "oauth"))]
365    pub fn api_dev_client() -> Self {
366        Self {
367            client: Client::new(),
368            base_url: Url::parse(API_DEV_URL).expect("error parsing the base url"),
369            auth_tokens: None,
370            captcha: None,
371        }
372    }
373    #[cfg(feature = "oauth")]
374    pub fn api_dev_client() -> Self {
375        Self {
376            client: Client::new(),
377            base_url: Url::parse(API_DEV_URL).expect("error parsing the base url"),
378            auth_tokens: None,
379            captcha: None,
380            client_info: None,
381        }
382    }
383}
384
385/// Helper macros for implementing the send function on the builder
386///
387/// Introduced in v3.0.0-alpha.1
388///
389///
390macro_rules! builder_send {
391    {
392        #[$builder:ident] $typ:ty,
393        $(#[$out_res:ident])? $out_type:ty
394    } => {
395        builder_send! { @send $(:$out_res)?, $typ, $out_type }
396    };
397    { @send, $typ:ty, $out_type:ty } => {
398        impl $typ {
399            pub async fn send(&self) -> mangadex_api_types::error::Result<$out_type>{
400                self.build()?.send().await
401            }
402        }
403    };
404    { @send:discard_result, $typ:ty, $out_type:ty } => {
405        impl $typ {
406            pub async fn send(&self) -> mangadex_api_types::error::Result<()>{
407                self.build()?.send().await?;
408                Ok(())
409            }
410        }
411    };
412    { @send:flatten_result, $typ:ty, $out_type:ty } => {
413        impl $typ {
414            pub async fn send(&self) -> $out_type{
415                self.build()?.send().await
416            }
417        }
418    };
419    { @send:rate_limited, $typ:ty, $out_type:ty } => {
420        impl $typ {
421
422            pub async fn send(&self) -> mangadex_api_types::error::Result<mangadex_api_schema::Limited<$out_type>>{
423                self.build()?.send().await
424            }
425        }
426    };
427    { @send:no_send, $typ:ty, $out_type:ty } => {
428        impl $typ {
429            pub async fn send(&self) -> $out_type{
430                self.build()?.send().await
431            }
432        }
433    };
434}
435
436/// Helper macro to quickly implement the `Endpoint` trait,
437/// and optionally a `send()` method for the input struct.
438///
439/// The arguments are ordered as follows:
440///
441/// 1. HTTP method and endpoint path.
442/// 2. Input data to serialize unless `no_data` is specified.
443/// 3. Response struct to deserialize into.
444///
445/// with the following format:
446///
447/// 1. \<HTTP Method\> "\<ENDPOINT PATH\>"
448/// 2. \#\[\<ATTRIBUTE\>\] \<INPUT STRUCT\>
449/// 3. \#\[\<OPTIONAL ATTRIBUTE\>\] \<OUTPUT STRUCT\>
450///
451/// The endpoint is specified by the HTTP method, followed by the path. To get a dynamic path
452/// based on the input structure, surround the path with parenthesis:
453///
454/// ```rust, ignore
455/// POST ("/account/activate/{}", id)
456/// ```
457///
458/// The format is the same as the `format!()` macro, except `id` will be substituted by `self.id`,
459/// where `self` represents an instance of the second parameter.
460///
461/// The input structure is preceded by an attribute-like structure.
462///
463/// - `query`: The input structure will be serialized as the query string.
464/// - `body`: The input structure will be serialized as a JSON body.
465/// - `no_data`: No data will be sent with the request.
466/// - `auth`: If this is included, the request will not be made if the user is not authenticated.
467///
468/// Some examples of valid tags are:
469///
470/// ```rust, ignore
471/// #[query] QueryReq
472/// #[body] BodyReq
473/// #[query auth] QueryReq
474/// #[no_data] QueryStruct
475/// ```
476///
477/// The input structure itself should implement `serde::Serialize` if it is used as a body or query.
478///
479/// The third argument is the output type, tagged similarly to the input, to modify the behaviour
480/// of the generated `send()` method.
481///
482/// - \<no tag\>: `send()` will simply return `Result<Output>`.
483/// - `flatten_result`: If `Output = Result<T>`, the return type will be simplified to `Result<T>`.
484/// - `discard_result`: If `Output = Result<T>`, discard `T`, and return `Result<()>`.
485/// - `no_send`: Do not implement a `send()` function.
486/// - `rate_limited`: `send()` will return `Result<Limited<Output>>`
487///
488/// # Examples
489///
490/// ```rust, ignore
491/// endpoint! {
492///     GET "/path/to/endpoint", // Endpoint.
493///     #[query] StructWithData<'_>, // Input data; this example will be serialized as a query string.
494///     #[flatten_result] Result<ResponseType> // Response struct; this example will return `Ok(res)` or `Err(e)` instead of `Result<ResponseType>` because of `#[flatten_result]`.
495/// }
496/// ```
497macro_rules! endpoint {
498    {
499        $method:ident $path:tt,
500        #[$payload:ident $($auth:ident)?] $typ:ty,
501        $(#[$out_res:ident])? $out:ty
502        $(,$builder_ty:ty)?
503    } => {
504        impl mangadex_api_schema::Endpoint for $typ {
505            /// The response type.
506            type Response = $out;
507
508            /// Get the method of the request.
509            fn method(&self) -> reqwest::Method {
510                reqwest::Method::$method
511            }
512
513            endpoint! { @path $path }
514            endpoint! { @payload $payload }
515            // If the `auth` attribute is set, make the request require authentication.
516            $(endpoint! { @$auth })?
517        }
518
519        endpoint! { @send $(:$out_res)?, $typ, $out $(,$builder_ty)? }
520
521    };
522
523    { @path ($path:expr, $($arg:ident),+) } => {
524        /// Get the path of the request.
525        fn path(&self) -> std::borrow::Cow<str> {
526            std::borrow::Cow::Owned(format!($path, $(self.$arg),+))
527        }
528    };
529    { @path $path:expr } => {
530        /// Get the path of the request.
531        fn path(&self) -> std::borrow::Cow<str> {
532            std::borrow::Cow::Borrowed($path)
533        }
534    };
535
536    // Set a query string.
537    { @payload query } => {
538        type Query = Self;
539        type Body = ();
540
541        /// Get the query of the request.
542        fn query(&self) -> Option<&Self::Query> {
543            Some(&self)
544        }
545    };
546    // Set a JSON body.
547    { @payload body } => {
548        type Query = ();
549        type Body = Self;
550
551        /// Get the body of the request.
552        fn body(&self) -> Option<&Self::Body> {
553            Some(&self)
554        }
555    };
556    // Don't send any additional data with the request.
557    { @payload no_data } => {
558        type Query = ();
559        type Body = ();
560    };
561
562    { @auth } => {
563        /// Get whether auth is required for this request.
564        fn require_auth(&self) -> bool {
565            true
566        }
567    };
568
569    // Return the response as a `Result`.
570    { @send, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
571        impl $typ {
572            /// Send the request.
573            pub async fn send(&self) -> mangadex_api_types::error::Result<$out> {
574                #[cfg(all(not(feature = "multi-thread"), not(feature = "tokio-multi-thread"), not(feature = "rw-multi-thread")))]
575                {
576                    self.http_client.try_borrow()?.send_request(self).await
577                }
578                #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
579                {
580                    self.http_client.lock().await.send_request(self).await
581                }
582                #[cfg(feature = "rw-multi-thread")]
583                {
584                    self.http_client.read().await.send_request(self).await
585                }
586            }
587        }
588
589        $(
590            builder_send! {
591                #[builder] $builder_ty,
592                $out
593            }
594        )?
595    };
596    // Return the response as a `Result`.
597    { @send:rate_limited, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
598        impl $typ {
599            /// Send the request.
600            pub async fn send(&self) -> mangadex_api_types::error::Result<mangadex_api_schema::Limited<$out>> {
601                #[cfg(all(not(feature = "multi-thread"), not(feature = "tokio-multi-thread"), not(feature = "rw-multi-thread")))]
602                {
603                    self.http_client.try_borrow()?.send_request_with_rate_limit(self).await
604                }
605                #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
606                {
607                    self.http_client.lock().await.send_request_with_rate_limit(self).await
608                }
609                #[cfg(feature = "rw-multi-thread")]
610                {
611                    self.http_client.read().await.send_request_with_rate_limit(self).await
612                }
613            }
614        }
615
616        $(
617            builder_send! {
618                #[builder] $builder_ty,
619                #[rate_limited] $out
620            }
621        )?
622    };
623    // Return the `Result` variants, `Ok` or `Err`.
624    { @send:flatten_result, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
625        impl $typ {
626            /// Send the request.
627            #[allow(dead_code)]
628            pub async fn send(&self) -> $out {
629                #[cfg(all(not(feature = "multi-thread"), not(feature = "tokio-multi-thread"), not(feature = "rw-multi-thread")))]
630                {
631                    self.http_client.try_borrow()?.send_request(self).await?
632                }
633                #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
634                {
635                    self.http_client.lock().await.send_request(self).await?
636                }
637                #[cfg(feature = "rw-multi-thread")]
638                {
639                    self.http_client.read().await.send_request(self).await?
640                }
641            }
642        }
643
644        $(
645            builder_send! {
646                #[builder] $builder_ty,
647                #[flatten_result] $out
648            }
649        )?
650    };
651    // Don't return any data from the response.
652    { @send:discard_result, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
653        impl $typ {
654            /// Send the request.
655            #[allow(dead_code)]
656            pub async fn send(&self) -> mangadex_api_types::error::Result<()> {
657                #[cfg(all(not(feature = "multi-thread"), not(feature = "tokio-multi-thread"), not(feature = "rw-multi-thread")))]
658                self.http_client.try_borrow()?.send_request(self).await??;
659                #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
660                self.http_client.lock().await.send_request(self).await??;
661                #[cfg(feature = "rw-multi-thread")]
662                self.http_client.read().await.send_request(self).await??;
663
664                Ok(())
665            }
666        }
667
668        $(
669            builder_send! {
670                #[builder] $builder_ty,
671                #[discard_result] $out
672            }
673        )?
674    };
675    // Don't implement `send()` and require manual implementation.
676    { @send:no_send, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
677        $(
678            builder_send! {
679                #[builder] $builder_ty,
680                #[no_send] $out
681            }
682        )?
683    };
684
685}
686
687macro_rules! create_endpoint_node {
688    {
689        #[$name:ident] $sname:ident $tname:ident,
690        #[$args:ident] {$($arg_name:ident: $arg_ty:ty,)+},
691        #[$methods:ident] {$($func:ident($($farg_name:ident: $farg_ty:ty,)*) -> $output:ty;)*}
692    } => {
693        #[derive(Debug)]
694        pub struct $sname {
695            $( $arg_name: $arg_ty, )+
696        }
697        trait $tname {
698            $(
699                fn $func(&self, $( $farg_name: $farg_ty, )*) -> $output;
700            )*
701        }
702        impl $sname {
703            pub fn new($( $arg_name: $arg_ty, )+) -> Self {
704                Self {
705                    $( $arg_name, )+
706                }
707            }
708            $(
709                pub fn $func(&self, $( $farg_name: $farg_ty, )*) -> $output {
710                    <Self as $tname>::$func(&self, $( $farg_name,)*)
711                }
712            )*
713        }
714        $(
715            impl From<&$sname> for $arg_ty {
716                fn from(value: &$sname) -> Self {
717                    value.$arg_name.clone()
718                }
719            }
720        )+
721    }
722}