Skip to main content

mal_api/manga/
api.rs

1use super::{error::MangaApiError, requests::GetUserMangaList, responses::MangaListStatus};
2use async_trait::async_trait;
3use oauth2::{AccessToken, ClientId};
4use serde::{de::DeserializeOwned, Serialize};
5use std::marker::PhantomData;
6
7use crate::{
8    common::{struct_to_form_data, PagingIter},
9    manga::requests::{DeleteMyMangaListItem, UpdateMyMangaListStatus},
10    oauth::{Authenticated, MalClientId, OauthClient},
11    MANGA_URL, USER_URL,
12};
13
14use super::{
15    requests::{GetMangaDetails, GetMangaList, GetMangaRanking},
16    responses::{MangaDetails, MangaList, MangaRanking},
17};
18use reqwest;
19
20#[doc(hidden)]
21#[derive(Debug)]
22pub struct Client {}
23
24#[doc(hidden)]
25#[derive(Debug)]
26pub struct Oauth {}
27
28#[doc(hidden)]
29#[derive(Debug)]
30pub struct None {}
31
32/// The MangaApiClient provides functions for interacting with the various
33/// `manga` and `user mangalist` MAL API endpoints. The accessible endpoints
34/// vary depending on if the MangaApiClient was constructed from a
35/// [MalClientId] or an [OauthClient].
36///
37/// Keep in mind that constructing a MangaApiClient from an [OauthClient] provides
38/// more access to the MAL API than from a [MalClientId]. Check the MAL API documentation
39/// to view which endpoints require an [OauthClient] versus a [MalClientId] to see which
40/// one is most appropriate for your use case.
41///
42/// # Example
43///
44/// ```rust,ignore
45/// use dotenvy;
46/// use mal_api::oauth::MalClientId;
47/// use mal_api::prelude::*;
48///
49/// #[tokio::main]
50/// async fn main() {
51///     dotenvy::dotenv().ok();
52///
53///     let client_id = MalClientId::from_env().unwrap();
54///     let api_client = MangaApiClient::from(&client_id);
55///     let common_fields = mal_api::manga::all_common_fields();
56///     let detail_fields = mal_api::manga::all_detail_fields();
57///
58///     let query = GetMangaList::builder("one")
59///         .fields(&common_fields)
60///         .limit(3)
61///         .build()
62///         .unwrap();
63///     let response = api_client.get_manga_list(&query).await;
64///     if let Ok(response) = response {
65///         println!("Response: {}\n", response);
66///     }
67///
68///     let query = GetMangaDetails::builder(44347)
69///         .fields(&detail_fields)
70///         .build()
71///         .unwrap();
72///     let response = api_client.get_manga_details(&query).await;
73///     if let Ok(response) = response {
74///         println!("Response: {}\n", response);
75///     }
76///
77///     let query = GetMangaRanking::builder(MangaRankingType::All)
78///         .enable_nsfw()
79///         .fields(&common_fields)
80///         .limit(10)
81///         .build()
82///         .unwrap();
83///     let response = api_client.get_manga_ranking(&query).await;
84///     if let Ok(response) = response {
85///         println!("Response: {}\n", response);
86///     }
87/// }
88/// ```
89
90#[derive(Debug, Clone)]
91pub struct MangaApiClient<State = None> {
92    client: reqwest::Client,
93    client_id: Option<String>,
94    access_token: Option<String>,
95    state: PhantomData<State>,
96}
97
98impl From<&AccessToken> for MangaApiClient<Oauth> {
99    fn from(value: &AccessToken) -> Self {
100        MangaApiClient::<Oauth> {
101            client: reqwest::Client::new(),
102            client_id: None,
103            access_token: Some(value.secret().clone()),
104            state: PhantomData::<Oauth>,
105        }
106    }
107}
108
109impl From<&ClientId> for MangaApiClient<Client> {
110    fn from(value: &ClientId) -> Self {
111        MangaApiClient::<Client> {
112            client: reqwest::Client::new(),
113            client_id: Some(value.clone().to_string()),
114            access_token: None,
115            state: PhantomData::<Client>,
116        }
117    }
118}
119
120impl From<&MalClientId> for MangaApiClient<Client> {
121    fn from(value: &MalClientId) -> Self {
122        MangaApiClient::<Client> {
123            client: reqwest::Client::new(),
124            client_id: Some(value.0.to_string()),
125            access_token: None,
126            state: PhantomData::<Client>,
127        }
128    }
129}
130
131impl From<&OauthClient<Authenticated>> for MangaApiClient<Oauth> {
132    fn from(value: &OauthClient<Authenticated>) -> Self {
133        MangaApiClient {
134            client: reqwest::Client::new(),
135            client_id: None,
136            access_token: Some(value.get_access_token().secret().clone()),
137            state: PhantomData::<Oauth>,
138        }
139    }
140}
141
142/// This trait defines the common request methods available to both
143/// Client and Oauth MangaApiClients
144#[async_trait]
145pub trait Request {
146    async fn get<T>(&self, query: &T) -> Result<String, MangaApiError>
147    where
148        T: Serialize + Send + Sync;
149
150    async fn get_details(&self, query: &GetMangaDetails) -> Result<String, MangaApiError>;
151
152    async fn get_ranking(&self, query: &GetMangaRanking) -> Result<String, MangaApiError>;
153
154    async fn get_user(&self, query: &GetUserMangaList) -> Result<String, MangaApiError>;
155
156    async fn get_next_or_prev(&self, query: Option<&String>) -> Result<String, MangaApiError>;
157}
158
159#[async_trait]
160impl Request for MangaApiClient<Client> {
161    async fn get<T>(&self, query: &T) -> Result<String, MangaApiError>
162    where
163        T: Serialize + Send + Sync,
164    {
165        let response = self
166            .client
167            .get(MANGA_URL)
168            .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
169            .query(&query)
170            .send()
171            .await
172            .map_err(|err| MangaApiError::new(format!("Failed get request: {}", err)))?;
173
174        handle_response(response).await
175    }
176
177    async fn get_details(&self, query: &GetMangaDetails) -> Result<String, MangaApiError> {
178        let response = self
179            .client
180            .get(format!("{}/{}", MANGA_URL, query.manga_id))
181            .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
182            .query(&query)
183            .send()
184            .await
185            .map_err(|err| MangaApiError::new(format!("Failed get request: {}", err)))?;
186
187        handle_response(response).await
188    }
189
190    async fn get_ranking(&self, query: &GetMangaRanking) -> Result<String, MangaApiError> {
191        let response = self
192            .client
193            .get(format!("{}/ranking", MANGA_URL))
194            .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
195            .query(&query)
196            .send()
197            .await
198            .map_err(|err| MangaApiError::new(format!("Failed get request: {}", err)))?;
199
200        handle_response(response).await
201    }
202
203    async fn get_user(&self, query: &GetUserMangaList) -> Result<String, MangaApiError> {
204        let response = self
205            .client
206            .get(format!("{}/{}/mangalist", USER_URL, query.user_name))
207            .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
208            .query(&query)
209            .send()
210            .await
211            .map_err(|err| MangaApiError::new(format!("Failed get request: {}", err)))?;
212
213        handle_response(response).await
214    }
215
216    async fn get_next_or_prev(&self, query: Option<&String>) -> Result<String, MangaApiError> {
217        if let Some(itr) = query {
218            let response = self
219                .client
220                .get(itr)
221                .header("X-MAL-CLIENT-ID", self.client_id.as_ref().unwrap())
222                .send()
223                .await
224                .map_err(|err| MangaApiError::new(format!("Failed get request: {}", err)))?;
225
226            handle_response(response).await
227        } else {
228            Err(MangaApiError::new("Page does not exist".to_string()))
229        }
230    }
231}
232
233#[async_trait]
234impl Request for MangaApiClient<Oauth> {
235    async fn get<T>(&self, query: &T) -> Result<String, MangaApiError>
236    where
237        T: Serialize + std::marker::Send + std::marker::Sync,
238    {
239        let response = self
240            .client
241            .get(MANGA_URL)
242            .bearer_auth(self.access_token.as_ref().unwrap())
243            .query(&query)
244            .send()
245            .await
246            .map_err(|err| MangaApiError::new(format!("Failed get request: {}", err)))?;
247
248        handle_response(response).await
249    }
250
251    async fn get_details(&self, query: &GetMangaDetails) -> Result<String, MangaApiError> {
252        let response = self
253            .client
254            .get(format!("{}/{}", MANGA_URL, query.manga_id))
255            .bearer_auth(self.access_token.as_ref().unwrap())
256            .query(&query)
257            .send()
258            .await
259            .map_err(|err| MangaApiError::new(format!("Failed get request: {}", err)))?;
260
261        handle_response(response).await
262    }
263
264    async fn get_ranking(&self, query: &GetMangaRanking) -> Result<String, MangaApiError> {
265        let response = self
266            .client
267            .get(format!("{}/ranking", MANGA_URL))
268            .bearer_auth(self.access_token.as_ref().unwrap())
269            .query(&query)
270            .send()
271            .await
272            .map_err(|err| MangaApiError::new(format!("Failed get request: {}", err)))?;
273
274        handle_response(response).await
275    }
276
277    async fn get_user(&self, query: &GetUserMangaList) -> Result<String, MangaApiError> {
278        let response = self
279            .client
280            .get(format!("{}/{}/mangalist", USER_URL, query.user_name))
281            .bearer_auth(self.access_token.as_ref().unwrap())
282            .query(&query)
283            .send()
284            .await
285            .map_err(|err| MangaApiError::new(format!("Failed get request: {}", err)))?;
286
287        handle_response(response).await
288    }
289
290    async fn get_next_or_prev(&self, query: Option<&String>) -> Result<String, MangaApiError> {
291        if let Some(itr) = query {
292            let response = self
293                .client
294                .get(itr)
295                .bearer_auth(self.access_token.as_ref().unwrap())
296                .send()
297                .await
298                .map_err(|err| MangaApiError::new(format!("Failed get request: {}", err)))?;
299
300            handle_response(response).await
301        } else {
302            Err(MangaApiError::new("Page does not exist".to_string()))
303        }
304    }
305}
306
307/// This trait defines the shared endpoints for Client and Oauth
308/// MangaApiClients. It provides default implementations such that
309/// the Oauth MangaApiClient can override them if needed.
310#[async_trait]
311pub trait MangaApi {
312    type State: Request + Send + Sync;
313
314    /// Get a list of manga that are similar to the given query
315    ///
316    /// Corresponds to the [Get manga list](https://myanimelist.net/apiconfig/references/api/v2#operation/manga_get) endpoint
317    async fn get_manga_list(&self, query: &GetMangaList) -> Result<MangaList, MangaApiError> {
318        let response = self.get_self().get(query).await?;
319        let result: MangaList = serde_json::from_str(response.as_str()).map_err(|err| {
320            MangaApiError::new(format!("Failed to parse MangaList result: {}", err))
321        })?;
322        Ok(result)
323    }
324
325    /// Get the details of a manga that matches the given query
326    ///
327    /// Corresponds to the [Get manga details](https://myanimelist.net/apiconfig/references/api/v2#operation/manga_manga_id_get) endpoint
328    async fn get_manga_details(
329        &self,
330        query: &GetMangaDetails,
331    ) -> Result<MangaDetails, MangaApiError> {
332        let response = self.get_self().get_details(query).await?;
333        let result: MangaDetails = serde_json::from_str(response.as_str()).map_err(|err| {
334            MangaApiError::new(format!("Failed to parse MangaList result: {}", err))
335        })?;
336        Ok(result)
337    }
338
339    /// Get the ranking of manga
340    ///
341    /// Corresponds to the [Get manga ranking](https://myanimelist.net/apiconfig/references/api/v2#operation/manga_ranking_get) endpoint
342    async fn get_manga_ranking(
343        &self,
344        query: &GetMangaRanking,
345    ) -> Result<MangaRanking, MangaApiError> {
346        let response = self.get_self().get_ranking(query).await?;
347        let result: MangaRanking = serde_json::from_str(response.as_str()).map_err(|err| {
348            MangaApiError::new(format!("Failed to parse MangaList result: {}", err))
349        })?;
350        Ok(result)
351    }
352
353    /// Get a users manga list
354    ///
355    /// You **cannot** get the manga list of `@me` with a [ClientId] MangaApiClient
356    ///
357    /// Corresponds to the [Get user mangalist](https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_mangalist_get) endpoint
358    async fn get_user_manga_list(
359        &self,
360        query: &GetUserMangaList,
361    ) -> Result<MangaList, MangaApiError> {
362        if query.user_name == "@me".to_string() {
363            return Err(MangaApiError::new(
364                "You can only get your list via an Oauth client".to_string(),
365            ));
366        }
367        let response = self.get_self().get_user(query).await?;
368        let result: MangaList = serde_json::from_str(response.as_str()).map_err(|err| {
369            MangaApiError::new(format!("Failed to parse Anime List result: {}", err))
370        })?;
371        Ok(result)
372    }
373
374    /// Return the results of the next page, if possible
375    async fn next<T>(&self, response: &T) -> Result<T, MangaApiError>
376    where
377        T: DeserializeOwned + PagingIter + Sync + Send,
378    {
379        let response = self
380            .get_self()
381            .get_next_or_prev(response.next_page())
382            .await?;
383        let result: T = serde_json::from_str(response.as_str())
384            .map_err(|err| MangaApiError::new(format!("Failed to fetch next page: {}", err)))?;
385        Ok(result)
386    }
387
388    /// Return the results of the previous page, if possible
389    async fn prev<T>(&self, response: &T) -> Result<T, MangaApiError>
390    where
391        T: DeserializeOwned + PagingIter + Sync + Send,
392    {
393        let response = self
394            .get_self()
395            .get_next_or_prev(response.prev_page())
396            .await?;
397        let result: T = serde_json::from_str(response.as_str())
398            .map_err(|err| MangaApiError::new(format!("Failed to fetch next page: {}", err)))?;
399        Ok(result)
400    }
401
402    /// Utility method for API trait to use the appropriate request method
403    fn get_self(&self) -> &Self::State;
404}
405
406#[async_trait]
407impl MangaApi for MangaApiClient<Client> {
408    type State = MangaApiClient<Client>;
409
410    fn get_self(&self) -> &Self::State {
411        self
412    }
413}
414
415#[async_trait]
416impl MangaApi for MangaApiClient<Oauth> {
417    type State = MangaApiClient<Oauth>;
418
419    fn get_self(&self) -> &Self::State {
420        self
421    }
422
423    /// Get a users manga list
424    ///
425    /// You **can** get the manga list of `@me` with an [OauthClient] MangaApiClient
426    ///
427    /// Corresponds to the [Get user mangalist](https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_mangalist_get) endpoint
428    async fn get_user_manga_list(
429        &self,
430        query: &GetUserMangaList,
431    ) -> Result<MangaList, MangaApiError> {
432        let response = self.get_self().get_user(query).await?;
433        let result: MangaList = serde_json::from_str(response.as_str()).map_err(|err| {
434            MangaApiError::new(format!("Failed to parse Anime List result: {}", err))
435        })?;
436        Ok(result)
437    }
438}
439
440impl MangaApiClient<Oauth> {
441    /// Update the status of a manga for the OAuth user's manga list
442    ///
443    /// Correspoonds to the [Update my manga list status](https://myanimelist.net/apiconfig/references/api/v2#operation/manga_manga_id_my_list_status_put) endpoint
444    pub async fn update_manga_list_status(
445        &self,
446        query: &UpdateMyMangaListStatus,
447    ) -> Result<MangaListStatus, MangaApiError> {
448        let form_data = struct_to_form_data(&query).map_err(|err| {
449            MangaApiError::new(format!("Failed to turn request into form data: {}", err))
450        })?;
451        let response = self
452            .client
453            .put(format!("{}/{}/my_list_status", MANGA_URL, query.manga_id))
454            .bearer_auth(&self.access_token.as_ref().unwrap())
455            .form(&form_data)
456            .send()
457            .await
458            .map_err(|err| MangaApiError::new(format!("Failed put request: {}", err)))?;
459
460        let response = handle_response(response).await?;
461        let result: MangaListStatus = serde_json::from_str(response.as_str()).map_err(|err| {
462            MangaApiError::new(format!("Failed to parse Anime List result: {}", err))
463        })?;
464        Ok(result)
465    }
466
467    /// Delete a manga entry from the OAuth user's manga list
468    ///
469    /// Corresponds to the [Delete my manga list item](https://myanimelist.net/apiconfig/references/api/v2#operation/manga_manga_id_my_list_status_delete) endpoint
470    pub async fn delete_manga_list_item(
471        &self,
472        query: &DeleteMyMangaListItem,
473    ) -> Result<(), MangaApiError> {
474        let response = self
475            .client
476            .delete(format!("{}/{}/my_list_status", MANGA_URL, query.manga_id))
477            .bearer_auth(&self.access_token.as_ref().unwrap())
478            .send()
479            .await
480            .map_err(|err| MangaApiError::new(format!("Failed delete request: {}", err)))?;
481
482        match response.status() {
483            reqwest::StatusCode::OK => Ok(()),
484            reqwest::StatusCode::NOT_FOUND => Err(MangaApiError::new(
485                "Manga does not exist in user's manga list".to_string(),
486            )),
487            _ => Err(MangaApiError::new(format!(
488                "Did not recieve expected response: {}",
489                response.status()
490            ))),
491        }
492    }
493}
494
495async fn handle_response(response: reqwest::Response) -> Result<String, MangaApiError> {
496    match response.status() {
497        reqwest::StatusCode::OK => {
498            let content = response.text().await.map_err(|err| {
499                MangaApiError::new(format!("Failed to get content from response: {}", err))
500            })?;
501            Ok(content)
502        }
503        _ => Err(MangaApiError::new(format!(
504            "Did not recieve OK response: {}",
505            response.status()
506        ))),
507    }
508}