mangadex_api/
http_client.rs

1#[cfg(not(feature = "multi-thread"))]
2use std::cell::RefCell;
3#[cfg(not(feature = "multi-thread"))]
4use std::rc::Rc;
5#[cfg(feature = "multi-thread")]
6use std::sync::Arc;
7
8use derive_builder::Builder;
9#[cfg(feature = "multi-thread")]
10use futures::lock::Mutex;
11use mangadex_api_schema::{Endpoint, FromResponse, UrlSerdeQS};
12use mangadex_api_types::error::Error;
13use reqwest::Client;
14use serde::de::DeserializeOwned;
15use url::Url;
16
17use crate::v5::AuthTokens;
18use crate::{Result, API_URL};
19
20#[cfg(not(feature = "multi-thread"))]
21pub type HttpClientRef = Rc<RefCell<HttpClient>>;
22#[cfg(feature = "multi-thread")]
23pub type HttpClientRef = Arc<Mutex<HttpClient>>;
24
25#[derive(Debug, Builder, Clone)]
26#[builder(setter(into, strip_option), default)]
27pub struct HttpClient {
28    pub client: Client,
29    pub base_url: Url,
30    auth_tokens: Option<AuthTokens>,
31    captcha: Option<String>,
32}
33
34impl Default for HttpClient {
35    fn default() -> Self {
36        Self {
37            client: Client::new(),
38            base_url: Url::parse(API_URL).expect("error parsing the base url"),
39            auth_tokens: None,
40            captcha: None,
41        }
42    }
43}
44
45impl HttpClient {
46    /// Create a new `HttpClient` with a custom [`reqwest::Client`](https://docs.rs/reqwest/latest/reqwest/struct.Client.html).
47    pub fn new(client: Client) -> Self {
48        Self {
49            client,
50            ..Default::default()
51        }
52    }
53
54    /// Get a builder struct to customize the `HttpClient` fields.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use url::Url;
60    ///
61    /// use mangadex_api::{MangaDexClient, HttpClient};
62    ///
63    /// # async fn run() -> anyhow::Result<()> {
64    /// let http_client = HttpClient::builder()
65    ///     .base_url(Url::parse("127.0.0.1:8000")?)
66    ///     .build()?;
67    ///
68    /// let mangadex_client = MangaDexClient::new_with_http_client(http_client);
69    /// # Ok(())
70    /// # }
71    /// ```
72    pub fn builder() -> HttpClientBuilder {
73        HttpClientBuilder::default()
74    }
75
76    /// Send the request to the endpoint but don't deserialize the response.
77    ///
78    /// This is useful to handle things such as response header data for more control over areas
79    /// such as rate limiting.
80    pub(crate) async fn send_request_without_deserializing<E>(
81        &self,
82        endpoint: &E,
83    ) -> Result<reqwest::Response>
84    where
85        E: Endpoint,
86    {
87        let mut endpoint_url = self.base_url.join(&endpoint.path())?;
88        if let Some(query) = endpoint.query() {
89            endpoint_url = endpoint_url.query_qs(query);
90        }
91
92        let mut req = self.client.request(endpoint.method(), endpoint_url);
93
94        if let Some(body) = endpoint.body() {
95            req = req.json(body);
96        }
97
98        if let Some(multipart) = endpoint.multipart() {
99            req = req.multipart(multipart);
100        }
101
102        if let Some(tokens) = self.get_tokens() {
103            req = req.bearer_auth(&tokens.session)
104        } else if endpoint.require_auth() {
105            return Err(Error::MissingTokens);
106        }
107
108        if let Some(captcha) = self.get_captcha() {
109            req = req.header("X-Captcha-Result", captcha);
110        }
111
112        Ok(req.send().await?)
113    }
114
115    /// Send the request to the endpoint and deserialize the response body.
116    pub(crate) async fn send_request<E>(&self, endpoint: &E) -> Result<E::Response>
117    where
118        E: Endpoint,
119        <<E as Endpoint>::Response as FromResponse>::Response: DeserializeOwned,
120    {
121        let res = self.send_request_without_deserializing(endpoint).await?;
122
123        let status_code = res.status();
124
125        if status_code.is_server_error() {
126            return Err(Error::ServerError(status_code.as_u16(), res.text().await?));
127        }
128
129        let res = res
130            .json::<<E::Response as FromResponse>::Response>()
131            .await?;
132
133        Ok(FromResponse::from_response(res))
134    }
135
136    /// Get the authentication tokens stored in the client.
137    pub fn get_tokens(&self) -> Option<&AuthTokens> {
138        self.auth_tokens.as_ref()
139    }
140
141    /// Set new authentication tokens into the client.
142    pub fn set_auth_tokens(&mut self, auth_tokens: &AuthTokens) {
143        self.auth_tokens = Some(auth_tokens.clone());
144    }
145
146    /// Remove all authentication tokens from the client.
147    ///
148    /// This is effectively the same as logging out, though will not remove the active session from
149    /// the MangaDex server. Be sure to call the logout endpoint to ensure your session is removed.
150    pub fn clear_auth_tokens(&mut self) {
151        self.auth_tokens = None;
152    }
153
154    /// Get the captcha solution stored in the client.
155    pub fn get_captcha(&self) -> Option<&String> {
156        self.captcha.as_ref()
157    }
158
159    /// Set a new captcha solution into the client.
160    ///
161    /// The code needed for this can be found in the "X-Captcha-Sitekey" header field,
162    /// or the `siteKey` parameter in the error context of a 403 response,
163    /// `captcha_required_exception` error code.
164    pub fn set_captcha<T: Into<String>>(&mut self, captcha: T) {
165        self.captcha = Some(captcha.into());
166    }
167
168    /// Remove the captcha solution from the client.
169    pub fn clear_captcha(&mut self) {
170        self.captcha = None;
171    }
172}
173
174/// Helper macro to quickly implement the `Endpoint` trait,
175/// and optionally a `send()` method for the input struct.
176///
177/// The arguments are ordered as follows:
178///
179/// 1. HTTP method and endpoint path.
180/// 2. Input data to serialize unless `no_data` is specified.
181/// 3. Response struct to deserialize into.
182///
183/// with the following format:
184///
185/// 1. \<HTTP Method\> "\<ENDPOINT PATH\>"
186/// 2. \#\[\<ATTRIBUTE\>\] \<INPUT STRUCT\>
187/// 3. \#\[\<OPTIONAL ATTRIBUTE\>\] \<OUTPUT STRUCT\>
188///
189/// The endpoint is specified by the HTTP method, followed by the path. To get a dynamic path
190/// based on the input structure, surround the path with parenthesis:
191///
192/// ```rust, ignore
193/// POST ("/account/activate/{}", id)
194/// ```
195///
196/// The format is the same as the `format!()` macro, except `id` will be substituted by `self.id`,
197/// where `self` represents an instance of the second parameter.
198///
199/// The input structure is preceded by an attribute-like structure.
200///
201/// - `query`: The input structure will be serialized as the query string.
202/// - `body`: The input structure will be serialized as a JSON body.
203/// - `no_data`: No data will be sent with the request.
204/// - `auth`: If this is included, the request will not be made if the user is not authenticated.
205///
206/// Some examples of valid tags are:
207///
208/// ```rust, ignore
209/// #[query] QueryReq
210/// #[body] BodyReq
211/// #[query auth] QueryReq
212/// #[no_data] QueryStruct
213/// ```
214///
215/// The input structure itself should implement `serde::Serialize` if it is used as a body or query.
216///
217/// The third argument is the output type, tagged similarly to the input, to modify the behaviour
218/// of the generated `send()` method.
219///
220/// - \<no tag\>: `send()` will simply return `Result<Output>`.
221/// - `flatten_result`: If `Output = Result<T>`, the return type will be simplified to `Result<T>`.
222/// - `discard_result`: If `Output = Result<T>`, discard `T`, and return `Result<()>`.
223/// - `no_send`: Do not implement a `send()` function.
224///
225/// # Examples
226///
227/// ```rust, ignore
228/// endpoint! {
229///     GET "/path/to/endpoint", // Endpoint.
230///     #[query] StructWithData<'_>, // Input data; this example will be serialized as a query string.
231///     #[flatten_result] Result<ResponseType> // Response struct; this example will return `Ok(res)` or `Err(e)` instead of `Result<ResponseType>` because of `#[flatten_result]`.
232/// }
233/// ```
234macro_rules! endpoint {
235    {
236        $method:ident $path:tt,
237        #[$payload:ident $($auth:ident)?] $typ:ty,
238        $(#[$out_res:ident])? $out:ty
239    } => {
240        impl mangadex_api_schema::Endpoint for $typ {
241            /// The response type.
242            type Response = $out;
243
244            /// Get the method of the request.
245            fn method(&self) -> reqwest::Method {
246                reqwest::Method::$method
247            }
248
249            endpoint! { @path $path }
250            endpoint! { @payload $payload }
251            // If the `auth` attribute is set, make the request require authentication.
252            $(endpoint! { @$auth })?
253        }
254
255        endpoint! { @send $(:$out_res)?, $typ, $out }
256    };
257
258    { @path ($path:expr, $($arg:ident),+) } => {
259        /// Get the path of the request.
260        fn path(&self) -> std::borrow::Cow<str> {
261            std::borrow::Cow::Owned(format!($path, $(self.$arg),+))
262        }
263    };
264    { @path $path:expr } => {
265        /// Get the path of the request.
266        fn path(&self) -> std::borrow::Cow<str> {
267            std::borrow::Cow::Borrowed($path)
268        }
269    };
270
271    // Set a query string.
272    { @payload query } => {
273        type Query = Self;
274        type Body = ();
275
276        /// Get the query of the request.
277        fn query(&self) -> Option<&Self::Query> {
278            Some(&self)
279        }
280    };
281    // Set a JSON body.
282    { @payload body } => {
283        type Query = ();
284        type Body = Self;
285
286        /// Get the body of the request.
287        fn body(&self) -> Option<&Self::Body> {
288            Some(&self)
289        }
290    };
291    // Don't send any additional data with the request.
292    { @payload no_data } => {
293        type Query = ();
294        type Body = ();
295    };
296
297    { @auth } => {
298        /// Get whether auth is required for this request.
299        fn require_auth(&self) -> bool {
300            true
301        }
302    };
303
304    // Return the response as a `Result`.
305    { @send, $typ:ty, $out:ty } => {
306        impl $typ {
307            /// Send the request.
308            pub async fn send(&self) -> $crate::Result<$out> {
309                #[cfg(not(feature = "multi-thread"))]
310                {
311                    self.http_client.borrow().send_request(self).await
312                }
313                #[cfg(feature = "multi-thread")]
314                {
315                    self.http_client.lock().await.send_request(self).await
316                }
317            }
318        }
319    };
320    // Return the `Result` variants, `Ok` or `Err`.
321    { @send:flatten_result, $typ:ty, $out:ty } => {
322        impl $typ {
323            /// Send the request.
324            pub async fn send(&self) -> $out {
325                #[cfg(not(feature = "multi-thread"))]
326                {
327                    self.http_client.borrow().send_request(self).await?
328                }
329                #[cfg(feature = "multi-thread")]
330                {
331                    self.http_client.lock().await.send_request(self).await?
332                }
333            }
334        }
335    };
336    // Don't return any data from the response.
337    { @send:discard_result, $typ:ty, $out:ty } => {
338        impl $typ {
339            /// Send the request.
340            pub async fn send(&self) -> $crate::Result<()> {
341                #[cfg(not(feature = "multi-thread"))]
342                self.http_client.borrow().send_request(self).await??;
343                #[cfg(feature = "multi-thread")]
344                self.http_client.lock().await.send_request(self).await??;
345
346                Ok(())
347            }
348        }
349    };
350    // Don't implement `send()` and require manual implementation.
351    { @send:no_send, $typ:ty, $out:ty } => { };
352}