mal_api/manga/
requests.rs

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    /// Create new `Get manga list` query
17    ///
18    /// Limit must be within `[1, 100]`. Default to 100
19    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    /// Use builder pattern for building up the query with required arguments
43    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    /// Create new `Get manga details` query
109    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    /// Use builder pattern for building up the query with required arguments
128    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    /// Create new `Get manga ranking`
194    ///
195    /// Limit must be within `[1, 500]`. Defaults to 100
196    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    /// Use builder pattern for building up the query with required arguments
215    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    // TODO: This sort option is still under development according to MAL API reference
293    // MangaId,
294}
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    /// Create new `Get user manga list` query
313    ///
314    /// Limit must be within `[1, 1000]`. Defaults to 100
315    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    /// Use builder pattern for building up the query with required arguments
342    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    /// Create new `Update my manga list status` query
446    ///
447    /// Score must be within `[0-10]`
448    ///
449    /// Priority must be within `[0, 2]`
450    ///
451    /// Reread_value must be within `[0, 5]`
452    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        // Instead of clamping, be more verbose with errors so the user is more aware of the values
466        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    /// Use builder pattern for building up the query with required arguments
526    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    /// Create new `Delete my manga list item` query
641    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    // Common fields
677    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    // Detail specific fields
701    pictures,
702    background,
703    related_anime,
704    related_manga,
705    recommendations,
706    serialization,
707}
708
709/// Wrapper for a vector of valid Manga Common Fields
710#[derive(Debug)]
711pub struct MangaCommonFields(pub Vec<MangaField>);
712
713/// Wrapper for a vector of valid Manga Detail Fields
714#[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}