mangadex_api/
http_client.rs

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