mangadex_api/v5/manga/random/
get.rs1use 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 #[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 #[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}