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 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 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 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 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 Ok(res.json::<ApiResult<T>>().await?.into_result()?)
237 }
238
239 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 #[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 #[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 pub fn get_tokens(&self) -> Option<&AuthTokens> {
312 self.auth_tokens.as_ref()
313 }
314
315 pub fn set_auth_tokens(&mut self, auth_tokens: &AuthTokens) {
317 self.auth_tokens = Some(auth_tokens.clone());
318 }
319
320 pub fn clear_auth_tokens(&mut self) {
325 self.auth_tokens = None;
326 }
327
328 pub fn get_captcha(&self) -> Option<&String> {
330 self.captcha.as_ref()
331 }
332
333 pub fn set_captcha<T: Into<String>>(&mut self, captcha: T) {
339 self.captcha = Some(captcha.into());
340 }
341
342 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 #[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
385macro_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
436macro_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 type Response = $out;
507
508 fn method(&self) -> reqwest::Method {
510 reqwest::Method::$method
511 }
512
513 endpoint! { @path $path }
514 endpoint! { @payload $payload }
515 $(endpoint! { @$auth })?
517 }
518
519 endpoint! { @send $(:$out_res)?, $typ, $out $(,$builder_ty)? }
520
521 };
522
523 { @path ($path:expr, $($arg:ident),+) } => {
524 fn path(&self) -> std::borrow::Cow<str> {
526 std::borrow::Cow::Owned(format!($path, $(self.$arg),+))
527 }
528 };
529 { @path $path:expr } => {
530 fn path(&self) -> std::borrow::Cow<str> {
532 std::borrow::Cow::Borrowed($path)
533 }
534 };
535
536 { @payload query } => {
538 type Query = Self;
539 type Body = ();
540
541 fn query(&self) -> Option<&Self::Query> {
543 Some(&self)
544 }
545 };
546 { @payload body } => {
548 type Query = ();
549 type Body = Self;
550
551 fn body(&self) -> Option<&Self::Body> {
553 Some(&self)
554 }
555 };
556 { @payload no_data } => {
558 type Query = ();
559 type Body = ();
560 };
561
562 { @auth } => {
563 fn require_auth(&self) -> bool {
565 true
566 }
567 };
568
569 { @send, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
571 impl $typ {
572 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 { @send:rate_limited, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
598 impl $typ {
599 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 { @send:flatten_result, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
625 impl $typ {
626 #[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 { @send:discard_result, $typ:ty, $out:ty $(,$builder_ty:ty)? } => {
653 impl $typ {
654 #[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 { @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}