1use derive_builder::Builder;
29use serde::Serialize;
30use uuid::Uuid;
31
32use crate::HttpClientRef;
33use mangadex_api_schema::v5::MangaResponse;
34use mangadex_api_types::ReferenceExpansionResource;
35
36#[cfg_attr(
37 feature = "deserializable-endpoint",
38 derive(serde::Deserialize, getset::Getters, getset::Setters)
39)]
40#[derive(Debug, Serialize, Clone, Builder)]
41#[serde(rename_all = "camelCase")]
42#[builder(
43 setter(into, strip_option),
44 build_fn(error = "mangadex_api_types::error::BuilderError")
45)]
46pub struct GetManga {
47 #[doc(hidden)]
49 #[serde(skip)]
50 #[builder(pattern = "immutable")]
51 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
52 pub http_client: HttpClientRef,
53
54 #[serde(skip_serializing)]
55 pub manga_id: Uuid,
56
57 #[builder(setter(each = "include"), default)]
58 pub includes: Vec<ReferenceExpansionResource>,
59}
60
61endpoint! {
62 GET ("/manga/{}", manga_id),
63 #[query] GetManga,
64 #[flatten_result] MangaResponse,
65 GetMangaBuilder
66}
67
68#[cfg(test)]
69mod tests {
70 use serde_json::json;
71 use time::OffsetDateTime;
72 use url::Url;
73 use uuid::Uuid;
74 use wiremock::matchers::{method, path_regex};
75 use wiremock::{Mock, MockServer, ResponseTemplate};
76
77 use crate::{HttpClient, MangaDexClient};
78 use mangadex_api_schema::v5::RelatedAttributes;
79 use mangadex_api_types::{
80 MangaDexDateTime, MangaRelation, ReferenceExpansionResource, RelationshipType,
81 };
82
83 #[tokio::test]
84 async fn get_manga_fires_a_request_to_base_url() -> anyhow::Result<()> {
85 let mock_server = MockServer::start().await;
86 let http_client = HttpClient::builder()
87 .base_url(Url::parse(&mock_server.uri())?)
88 .build()?;
89 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
90
91 let manga_id = Uuid::new_v4();
92
93 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
94
95 let response_body = json!({
96 "result": "ok",
97 "response": "entity",
98 "data": {
99 "id": manga_id,
100 "type": "manga",
101 "attributes": {
102 "title": {
103 "en": "Test Manga"
104 },
105 "altTitles": [],
106 "description": {},
107 "isLocked": false,
108 "links": {},
109 "originalLanguage": "ja",
110 "lastVolume": "1",
111 "lastChapter": "1",
112 "publicationDemographic": "shoujo",
113 "status": "completed",
114 "year": 2021,
115 "contentRating": "safe",
116 "chapterNumbersResetOnNewVolume": true,
117 "availableTranslatedLanguages": ["en"],
118 "tags": [],
119 "state": "published",
120 "version": 1,
121 "createdAt": datetime.to_string(),
122 "updatedAt": datetime.to_string(),
123 },
124 "relationships": [
125 {
126 "id": "a3219a4f-73c0-4213-8730-05985130539a",
127 "type": "manga",
128 "related": "side_story",
129 }
130 ]
131 }
132 });
133
134 Mock::given(method("GET"))
135 .and(path_regex(r"/manga/[0-9a-fA-F-]+"))
136 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
137 .expect(1)
138 .mount(&mock_server)
139 .await;
140
141 let res = mangadex_client.manga().id(manga_id).get().send().await?;
142
143 assert_eq!(res.data.relationships[0].type_, RelationshipType::Manga);
144 assert_eq!(
145 res.data.relationships[0].related,
146 Some(MangaRelation::SideStory)
147 );
148 assert!(res.data.relationships[0].attributes.is_none());
149
150 Ok(())
151 }
152
153 #[tokio::test]
154 async fn get_manga_handles_reference_expansion() -> anyhow::Result<()> {
155 let mock_server = MockServer::start().await;
156 let http_client = HttpClient::builder()
157 .base_url(Url::parse(&mock_server.uri())?)
158 .build()?;
159 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
160
161 let manga_id = Uuid::new_v4();
162
163 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
164
165 let response_body = json!({
166 "result": "ok",
167 "response": "entity",
168 "data": {
169 "id": manga_id,
170 "type": "manga",
171 "attributes": {
172 "title": {
173 "en": "Test Manga"
174 },
175 "altTitles": [],
176 "description": {},
177 "isLocked": false,
178 "links": {},
179 "originalLanguage": "ja",
180 "lastVolume": "1",
181 "lastChapter": "1",
182 "publicationDemographic": "shoujo",
183 "status": "completed",
184 "year": 2021,
185 "contentRating": "safe",
186 "chapterNumbersResetOnNewVolume": true,
187 "availableTranslatedLanguages": ["en"],
188 "tags": [],
189 "state": "published",
190 "version": 1,
191 "createdAt": datetime.to_string(),
192 "updatedAt": datetime.to_string(),
193 },
194 "relationships": [
195 {
196 "id": "fc343004-569b-4750-aba0-05ab35efc17c",
197 "type": "author",
198 "attributes": {
199 "name": "Hologfx",
200 "imageUrl": null,
201 "biography": [],
202 "twitter": null,
203 "pixiv": null,
204 "melonBook": null,
205 "fanBox": null,
206 "booth": null,
207 "nicoVideo": null,
208 "skeb": null,
209 "fantia": null,
210 "tumblr": null,
211 "youtube": null,
212 "website": null,
213 "createdAt": "2021-04-19T21:59:45+00:00",
214 "updatedAt": "2021-04-19T21:59:45+00:00",
215 "version": 1
216 }
217 }
218 ]
219 }
220 });
221
222 Mock::given(method("GET"))
223 .and(path_regex(r"/manga/[0-9a-fA-F-]+"))
224 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
225 .expect(1)
226 .mount(&mock_server)
227 .await;
228
229 let res = mangadex_client
230 .manga()
231 .id(manga_id)
232 .get()
233 .include(&ReferenceExpansionResource::Author)
234 .send()
235 .await?;
236
237 assert_eq!(res.data.relationships[0].type_, RelationshipType::Author);
238 assert!(res.data.relationships[0].related.is_none());
239 match res.data.relationships[0].attributes.as_ref().unwrap() {
240 RelatedAttributes::Author(author) => assert_eq!(author.name, "Hologfx".to_string()),
241 _ => panic!("Expected author RelatedAttributes"),
242 }
243
244 Ok(())
245 }
246
247 #[tokio::test]
248 async fn get_manga_handles_null_available_translated_languages_element_value(
249 ) -> anyhow::Result<()> {
250 let mock_server = MockServer::start().await;
251 let http_client = HttpClient::builder()
252 .base_url(Url::parse(&mock_server.uri())?)
253 .build()?;
254 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
255
256 let manga_id = Uuid::new_v4();
257
258 let response_body = json!({
259 "result": "ok",
260 "response": "entity",
261 "data": {
262 "id": manga_id,
263 "type": "manga",
264 "attributes": {
265 "title": {
266 "en": "Komi-san wa Komyushou Desu."
267 },
268 "altTitles": [
269 {
270 "ja-ro": "Comi-san ha Comyusho Desu."
271 },
272 {
273 "en": "Komi Can't Communicate"
274 },
275 {
276 "en": "Komi-san Can't Communicate."
277 },
278 {
279 "en": "Komi-san Has a Communication Disorder."
280 },
281 {
282 "ja-ro": "Komi-san wa Komyushou Desu"
283 },
284 {
285 "ja-ro": "Komi-san wa, Communication Shougai desu."
286 },
287 {
288 "ja-ro": "Komi-san wa, Comyushou desu."
289 },
290 {
291 "ja-ro": "Komi-san wa, Komyushou desu."
292 },
293 {
294 "en": "Miss Komi Is Bad at Communication."
295 },
296 {
297 "ru": "У Коми-сан проблемы с общением"
298 },
299 {
300 "th": "โฉมงามพูดไม่เก่งกับผองเพื่อนไม่เต็มเต็ง"
301 },
302 {
303 "ja": "古見さんは、コミュ症です。"
304 },
305 {
306 "zh": "古見同學有交流障礙症"
307 },
308 {
309 "ko": "코미 양은, 커뮤증이에요"
310 }
311 ],
312 "description": {
313 "en": "Komi-san is a beautiful and admirable girl that no one can take their eyes off of. Almost the whole school sees her as the cold beauty that's out of their league, but Tadano Hitohito knows the truth: she's just really bad at communicating with others.\n\nKomi-san, who wishes to fix this bad habit of hers, tries to improve it with the help of Tadano-kun by achieving her goal of having 100 friends.",
314 "pl": "Komi-san jest piękną i godną podziwu dziewczyną, od której nikt nie może oderwać oczu. Prawie cała szkoła postrzega ją jako zimne piękno, które jest poza ich zasięgiem, ale Tadano Hitohito zna prawdę: młoda piękność po prostu źle komunikuje się z innymi. Komi-san, chce to zmienić, a ma jej w tym pomóc Tadano.",
315 "pt-br": "Komi-san é uma bela e admirável garota que ninguém consegue tirar os olhos. Quase todos da escola a veem como alguém fora do alcance, mas Tadano Shigeo sabe a verdade: **ela apenas não sabe como se comunicar com os outras pessoas**. Komi-san, que deseja corrigir este mau hábito dela, tenta melhorar com a ajuda do Tadano-kun..."
316 },
317 "isLocked": true,
318 "links": {
319 "al": "97852",
320 "ap": "komi-cant-communicate",
321 "bw": "series/129153",
322 "kt": "37855",
323 "mu": "127281",
324 "amz": "https://www.amazon.co.jp/gp/product/B07CBD8DKM",
325 "cdj": "http://www.cdjapan.co.jp/product/NEOBK-1985640",
326 "ebj": "https://ebookjapan.yahoo.co.jp/books/382444/",
327 "mal": "99007",
328 "raw": "https://websunday.net/rensai/komisan/",
329 "engtl": "https://www.viz.com/komi-can-t-communicate"
330 },
331 "originalLanguage": "ja",
332 "lastVolume": "",
333 "lastChapter": "",
334 "publicationDemographic": "shounen",
335 "status": "ongoing",
336 "year": 2016,
337 "contentRating": "safe",
338 "tags": [
339 {
340 "id": "423e2eae-a7a2-4a8b-ac03-a8351462d71d",
341 "type": "tag",
342 "attributes": {
343 "name": {
344 "en": "Romance"
345 },
346 "description": [],
347 "group": "genre",
348 "version": 1
349 },
350 "relationships": []
351 },
352 {
353 "id": "4d32cc48-9f00-4cca-9b5a-a839f0764984",
354 "type": "tag",
355 "attributes": {
356 "name": {
357 "en": "Comedy"
358 },
359 "description": [],
360 "group": "genre",
361 "version": 1
362 },
363 "relationships": []
364 },
365 {
366 "id": "caaa44eb-cd40-4177-b930-79d3ef2afe87",
367 "type": "tag",
368 "attributes": {
369 "name": {
370 "en": "School Life"
371 },
372 "description": [],
373 "group": "theme",
374 "version": 1
375 },
376 "relationships": []
377 },
378 {
379 "id": "e5301a23-ebd9-49dd-a0cb-2add944c7fe9",
380 "type": "tag",
381 "attributes": {
382 "name": {
383 "en": "Slice of Life"
384 },
385 "description": [],
386 "group": "genre",
387 "version": 1
388 },
389 "relationships": []
390 }
391 ],
392 "state": "published",
393 "chapterNumbersResetOnNewVolume": false,
394 "createdAt": "2018-11-22T23:31:37+00:00",
395 "updatedAt": "2022-02-13T22:49:56+00:00",
396 "version": 85,
397 "availableTranslatedLanguages": [
398 "pt-br",
399 "cs",
400 "ru",
401 "en",
402 "fa",
403 "tr",
404 "fr",
405 "pl",
406 "mn",
407 "es-la",
408 "id",
409 "it",
410 "hi",
411 "tl",
412 "hu",
413 "de",
414 "ro",
415 "nl",
416 null
417 ]
418 },
419 "relationships": [
420 {
421 "id": "4218b1ee-cde4-44dc-84c7-d9a794a7e56d",
422 "type": "author"
423 },
424 {
425 "id": "4218b1ee-cde4-44dc-84c7-d9a794a7e56d",
426 "type": "artist"
427 },
428 {
429 "id": "9324d3c0-d90d-4f3e-b79b-866029b721a7",
430 "type": "cover_art"
431 },
432 {
433 "id": "2917e1b1-06c0-45fe-b30b-6688d83859b2",
434 "type": "manga",
435 "related": "doujinshi"
436 },
437 {
438 "id": "3e8df40e-e2b3-4336-987b-f3e52d00ce5f",
439 "type": "manga",
440 "related": "doujinshi"
441 },
442 {
443 "id": "60e5c222-f0aa-4f14-baba-b18207321d5e",
444 "type": "manga",
445 "related": "doujinshi"
446 },
447 {
448 "id": "82478f68-943e-4391-b445-f2f9b0007b95",
449 "type": "manga",
450 "related": "doujinshi"
451 },
452 {
453 "id": "973de049-748a-4446-98b9-dfea826f61a5",
454 "type": "manga",
455 "related": "doujinshi"
456 },
457 {
458 "id": "cb655d77-f369-4a06-9a35-b38c00f34e9b",
459 "type": "manga",
460 "related": "doujinshi"
461 },
462 {
463 "id": "d6448e6b-4409-4380-b74e-1629c6d1d1a7",
464 "type": "manga",
465 "related": "doujinshi"
466 },
467 {
468 "id": "fb569d12-1e00-47e3-86cd-793b4eae715c",
469 "type": "manga",
470 "related": "colored"
471 }
472 ]
473 }
474 });
475
476 Mock::given(method("GET"))
477 .and(path_regex(r"/manga/[0-9a-fA-F-]+"))
478 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
479 .expect(1)
480 .mount(&mock_server)
481 .await;
482
483 let res = mangadex_client.manga().id(manga_id).get().send().await?;
484
485 assert_eq!(res.data.attributes.available_translated_languages.len(), 18);
487
488 Ok(())
489 }
490}