1use super::error::MangaApiError;
2use serde::{Deserialize, Serialize};
3use strum_macros::EnumIter;
4
5#[derive(Debug, Serialize)]
6pub struct GetMangaList {
7 q: String,
8 nsfw: bool,
9 limit: u16,
10 offset: u32,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 fields: Option<String>,
13}
14
15impl GetMangaList {
16 pub fn new<T: Into<String>>(
20 q: T,
21 nsfw: bool,
22 fields: Option<&MangaCommonFields>,
23 limit: Option<u16>,
24 offset: Option<u32>,
25 ) -> Result<Self, MangaApiError> {
26 let q = q.into();
27 let limit = limit.map(|l| l.clamp(1, 100));
28
29 if q.is_empty() {
30 return Err(MangaApiError::new("Query cannot be empty".to_string()));
31 }
32
33 Ok(Self {
34 q,
35 nsfw,
36 limit: limit.unwrap_or(100),
37 offset: offset.unwrap_or(0),
38 fields: fields.map(|f| f.into()),
39 })
40 }
41
42 pub fn builder<T: Into<String>>(q: T) -> GetMangaListBuilder<'static> {
44 GetMangaListBuilder::new(q.into())
45 }
46}
47
48pub struct GetMangaListBuilder<'a> {
49 q: String,
50 nsfw: bool,
51 fields: Option<&'a MangaCommonFields>,
52 limit: Option<u16>,
53 offset: Option<u32>,
54}
55
56impl<'a> GetMangaListBuilder<'a> {
57 pub fn new<T: Into<String>>(q: T) -> Self {
58 let q = q.into();
59 Self {
60 q,
61 nsfw: false,
62 fields: None,
63 limit: None,
64 offset: None,
65 }
66 }
67
68 pub fn q<T: Into<String>>(mut self, value: T) -> Self {
69 self.q = value.into();
70 self
71 }
72
73 pub fn enable_nsfw(mut self) -> Self {
74 self.nsfw = true;
75 self
76 }
77
78 pub fn fields(mut self, value: &'a MangaCommonFields) -> Self {
79 self.fields = Some(value.into());
80 self
81 }
82
83 pub fn limit(mut self, value: u16) -> Self {
84 self.limit = Some(value.clamp(1, 100));
85 self
86 }
87
88 pub fn offset(mut self, value: u32) -> Self {
89 self.offset = Some(value);
90 self
91 }
92
93 pub fn build(self) -> Result<GetMangaList, MangaApiError> {
94 GetMangaList::new(self.q, self.nsfw, self.fields, self.limit, self.offset)
95 }
96}
97
98#[derive(Debug, Serialize)]
99pub struct GetMangaDetails {
100 #[serde(skip_serializing)]
101 pub(crate) manga_id: u32,
102 nsfw: bool,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 fields: Option<String>,
105}
106
107impl GetMangaDetails {
108 pub fn new(
110 manga_id: u32,
111 nsfw: bool,
112 fields: Option<&MangaDetailFields>,
113 ) -> Result<Self, MangaApiError> {
114 if manga_id == 0 {
115 return Err(MangaApiError::new(
116 "manga_id must be greater than 0".to_string(),
117 ));
118 }
119
120 Ok(Self {
121 manga_id,
122 nsfw,
123 fields: fields.map(|f| f.into()),
124 })
125 }
126
127 pub fn builder(manga_id: u32) -> GetMangaDetailsBuilder<'static> {
129 GetMangaDetailsBuilder::new(manga_id)
130 }
131}
132
133pub struct GetMangaDetailsBuilder<'a> {
134 manga_id: u32,
135 nsfw: bool,
136 fields: Option<&'a MangaDetailFields>,
137}
138
139impl<'a> GetMangaDetailsBuilder<'a> {
140 pub fn new(manga_id: u32) -> Self {
141 Self {
142 manga_id,
143 nsfw: false,
144 fields: None,
145 }
146 }
147
148 pub fn manga_id(mut self, value: u32) -> Self {
149 self.manga_id = value;
150 self
151 }
152
153 pub fn enable_nsfw(mut self) -> Self {
154 self.nsfw = true;
155 self
156 }
157
158 pub fn fields(mut self, value: &'a MangaDetailFields) -> Self {
159 self.fields = Some(value.into());
160 self
161 }
162
163 pub fn build(self) -> Result<GetMangaDetails, MangaApiError> {
164 GetMangaDetails::new(self.manga_id, self.nsfw, self.fields)
165 }
166}
167
168#[derive(Debug, Serialize)]
169#[serde(rename_all = "lowercase")]
170pub enum MangaRankingType {
171 All,
172 Manga,
173 Novels,
174 Oneshots,
175 Doujin,
176 Manhwa,
177 Manhua,
178 ByPopularity,
179 Favorite,
180}
181
182#[derive(Debug, Serialize)]
183pub struct GetMangaRanking {
184 ranking_type: MangaRankingType,
185 nsfw: bool,
186 limit: u16,
187 offset: u32,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 fields: Option<String>,
190}
191
192impl GetMangaRanking {
193 pub fn new(
197 ranking_type: MangaRankingType,
198 nsfw: bool,
199 fields: Option<&MangaCommonFields>,
200 limit: Option<u16>,
201 offset: Option<u32>,
202 ) -> Self {
203 let limit = limit.map(|l| l.clamp(1, 500));
204
205 Self {
206 ranking_type,
207 nsfw,
208 limit: limit.unwrap_or(100),
209 offset: offset.unwrap_or(0),
210 fields: fields.map(|f| f.into()),
211 }
212 }
213
214 pub fn builder(ranking_type: MangaRankingType) -> GetMangaRankingBuilder<'static> {
216 GetMangaRankingBuilder::new(ranking_type)
217 }
218}
219
220pub struct GetMangaRankingBuilder<'a> {
221 ranking_type: MangaRankingType,
222 nsfw: bool,
223 fields: Option<&'a MangaCommonFields>,
224 limit: Option<u16>,
225 offset: Option<u32>,
226}
227
228impl<'a> GetMangaRankingBuilder<'a> {
229 pub fn new(ranking_type: MangaRankingType) -> Self {
230 Self {
231 ranking_type,
232 nsfw: false,
233 fields: None,
234 limit: None,
235 offset: None,
236 }
237 }
238
239 pub fn ranking_type(mut self, value: MangaRankingType) -> Self {
240 self.ranking_type = value;
241 self
242 }
243
244 pub fn enable_nsfw(mut self) -> Self {
245 self.nsfw = true;
246 self
247 }
248
249 pub fn fields(mut self, value: &'a MangaCommonFields) -> Self {
250 self.fields = Some(value.into());
251 self
252 }
253
254 pub fn limit(mut self, value: u16) -> Self {
255 self.limit = Some(value.clamp(1, 500));
256 self
257 }
258
259 pub fn offset(mut self, value: u32) -> Self {
260 self.offset = Some(value);
261 self
262 }
263
264 pub fn build(self) -> GetMangaRanking {
265 GetMangaRanking::new(
266 self.ranking_type,
267 self.nsfw,
268 self.fields,
269 self.limit,
270 self.offset,
271 )
272 }
273}
274
275#[derive(Debug, Serialize, Deserialize)]
276#[serde(rename_all = "snake_case")]
277pub enum UserMangaListStatus {
278 Reading,
279 Completed,
280 OnHold,
281 Dropped,
282 PlanToRead,
283}
284
285#[derive(Debug, Serialize)]
286#[serde(rename_all = "snake_case")]
287pub enum UserMangaListSort {
288 ListScore,
289 ListUpdatedAt,
290 MangaTitle,
291 MangaStartDate,
292 }
295
296#[derive(Debug, Serialize)]
297pub struct GetUserMangaList {
298 #[serde(skip_serializing)]
299 pub(crate) user_name: String,
300 nsfw: bool,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 status: Option<UserMangaListStatus>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 sort: Option<UserMangaListSort>,
305 limit: u16,
306 offset: u32,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 fields: Option<String>,
309}
310
311impl GetUserMangaList {
312 pub fn new(
316 user_name: String,
317 nsfw: bool,
318 fields: Option<&MangaCommonFields>,
319 status: Option<UserMangaListStatus>,
320 sort: Option<UserMangaListSort>,
321 limit: Option<u16>,
322 offset: Option<u32>,
323 ) -> Result<Self, MangaApiError> {
324 let limit = limit.map(|l| l.clamp(1, 1000));
325
326 if user_name.is_empty() {
327 return Err(MangaApiError::new("user_name cannot be empty".to_string()));
328 }
329
330 Ok(Self {
331 user_name,
332 nsfw,
333 status,
334 sort,
335 limit: limit.unwrap_or(100),
336 offset: offset.unwrap_or(0),
337 fields: fields.map(|f| f.into()),
338 })
339 }
340
341 pub fn builder(user_name: &str) -> GetUserMangaListBuilder<'static> {
343 GetUserMangaListBuilder::new(user_name.to_string())
344 }
345}
346
347pub struct GetUserMangaListBuilder<'a> {
348 user_name: String,
349 nsfw: bool,
350 fields: Option<&'a MangaCommonFields>,
351 status: Option<UserMangaListStatus>,
352 sort: Option<UserMangaListSort>,
353 limit: Option<u16>,
354 offset: Option<u32>,
355}
356
357impl<'a> GetUserMangaListBuilder<'a> {
358 pub fn new(user_name: String) -> Self {
359 Self {
360 user_name,
361 nsfw: false,
362 fields: None,
363 status: None,
364 sort: None,
365 limit: None,
366 offset: None,
367 }
368 }
369
370 pub fn user_name<T: Into<String>>(mut self, value: T) -> Self {
371 self.user_name = value.into();
372 self
373 }
374
375 pub fn enable_nsfw(mut self) -> Self {
376 self.nsfw = true;
377 self
378 }
379
380 pub fn fields(mut self, value: &'a MangaCommonFields) -> Self {
381 self.fields = Some(value.into());
382 self
383 }
384
385 pub fn status(mut self, value: UserMangaListStatus) -> Self {
386 self.status = Some(value);
387 self
388 }
389
390 pub fn sort(mut self, value: UserMangaListSort) -> Self {
391 self.sort = Some(value);
392 self
393 }
394
395 pub fn limit(mut self, value: u16) -> Self {
396 self.limit = Some(value.clamp(1, 1000));
397 self
398 }
399
400 pub fn offset(mut self, value: u32) -> Self {
401 self.offset = Some(value);
402 self
403 }
404
405 pub fn build(self) -> Result<GetUserMangaList, MangaApiError> {
406 GetUserMangaList::new(
407 self.user_name,
408 self.nsfw,
409 self.fields,
410 self.status,
411 self.sort,
412 self.limit,
413 self.offset,
414 )
415 }
416}
417
418#[derive(Debug, Serialize)]
419pub struct UpdateMyMangaListStatus {
420 #[serde(skip_serializing)]
421 pub(crate) manga_id: u32,
422 #[serde(skip_serializing_if = "Option::is_none")]
423 status: Option<UserMangaListStatus>,
424 #[serde(skip_serializing_if = "Option::is_none")]
425 is_rereading: Option<bool>,
426 #[serde(skip_serializing_if = "Option::is_none")]
427 score: Option<u8>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 num_volumes_read: Option<u32>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 num_chapters_read: Option<u32>,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 priority: Option<u8>,
434 #[serde(skip_serializing_if = "Option::is_none")]
435 num_times_reread: Option<u32>,
436 #[serde(skip_serializing_if = "Option::is_none")]
437 reread_value: Option<u8>,
438 #[serde(skip_serializing_if = "Option::is_none")]
439 tags: Option<String>,
440 #[serde(skip_serializing_if = "Option::is_none")]
441 comments: Option<String>,
442}
443
444impl UpdateMyMangaListStatus {
445 pub fn new(
453 manga_id: u32,
454 status: Option<UserMangaListStatus>,
455 is_rereading: Option<bool>,
456 score: Option<u8>,
457 num_volumes_read: Option<u32>,
458 num_chapters_read: Option<u32>,
459 priority: Option<u8>,
460 num_times_reread: Option<u32>,
461 reread_value: Option<u8>,
462 tags: Option<String>,
463 comments: Option<String>,
464 ) -> Result<Self, MangaApiError> {
465 if let Some(score) = score {
467 if score > 10 {
468 return Err(MangaApiError::new(
469 "Score must be between 0 and 10 inclusive".to_string(),
470 ));
471 }
472 }
473 if let Some(priority) = priority {
474 if priority > 2 {
475 return Err(MangaApiError::new(
476 "Priority must be between 0 and 2 inclusive".to_string(),
477 ));
478 }
479 }
480 if let Some(reread_value) = reread_value {
481 if reread_value > 5 {
482 return Err(MangaApiError::new(
483 "Reread value must be between 0 and 5 inclusive".to_string(),
484 ));
485 }
486 }
487
488 if manga_id == 0 {
489 return Err(MangaApiError::new(
490 "manga_id must be greater than 0".to_string(),
491 ));
492 }
493
494 if !(status.is_some()
495 || is_rereading.is_some()
496 || score.is_some()
497 || num_chapters_read.is_some()
498 || num_volumes_read.is_some()
499 || priority.is_some()
500 || num_times_reread.is_some()
501 || reread_value.is_some()
502 || tags.is_some()
503 || comments.is_some())
504 {
505 return Err(MangaApiError::new(
506 "At least one of the optional arguments must be Some".to_string(),
507 ));
508 }
509
510 Ok(Self {
511 manga_id,
512 status,
513 is_rereading,
514 score,
515 num_volumes_read,
516 num_chapters_read,
517 priority,
518 num_times_reread,
519 reread_value,
520 tags,
521 comments,
522 })
523 }
524
525 pub fn builder(manga_id: u32) -> UpdateMyMangaListStatusBuilder {
527 UpdateMyMangaListStatusBuilder::new(manga_id)
528 }
529}
530
531pub struct UpdateMyMangaListStatusBuilder {
532 manga_id: u32,
533 status: Option<UserMangaListStatus>,
534 is_rereading: Option<bool>,
535 score: Option<u8>,
536 num_volumes_read: Option<u32>,
537 num_chapters_read: Option<u32>,
538 priority: Option<u8>,
539 num_times_reread: Option<u32>,
540 reread_value: Option<u8>,
541 tags: Option<String>,
542 comments: Option<String>,
543}
544
545impl UpdateMyMangaListStatusBuilder {
546 pub fn new(manga_id: u32) -> Self {
547 Self {
548 manga_id,
549 status: None,
550 is_rereading: None,
551 score: None,
552 num_volumes_read: None,
553 num_chapters_read: None,
554 priority: None,
555 num_times_reread: None,
556 reread_value: None,
557 tags: None,
558 comments: None,
559 }
560 }
561
562 pub fn manga_id(mut self, value: u32) -> Self {
563 self.manga_id = value;
564 self
565 }
566
567 pub fn status(mut self, value: UserMangaListStatus) -> Self {
568 self.status = Some(value);
569 self
570 }
571
572 pub fn is_rereading(mut self, value: bool) -> Self {
573 self.is_rereading = Some(value);
574 self
575 }
576
577 pub fn score(mut self, value: u8) -> Self {
578 self.score = Some(value);
579 self
580 }
581
582 pub fn num_volumes_read(mut self, value: u32) -> Self {
583 self.num_volumes_read = Some(value);
584 self
585 }
586
587 pub fn num_chapters_read(mut self, value: u32) -> Self {
588 self.num_chapters_read = Some(value);
589 self
590 }
591
592 pub fn priority(mut self, value: u8) -> Self {
593 self.priority = Some(value);
594 self
595 }
596
597 pub fn num_times_reread(mut self, value: u32) -> Self {
598 self.num_times_reread = Some(value);
599 self
600 }
601
602 pub fn reread_value(mut self, value: u8) -> Self {
603 self.reread_value = Some(value);
604 self
605 }
606
607 pub fn tags(mut self, value: &str) -> Self {
608 self.tags = Some(value.to_string());
609 self
610 }
611
612 pub fn comments(mut self, value: &str) -> Self {
613 self.comments = Some(value.to_string());
614 self
615 }
616
617 pub fn build(self) -> Result<UpdateMyMangaListStatus, MangaApiError> {
618 UpdateMyMangaListStatus::new(
619 self.manga_id,
620 self.status,
621 self.is_rereading,
622 self.score,
623 self.num_volumes_read,
624 self.num_chapters_read,
625 self.priority,
626 self.num_times_reread,
627 self.reread_value,
628 self.tags,
629 self.comments,
630 )
631 }
632}
633
634#[derive(Debug)]
635pub struct DeleteMyMangaListItem {
636 pub(crate) manga_id: u32,
637}
638
639impl DeleteMyMangaListItem {
640 pub fn new(manga_id: u32) -> Self {
642 Self { manga_id }
643 }
644}
645
646#[derive(Debug, EnumIter, PartialEq)]
647#[allow(non_camel_case_types)]
648pub enum MangaField {
649 id,
650 title,
651 main_picture,
652 alternative_titles,
653 start_date,
654 end_date,
655 synopsis,
656 mean,
657 rank,
658 popularity,
659 num_list_users,
660 num_scoring_users,
661 nsfw,
662 genres,
663 created_at,
664 updated_at,
665 media_type,
666 status,
667 my_list_status,
668 num_volumes,
669 num_chapters,
670 authors,
671}
672
673#[derive(Debug, EnumIter, PartialEq)]
674#[allow(non_camel_case_types)]
675pub enum MangaDetail {
676 id,
678 title,
679 main_picture,
680 alternative_titles,
681 start_date,
682 end_date,
683 synopsis,
684 mean,
685 rank,
686 popularity,
687 num_list_users,
688 num_scoring_users,
689 nsfw,
690 genres,
691 created_at,
692 updated_at,
693 media_type,
694 status,
695 my_list_status,
696 num_volumes,
697 num_chapters,
698 authors,
699
700 pictures,
702 background,
703 related_anime,
704 related_manga,
705 recommendations,
706 serialization,
707}
708
709#[derive(Debug)]
711pub struct MangaCommonFields(pub Vec<MangaField>);
712
713#[derive(Debug)]
715pub struct MangaDetailFields(pub Vec<MangaDetail>);
716
717impl Into<String> for &MangaCommonFields {
718 fn into(self) -> String {
719 let result = self
720 .0
721 .iter()
722 .map(|e| format!("{:?}", e))
723 .collect::<Vec<String>>()
724 .join(",");
725 result
726 }
727}
728
729impl Into<String> for &MangaDetailFields {
730 fn into(self) -> String {
731 let result = self
732 .0
733 .iter()
734 .map(|e| format!("{:?}", e))
735 .collect::<Vec<String>>()
736 .join(",");
737 result
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744 use crate::manga::all_common_fields;
745
746 #[test]
747 fn test_get_manga_list() {
748 let fields = all_common_fields();
749 let query = GetMangaList::new("".to_string(), false, Some(&fields), None, None);
750 assert!(query.is_err());
751
752 let query = GetMangaList::new("one".to_string(), false, Some(&fields), Some(101), None);
753 assert_eq!(query.unwrap().limit, 100);
754
755 let query = GetMangaList::new("one".to_string(), false, Some(&fields), Some(0), None);
756 assert_eq!(query.unwrap().limit, 1);
757
758 let query = GetMangaList::new("one".to_string(), false, Some(&fields), Some(100), None);
759 assert_eq!(query.unwrap().limit, 100);
760
761 let query = GetMangaList::new("one".to_string(), false, Some(&fields), None, None);
762 assert_eq!(query.unwrap().limit, 100);
763 }
764
765 #[test]
766 fn test_get_manga_ranking() {
767 let fields = all_common_fields();
768 let query =
769 GetMangaRanking::new(MangaRankingType::All, false, Some(&fields), Some(501), None);
770 assert_eq!(query.limit, 500);
771
772 let query =
773 GetMangaRanking::new(MangaRankingType::All, false, Some(&fields), Some(0), None);
774 assert_eq!(query.limit, 1);
775
776 let query =
777 GetMangaRanking::new(MangaRankingType::All, false, Some(&fields), Some(500), None);
778 assert_eq!(query.limit, 500);
779
780 let query = GetMangaRanking::new(MangaRankingType::All, false, Some(&fields), None, None);
781 assert_eq!(query.limit, 100);
782 }
783
784 #[test]
785 fn test_get_user_manga_list() {
786 let fields = all_common_fields();
787 let query =
788 GetUserMangaList::new("".to_string(), false, Some(&fields), None, None, None, None);
789 assert!(query.is_err());
790
791 let query = GetUserMangaList::new(
792 "hello".to_string(),
793 false,
794 Some(&fields),
795 None,
796 None,
797 Some(1001),
798 None,
799 );
800 assert_eq!(query.unwrap().limit, 1000);
801
802 let query = GetUserMangaList::new(
803 "hello".to_string(),
804 false,
805 Some(&fields),
806 None,
807 None,
808 Some(0),
809 None,
810 );
811 assert_eq!(query.unwrap().limit, 1);
812
813 let query = GetUserMangaList::new(
814 "hello".to_string(),
815 false,
816 Some(&fields),
817 None,
818 None,
819 Some(1000),
820 None,
821 );
822 assert_eq!(query.unwrap().limit, 1000);
823
824 let query = GetUserMangaList::new(
825 "hello".to_string(),
826 false,
827 Some(&fields),
828 None,
829 None,
830 None,
831 None,
832 );
833 assert_eq!(query.unwrap().limit, 100);
834 }
835
836 #[test]
837 fn test_update_my_manga_list_status() {
838 let query = UpdateMyMangaListStatus::new(
839 1234, None, None, None, None, None, None, None, None, None, None,
840 );
841 assert!(query.is_err());
842
843 let query = UpdateMyMangaListStatus::new(
844 1234,
845 Some(UserMangaListStatus::Completed),
846 None,
847 Some(11),
848 None,
849 None,
850 None,
851 None,
852 None,
853 None,
854 None,
855 );
856 assert!(query.is_err());
857
858 let query = UpdateMyMangaListStatus::new(
859 1234,
860 Some(UserMangaListStatus::Completed),
861 None,
862 None,
863 None,
864 None,
865 Some(3),
866 None,
867 None,
868 None,
869 None,
870 );
871 assert!(query.is_err());
872
873 let query = UpdateMyMangaListStatus::new(
874 1234,
875 Some(UserMangaListStatus::Completed),
876 None,
877 None,
878 None,
879 None,
880 None,
881 None,
882 Some(6),
883 None,
884 None,
885 );
886 assert!(query.is_err());
887
888 let query = UpdateMyMangaListStatus::new(
889 1234,
890 Some(UserMangaListStatus::Completed),
891 None,
892 Some(10),
893 None,
894 None,
895 Some(2),
896 None,
897 Some(5),
898 None,
899 None,
900 );
901 assert!(query.is_ok())
902 }
903}