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}