mangadex_api/v5/manga/random/
get.rs

1//! Builder for the random manga endpoint.
2//!
3//! <https://api.mangadex.org/docs/swagger.html#/Manga/get-manga-random>
4//!
5//! # Examples
6//!
7//! ```rust
8//! use mangadex_api::v5::MangaDexClient;
9//!
10//! # async fn run() -> anyhow::Result<()> {
11//! let client = MangaDexClient::default();
12//!
13//! let manga_res = client
14//!     .manga()
15//!     .random()
16//!     .get()
17//!     .send()
18//!     .await?;
19//!
20//! println!("random manga: {:?}", manga_res);
21//! # Ok(())
22//! # }
23//! ```
24
25use derive_builder::Builder;
26use mangadex_api_schema::v5::MangaData;
27use mangadex_api_types::{ContentRating, ReferenceExpansionResource, TagSearchMode};
28use serde::Serialize;
29use uuid::Uuid;
30
31use crate::HttpClientRef;
32
33#[cfg_attr(
34    feature = "deserializable-endpoint",
35    derive(serde::Deserialize, getset::Getters, getset::Setters)
36)]
37#[derive(Debug, Serialize, Clone, Builder)]
38#[serde(rename_all = "camelCase")]
39#[builder(
40    setter(into, strip_option),
41    build_fn(error = "mangadex_api_types::error::BuilderError")
42)]
43pub struct GetRandomManga {
44    /// This should never be set manually as this is only for internal use.
45    #[doc(hidden)]
46    #[serde(skip)]
47    #[builder(pattern = "immutable")]
48    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
49    pub http_client: HttpClientRef,
50
51    #[builder(setter(each = "include"), default)]
52    #[serde(skip_serializing_if = "Vec::is_empty")]
53    pub includes: Vec<ReferenceExpansionResource>,
54
55    /// Ensure the returned Manga is one of the given content ratings.
56    ///
57    /// If this is not set, the default ratings MangaDex will use are:
58    ///     - safe
59    ///     - suggestive
60    ///     - erotica
61    #[builder(setter(each = "add_content_rating"), default)]
62    #[serde(skip_serializing_if = "Vec::is_empty")]
63    pub content_rating: Vec<ContentRating>,
64
65    #[builder(setter(each = "include_tag"), default)]
66    #[serde(skip_serializing_if = "Vec::is_empty")]
67    pub included_tags: Vec<Uuid>,
68
69    #[builder(default)]
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub included_tags_mode: Option<TagSearchMode>,
72
73    #[builder(setter(each = "exclude_tag"), default)]
74    #[serde(skip_serializing_if = "Vec::is_empty")]
75    pub excluded_tags: Vec<Uuid>,
76
77    #[builder(default)]
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub excluded_tags_mode: Option<TagSearchMode>,
80}
81
82endpoint! {
83    GET ("/manga/random"),
84    #[query] GetRandomManga,
85    #[rate_limited] MangaData,
86    GetRandomMangaBuilder
87}
88
89#[cfg(test)]
90mod tests {
91    use serde_json::json;
92    use time::OffsetDateTime;
93    use url::Url;
94    use uuid::Uuid;
95    use wiremock::matchers::{method, path};
96    use wiremock::{Mock, MockServer, ResponseTemplate};
97
98    use crate::{HttpClient, MangaDexClient};
99    use mangadex_api_types::error::Error;
100    use mangadex_api_types::MangaDexDateTime;
101
102    #[tokio::test]
103    async fn get_random_manga_fires_a_request_to_base_url() -> anyhow::Result<()> {
104        let mock_server = MockServer::start().await;
105        let http_client = HttpClient::builder()
106            .base_url(Url::parse(&mock_server.uri())?)
107            .build()?;
108        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
109
110        let manga_id = Uuid::new_v4();
111
112        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
113
114        let response_body = json!({
115            "result": "ok",
116            "response": "entity",
117            "data": {
118                "id": manga_id,
119                "type": "manga",
120                "attributes": {
121                    "title": {
122                        "en": "Test Manga"
123                    },
124                    "altTitles": [],
125                    "description": {},
126                    "isLocked": false,
127                    "links": null,
128                    "originalLanguage": "ja",
129                    "lastVolume": "1",
130                    "lastChapter": "1",
131                    "publicationDemographic": "shoujo",
132                    "status": "completed",
133                    "year": 2021,
134                    "contentRating": "safe",
135                    "chapterNumbersResetOnNewVolume": true,
136                    "availableTranslatedLanguages": ["en"],
137                    "tags": [],
138                    "state": "published",
139                    "version": 1,
140                    "createdAt": datetime.to_string(),
141                    "updatedAt": datetime.to_string(),
142                },
143                "relationships": []
144            }
145        });
146
147        Mock::given(method("GET"))
148            .and(path(r"/manga/random"))
149            .respond_with(
150                ResponseTemplate::new(200)
151                    .insert_header("x-ratelimit-retry-after", "1698723860")
152                    .insert_header("x-ratelimit-limit", "40")
153                    .insert_header("x-ratelimit-remaining", "39")
154                    .set_body_json(response_body),
155            )
156            .expect(1)
157            .mount(&mock_server)
158            .await;
159
160        let _ = mangadex_client.manga().random().get().send().await?;
161
162        Ok(())
163    }
164
165    #[tokio::test]
166    async fn get_random_manga_deserialize_handles_empty_array_links_field() -> anyhow::Result<()> {
167        let mock_server = MockServer::start().await;
168        let http_client = HttpClient::builder()
169            .base_url(Url::parse(&mock_server.uri())?)
170            .build()?;
171        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
172
173        let manga_id = Uuid::new_v4();
174
175        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
176
177        let response_body = json!({
178            "result": "ok",
179            "response": "entity",
180            "data": {
181                "id": manga_id,
182                "type": "manga",
183                "attributes": {
184                    "title": {
185                        "en": "Test Manga"
186                    },
187                    "altTitles": [],
188                    "description": {},
189                    "isLocked": false,
190                    "links": [],
191                    "originalLanguage": "ja",
192                    "lastVolume": "1",
193                    "lastChapter": "1",
194                    "publicationDemographic": "shoujo",
195                    "status": "completed",
196                    "year": 2021,
197                    "contentRating": "safe",
198                    "chapterNumbersResetOnNewVolume": true,
199                    "availableTranslatedLanguages": ["en"],
200                    "tags": [],
201                    "state": "published",
202                    "version": 1,
203                    "createdAt": datetime.to_string(),
204                    "updatedAt": datetime.to_string(),
205                },
206                "relationships": []
207            }
208        });
209
210        Mock::given(method("GET"))
211            .and(path(r"/manga/random"))
212            .respond_with(
213                ResponseTemplate::new(200)
214                    .insert_header("x-ratelimit-retry-after", "1698723860")
215                    .insert_header("x-ratelimit-limit", "40")
216                    .insert_header("x-ratelimit-remaining", "39")
217                    .set_body_json(response_body),
218            )
219            .expect(1)
220            .mount(&mock_server)
221            .await;
222
223        let res = mangadex_client.manga().random().get().send().await?;
224
225        assert!(res.data.attributes.links.is_none());
226
227        Ok(())
228    }
229
230    #[tokio::test]
231    async fn get_random_manga_deserialize_handles_links_field() -> anyhow::Result<()> {
232        let mock_server = MockServer::start().await;
233        let http_client = HttpClient::builder()
234            .base_url(Url::parse(&mock_server.uri())?)
235            .build()?;
236        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
237
238        let manga_id = Uuid::new_v4();
239
240        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
241
242        let response_body = json!({
243            "result": "ok",
244            "response": "entity",
245            "data": {
246                "id": manga_id,
247                "type": "manga",
248                "attributes": {
249                    "title": {
250                        "en": "Test Manga"
251                    },
252                    "altTitles": [],
253                    "description": {},
254                    "isLocked": false,
255                    "links": {
256                        "bw": "1",
257                        "ebj": "https://ebookjapan.yahoo.co.jp/",
258                        "cdj": "https://www.cdjapan.co.jp/",
259                        "raw": "https://miku.mangadex.org",
260                        "engtl": "https://mangadex.org",
261                        "kt": "1",
262                        "al": "1",
263                        "ap": "a",
264                        "nu": "a",
265                        "mal": "1"
266                    },
267                    "originalLanguage": "ja",
268                    "lastVolume": "1",
269                    "lastChapter": "1",
270                    "publicationDemographic": "shoujo",
271                    "status": "completed",
272                    "year": 2021,
273                    "contentRating": "safe",
274                    "chapterNumbersResetOnNewVolume": true,
275                    "availableTranslatedLanguages": ["en"],
276                    "tags": [],
277                    "state": "published",
278                    "version": 1,
279                    "createdAt": datetime.to_string(),
280                    "updatedAt": datetime.to_string(),
281                },
282                "relationships": []
283            }
284        });
285
286        Mock::given(method("GET"))
287            .and(path(r"/manga/random"))
288            .respond_with(
289                ResponseTemplate::new(200)
290                    .insert_header("x-ratelimit-retry-after", "1698723860")
291                    .insert_header("x-ratelimit-limit", "40")
292                    .insert_header("x-ratelimit-remaining", "39")
293                    .set_body_json(response_body),
294            )
295            .expect(1)
296            .mount(&mock_server)
297            .await;
298
299        let res = mangadex_client.manga().random().get().send().await?;
300
301        if let Some(links) = &res.data.attributes.links {
302            assert_eq!(links.book_walker.clone().unwrap().0, "1".to_string());
303            assert_eq!(links.manga_updates, None);
304        } else {
305            panic!("error deserializing 'links' field");
306        }
307
308        Ok(())
309    }
310
311    #[tokio::test]
312    async fn get_random_manga_handles_http_503() -> anyhow::Result<()> {
313        let mock_server = MockServer::start().await;
314        let http_client = HttpClient::builder()
315            .base_url(Url::parse(&mock_server.uri())?)
316            .build()?;
317        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
318
319        Mock::given(method("GET"))
320            .and(path(r"/manga/random"))
321            .respond_with(
322                ResponseTemplate::new(503)
323                    .insert_header("x-ratelimit-retry-after", "1698723860")
324                    .insert_header("x-ratelimit-limit", "40")
325                    .insert_header("x-ratelimit-remaining", "39"),
326            )
327            .expect(1)
328            .mount(&mock_server)
329            .await;
330
331        match mangadex_client.manga().random().get().send().await {
332            Err(Error::ServerError(..)) => {}
333            _ => panic!("expected server error"),
334        }
335
336        Ok(())
337    }
338}