mal_api/anime/
requests.rs

1use serde::{Deserialize, Serialize};
2use strum_macros::EnumIter;
3
4use super::error::AnimeApiError;
5
6/// Corresponds to the [Get anime list](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_get) endpoint
7#[derive(Debug, Serialize)]
8pub struct GetAnimeList {
9    q: String,
10    nsfw: bool,
11    limit: u16,
12    offset: u32,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    fields: Option<String>,
15}
16
17impl GetAnimeList {
18    /// Create new `Get anime list` query
19    ///
20    /// Limit must be within `[1, 100]`. Defaults to 100
21    pub fn new<T: Into<String>>(
22        q: T,
23        nsfw: bool,
24        fields: Option<&AnimeCommonFields>,
25        limit: Option<u16>,
26        offset: Option<u32>,
27    ) -> Result<Self, AnimeApiError> {
28        let limit = limit.map(|l| l.clamp(1, 100));
29        let q: String = q.into();
30
31        if q.is_empty() {
32            return Err(AnimeApiError::new("Query cannot be empty".to_string()));
33        }
34
35        Ok(Self {
36            q,
37            nsfw,
38            limit: limit.unwrap_or(100),
39            offset: offset.unwrap_or(0),
40            fields: fields.map(|f| f.into()),
41        })
42    }
43
44    /// Use builder pattern for building up the query with required arguments
45    pub fn builder<T: Into<String>>(q: T) -> GetAnimeListBuilder<'static> {
46        GetAnimeListBuilder::new(q.into())
47    }
48}
49
50#[derive(Debug)]
51pub struct GetAnimeListBuilder<'a> {
52    q: String,
53    nsfw: bool,
54    limit: Option<u16>,
55    offset: Option<u32>,
56    fields: Option<&'a AnimeCommonFields>,
57}
58
59impl<'a> GetAnimeListBuilder<'a> {
60    pub fn new(q: String) -> Self {
61        Self {
62            q,
63            nsfw: false,
64            limit: None,
65            offset: None,
66            fields: None,
67        }
68    }
69
70    pub fn q<T: Into<String>>(mut self, value: T) -> Self {
71        self.q = value.into();
72        self
73    }
74
75    pub fn enable_nsfw(mut self) -> Self {
76        self.nsfw = true;
77        self
78    }
79
80    pub fn limit(mut self, value: u16) -> Self {
81        self.limit = Some(value.clamp(1, 100));
82        self
83    }
84
85    pub fn offset(mut self, value: u32) -> Self {
86        self.offset = Some(value);
87        self
88    }
89
90    pub fn fields(mut self, value: &'a AnimeCommonFields) -> Self {
91        self.fields = Some(value);
92        self
93    }
94
95    pub fn build(self) -> Result<GetAnimeList, AnimeApiError> {
96        GetAnimeList::new(self.q, self.nsfw, self.fields, self.limit, self.offset)
97    }
98}
99
100/// Corresponds to the [Get anime details](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get) endpoint
101#[derive(Debug, Serialize)]
102pub struct GetAnimeDetails {
103    #[serde(skip_serializing)]
104    pub(crate) anime_id: u32,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    fields: Option<String>,
107}
108
109impl GetAnimeDetails {
110    /// Create new `Get anime details` query
111    pub fn new(anime_id: u32, fields: Option<&AnimeDetailFields>) -> Result<Self, AnimeApiError> {
112        if anime_id == 0 {
113            return Err(AnimeApiError::new(
114                "anime_id must be greater than 0".to_string(),
115            ));
116        }
117
118        Ok(Self {
119            anime_id,
120            fields: fields.map(|f| f.into()),
121        })
122    }
123
124    /// Use builder pattern for building up the query with required arguments
125    pub fn builder(anime_id: u32) -> GetAnimeDetailsBuilder<'static> {
126        GetAnimeDetailsBuilder::new(anime_id)
127    }
128}
129
130pub struct GetAnimeDetailsBuilder<'a> {
131    anime_id: u32,
132    fields: Option<&'a AnimeDetailFields>,
133}
134
135impl<'a> GetAnimeDetailsBuilder<'a> {
136    pub fn new(anime_id: u32) -> Self {
137        Self {
138            anime_id,
139            fields: None,
140        }
141    }
142
143    pub fn anime_id(mut self, value: u32) -> Self {
144        self.anime_id = value;
145        self
146    }
147
148    pub fn fields(mut self, value: &'a AnimeDetailFields) -> Self {
149        self.fields = Some(value);
150        self
151    }
152
153    pub fn build(self) -> Result<GetAnimeDetails, AnimeApiError> {
154        GetAnimeDetails::new(self.anime_id, self.fields)
155    }
156}
157
158#[derive(Debug, Serialize, Deserialize, Clone)]
159#[serde(rename_all = "lowercase")]
160pub enum RankingType {
161    All,
162    Airing,
163    Upcoming,
164    Tv,
165    Ova,
166    Movie,
167    Special,
168    ByPopularity,
169    Favorite,
170}
171
172/// Corresponds to the [Get anime ranking](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_ranking_get) endpoint
173#[derive(Debug, Serialize)]
174pub struct GetAnimeRanking {
175    ranking_type: RankingType,
176    nsfw: bool,
177    limit: u16,
178    offset: u32,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    fields: Option<String>,
181}
182
183impl GetAnimeRanking {
184    /// Create a new `Get anime ranking` query
185    ///
186    /// Limit must be within `[1, 500]`. Defaults to 100
187    pub fn new(
188        ranking_type: RankingType,
189        nsfw: bool,
190        fields: Option<&AnimeCommonFields>,
191        limit: Option<u16>,
192        offset: Option<u32>,
193    ) -> Self {
194        let limit = limit.map(|l| l.clamp(1, 500));
195
196        Self {
197            ranking_type,
198            nsfw,
199            limit: limit.unwrap_or(100),
200            offset: offset.unwrap_or(0),
201            fields: fields.map(|f| f.into()),
202        }
203    }
204
205    /// Use builder pattern for building up the query with required arguments
206    pub fn builder(ranking_type: RankingType) -> GetAnimeRankingBuilder<'static> {
207        GetAnimeRankingBuilder::new(ranking_type)
208    }
209}
210
211pub struct GetAnimeRankingBuilder<'a> {
212    ranking_type: RankingType,
213    nsfw: bool,
214    limit: Option<u16>,
215    offset: Option<u32>,
216    fields: Option<&'a AnimeCommonFields>,
217}
218
219impl<'a> GetAnimeRankingBuilder<'a> {
220    pub fn new(ranking_type: RankingType) -> Self {
221        Self {
222            ranking_type,
223            nsfw: false,
224            limit: None,
225            offset: None,
226            fields: None,
227        }
228    }
229
230    pub fn ranking_type(mut self, value: RankingType) -> Self {
231        self.ranking_type = value;
232        self
233    }
234
235    pub fn enable_nsfw(mut self) -> Self {
236        self.nsfw = true;
237        self
238    }
239
240    pub fn limit(mut self, value: u16) -> Self {
241        self.limit = Some(value.clamp(1, 500));
242        self
243    }
244
245    pub fn offset(mut self, value: u32) -> Self {
246        self.offset = Some(value);
247        self
248    }
249
250    pub fn fields(mut self, value: &'a AnimeCommonFields) -> Self {
251        self.fields = Some(value.into());
252        self
253    }
254
255    pub fn build(self) -> GetAnimeRanking {
256        GetAnimeRanking::new(
257            self.ranking_type,
258            self.nsfw,
259            self.fields,
260            self.limit,
261            self.offset,
262        )
263    }
264}
265
266#[derive(Debug, Serialize, Deserialize, Clone)]
267#[serde(rename_all = "lowercase")]
268pub enum Season {
269    Winter,
270    Spring,
271    Summer,
272    Fall,
273}
274
275impl std::fmt::Display for Season {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        match self {
278            Self::Winter => {
279                write!(f, "winter")
280            }
281            Self::Fall => {
282                write!(f, "fall")
283            }
284            Self::Summer => {
285                write!(f, "summer")
286            }
287            Self::Spring => {
288                write!(f, "spring")
289            }
290        }
291    }
292}
293
294#[derive(Debug, Serialize, Clone)]
295#[serde(rename_all = "snake_case")]
296pub enum SeasonalAnimeSort {
297    AnimeScore,
298    AnimeNumListUsers,
299}
300
301/// Corresponds to the [Get seasonal anime](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_season_year_season_get) endpoint
302#[derive(Debug, Serialize)]
303pub struct GetSeasonalAnime {
304    #[serde(skip_serializing)]
305    pub(crate) year: u16,
306    #[serde(skip_serializing)]
307    pub(crate) season: Season,
308    nsfw: bool,
309    #[serde(skip_serializing_if = "Option::is_none")]
310    sort: Option<SeasonalAnimeSort>,
311    limit: u16,
312    offset: u32,
313    fields: Option<String>,
314}
315
316impl GetSeasonalAnime {
317    /// Create a new `Get seasonal anime` query
318    ///
319    /// Limit must be within `[1, 500]`
320    pub fn new(
321        year: u16,
322        season: Season,
323        nsfw: bool,
324        fields: Option<&AnimeCommonFields>,
325        sort: Option<SeasonalAnimeSort>,
326        limit: Option<u16>,
327        offset: Option<u32>,
328    ) -> Self {
329        let limit = limit.map(|l| l.clamp(1, 500));
330
331        Self {
332            year,
333            season,
334            nsfw,
335            sort,
336            limit: limit.unwrap_or(100),
337            offset: offset.unwrap_or(0),
338            fields: fields.map(|f| f.into()),
339        }
340    }
341
342    /// Use builder pattern for building up the query with required arguments
343    pub fn builder(year: u16, season: Season) -> GetSeasonalAnimeBuilder<'static> {
344        GetSeasonalAnimeBuilder::new(year, season)
345    }
346}
347
348pub struct GetSeasonalAnimeBuilder<'a> {
349    year: u16,
350    season: Season,
351    nsfw: bool,
352    sort: Option<SeasonalAnimeSort>,
353    limit: Option<u16>,
354    offset: Option<u32>,
355    fields: Option<&'a AnimeCommonFields>,
356}
357
358impl<'a> GetSeasonalAnimeBuilder<'a> {
359    pub fn new(year: u16, season: Season) -> Self {
360        Self {
361            year,
362            season,
363            nsfw: false,
364            sort: None,
365            limit: None,
366            offset: None,
367            fields: None,
368        }
369    }
370
371    pub fn year(mut self, value: u16) -> Self {
372        self.year = value;
373        self
374    }
375
376    pub fn season(mut self, value: Season) -> Self {
377        self.season = value;
378        self
379    }
380
381    pub fn enable_nsfw(mut self) -> Self {
382        self.nsfw = true;
383        self
384    }
385
386    pub fn sort(mut self, value: SeasonalAnimeSort) -> Self {
387        self.sort = Some(value);
388        self
389    }
390
391    pub fn limit(mut self, value: u16) -> Self {
392        self.limit = Some(value);
393        self
394    }
395
396    pub fn offset(mut self, value: u32) -> Self {
397        self.offset = Some(value);
398        self
399    }
400
401    pub fn fields(mut self, value: &'a AnimeCommonFields) -> Self {
402        self.fields = Some(value.into());
403        self
404    }
405
406    pub fn build(self) -> GetSeasonalAnime {
407        GetSeasonalAnime::new(
408            self.year,
409            self.season,
410            self.nsfw,
411            self.fields,
412            self.sort,
413            self.limit,
414            self.offset,
415        )
416    }
417}
418
419/// Corresponds to the [Get suggested anime](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_suggestions_get) endpoint
420#[derive(Debug, Serialize)]
421pub struct GetSuggestedAnime {
422    nsfw: bool,
423    limit: u16,
424    offset: u32,
425    #[serde(skip_serializing_if = "Option::is_none")]
426    fields: Option<String>,
427}
428
429impl GetSuggestedAnime {
430    /// Create a new `Get suggested anime` query
431    ///
432    /// Limit must be within `[1, 100]`. Defaults to 100
433    pub fn new(
434        nsfw: bool,
435        fields: Option<&AnimeCommonFields>,
436        limit: Option<u16>,
437        offset: Option<u32>,
438    ) -> Self {
439        let limit = limit.map(|l| l.clamp(1, 100));
440
441        Self {
442            nsfw,
443            limit: limit.unwrap_or(100),
444            offset: offset.unwrap_or(0),
445            fields: fields.map(|f| f.into()),
446        }
447    }
448
449    /// Use builder pattern for building up the query with required arguments
450    pub fn builder() -> GetSuggestedAnimeBuilder<'static> {
451        GetSuggestedAnimeBuilder::new()
452    }
453}
454
455pub struct GetSuggestedAnimeBuilder<'a> {
456    nsfw: bool,
457    fields: Option<&'a AnimeCommonFields>,
458    limit: Option<u16>,
459    offset: Option<u32>,
460}
461
462impl<'a> GetSuggestedAnimeBuilder<'a> {
463    pub fn new() -> Self {
464        Self {
465            nsfw: false,
466            fields: None,
467            limit: None,
468            offset: None,
469        }
470    }
471
472    pub fn enable_nsfw(mut self) -> Self {
473        self.nsfw = true;
474        self
475    }
476
477    pub fn fields(mut self, value: &'a AnimeCommonFields) -> Self {
478        self.fields = Some(value.into());
479        self
480    }
481
482    pub fn limit(mut self, value: u16) -> Self {
483        self.limit = Some(value.clamp(1, 100));
484        self
485    }
486
487    pub fn offset(mut self, value: u32) -> Self {
488        self.offset = Some(value);
489        self
490    }
491
492    pub fn build(self) -> GetSuggestedAnime {
493        GetSuggestedAnime::new(self.nsfw, self.fields, self.limit, self.offset)
494    }
495}
496
497#[derive(Debug, Serialize, Deserialize, Clone)]
498#[serde(rename_all = "snake_case")]
499pub enum UserAnimeListStatus {
500    Watching,
501    Completed,
502    OnHold,
503    Dropped,
504    PlanToWatch,
505}
506
507#[derive(Debug, Serialize)]
508#[serde(rename_all = "snake_case")]
509pub enum UserAnimeListSort {
510    ListScore,
511    ListUpdatedAt,
512    AnimeTitle,
513    AnimeStartDate,
514    // TODO: This sort option is still under development according to MAL API reference
515    // AnimeId,
516}
517
518/// Corresponds to the [Get user anime list](https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get) endpoint
519#[derive(Debug, Serialize)]
520pub struct GetUserAnimeList {
521    #[serde(skip_serializing)]
522    pub(crate) user_name: String,
523    nsfw: bool,
524    #[serde(skip_serializing_if = "Option::is_none")]
525    status: Option<UserAnimeListStatus>,
526    #[serde(skip_serializing_if = "Option::is_none")]
527    sort: Option<UserAnimeListSort>,
528    limit: u16,
529    offset: u32,
530    #[serde(skip_serializing_if = "Option::is_none")]
531    fields: Option<String>,
532}
533
534impl GetUserAnimeList {
535    /// Create a new `Get user anime list` query
536    ///
537    /// Limit must be within `[1, 1000]`. Defaults to 100
538    ///
539    /// Note: `user_name` should be the targets user name, or `@me` as a
540    /// shortcut for yourself. However, you can only use `@me` if you
541    /// have an `Oauth` client
542    pub fn new(
543        user_name: String,
544        nsfw: bool,
545        fields: Option<&AnimeCommonFields>,
546        status: Option<UserAnimeListStatus>,
547        sort: Option<UserAnimeListSort>,
548        limit: Option<u16>,
549        offset: Option<u32>,
550    ) -> Result<Self, AnimeApiError> {
551        let limit = limit.map(|l| l.clamp(1, 1000));
552
553        if user_name.is_empty() {
554            return Err(AnimeApiError::new("user_name cannot be empty".to_string()));
555        }
556
557        Ok(Self {
558            user_name,
559            nsfw,
560            status,
561            sort,
562            limit: limit.unwrap_or(100),
563            offset: offset.unwrap_or(0),
564            fields: fields.map(|f| f.into()),
565        })
566    }
567
568    /// Use builder pattern for building up the query with required arguments
569    pub fn builder(user_name: &str) -> GetUserAnimeListBuilder<'static> {
570        GetUserAnimeListBuilder::new(user_name.to_string())
571    }
572}
573
574pub struct GetUserAnimeListBuilder<'a> {
575    user_name: String,
576    nsfw: bool,
577    fields: Option<&'a AnimeCommonFields>,
578    status: Option<UserAnimeListStatus>,
579    sort: Option<UserAnimeListSort>,
580    limit: Option<u16>,
581    offset: Option<u32>,
582}
583
584impl<'a> GetUserAnimeListBuilder<'a> {
585    pub fn new<T: Into<String>>(user_name: T) -> Self {
586        let user_name = user_name.into();
587        Self {
588            user_name,
589            nsfw: false,
590            fields: None,
591            status: None,
592            sort: None,
593            limit: None,
594            offset: None,
595        }
596    }
597
598    pub fn user_name<T: Into<String>>(mut self, value: T) -> Self {
599        self.user_name = value.into();
600        self
601    }
602
603    pub fn enable_nsfw(mut self) -> Self {
604        self.nsfw = true;
605        self
606    }
607
608    pub fn fields(mut self, value: &'a AnimeCommonFields) -> Self {
609        self.fields = Some(value.into());
610        self
611    }
612
613    pub fn status(mut self, value: UserAnimeListStatus) -> Self {
614        self.status = Some(value);
615        self
616    }
617
618    pub fn sort(mut self, value: UserAnimeListSort) -> Self {
619        self.sort = Some(value);
620        self
621    }
622
623    pub fn limit(mut self, value: u16) -> Self {
624        self.limit = Some(value.clamp(1, 1000));
625        self
626    }
627
628    pub fn offset(mut self, value: u32) -> Self {
629        self.offset = Some(value);
630        self
631    }
632
633    pub fn build(self) -> Result<GetUserAnimeList, AnimeApiError> {
634        GetUserAnimeList::new(
635            self.user_name,
636            self.nsfw,
637            self.fields,
638            self.status,
639            self.sort,
640            self.limit,
641            self.offset,
642        )
643    }
644}
645
646/// Corresponds to the [Update my anime list status](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_my_list_status_put) endpoint
647#[derive(Debug, Serialize)]
648pub struct UpdateMyAnimeListStatus {
649    #[serde(skip_serializing)]
650    pub(crate) anime_id: u32,
651    #[serde(skip_serializing_if = "Option::is_none")]
652    status: Option<UserAnimeListStatus>,
653    #[serde(skip_serializing_if = "Option::is_none")]
654    is_rewatching: Option<bool>,
655    #[serde(skip_serializing_if = "Option::is_none")]
656    score: Option<u8>,
657    #[serde(skip_serializing_if = "Option::is_none")]
658    num_watched_episodes: Option<u32>,
659    #[serde(skip_serializing_if = "Option::is_none")]
660    priority: Option<u8>,
661    #[serde(skip_serializing_if = "Option::is_none")]
662    num_times_rewatched: Option<u32>,
663    #[serde(skip_serializing_if = "Option::is_none")]
664    rewatch_value: Option<u8>,
665    #[serde(skip_serializing_if = "Option::is_none")]
666    tags: Option<String>,
667    #[serde(skip_serializing_if = "Option::is_none")]
668    comments: Option<String>,
669}
670
671impl UpdateMyAnimeListStatus {
672    /// Create new `Update my anime list status` query
673    ///
674    /// Score must be within `[0, 10]`
675    ///
676    /// Priority must be within `[0, 2]`
677    ///
678    /// Rewatch_value must be within `[0, 5]`
679    pub fn new(
680        anime_id: u32,
681        status: Option<UserAnimeListStatus>,
682        is_rewatching: Option<bool>,
683        score: Option<u8>,
684        num_watched_episodes: Option<u32>,
685        priority: Option<u8>,
686        num_times_rewatched: Option<u32>,
687        rewatch_value: Option<u8>,
688        tags: Option<String>,
689        comments: Option<String>,
690    ) -> Result<Self, AnimeApiError> {
691        // Instead of clamping, be more verbose with errors so the user is more aware of the values
692        if let Some(score) = score {
693            if score > 10 {
694                return Err(AnimeApiError::new(
695                    "Score must be between 0 and 10 inclusive".to_string(),
696                ));
697            }
698        }
699        if let Some(priority) = priority {
700            if priority > 2 {
701                return Err(AnimeApiError::new(
702                    "Priority must be between 0 and 2 inclusive".to_string(),
703                ));
704            }
705        }
706        if let Some(rewatch_value) = rewatch_value {
707            if rewatch_value > 5 {
708                return Err(AnimeApiError::new(
709                    "Rewatch value must be between 0 and 5 inclusive".to_string(),
710                ));
711            }
712        }
713
714        if anime_id == 0 {
715            return Err(AnimeApiError::new(
716                "anime_id must be greater than 0".to_string(),
717            ));
718        }
719
720        // TODO: Abstract this logic to make it re-useable
721        if !(status.is_some()
722            || is_rewatching.is_some()
723            || score.is_some()
724            || num_watched_episodes.is_some()
725            || priority.is_some()
726            || num_times_rewatched.is_some()
727            || rewatch_value.is_some()
728            || tags.is_some()
729            || comments.is_some())
730        {
731            return Err(AnimeApiError::new(
732                "At least one of the optional arguments must be Some".to_string(),
733            ));
734        }
735
736        Ok(Self {
737            anime_id,
738            status,
739            is_rewatching,
740            score,
741            num_watched_episodes,
742            priority,
743            num_times_rewatched,
744            rewatch_value,
745            tags,
746            comments,
747        })
748    }
749
750    /// Use builder pattern for building up the query with required arguments
751    pub fn builder(anime_id: u32) -> UpdateMyAnimeListStatusBuilder {
752        UpdateMyAnimeListStatusBuilder::new(anime_id)
753    }
754}
755
756pub struct UpdateMyAnimeListStatusBuilder {
757    anime_id: u32,
758    status: Option<UserAnimeListStatus>,
759    is_rewatching: Option<bool>,
760    score: Option<u8>,
761    num_watched_episodes: Option<u32>,
762    priority: Option<u8>,
763    num_times_rewatched: Option<u32>,
764    rewatch_value: Option<u8>,
765    tags: Option<String>,
766    comments: Option<String>,
767}
768
769impl UpdateMyAnimeListStatusBuilder {
770    pub fn new(anime_id: u32) -> Self {
771        Self {
772            anime_id,
773            status: None,
774            is_rewatching: None,
775            score: None,
776            num_watched_episodes: None,
777            priority: None,
778            num_times_rewatched: None,
779            rewatch_value: None,
780            tags: None,
781            comments: None,
782        }
783    }
784
785    pub fn anime_id(mut self, value: u32) -> Self {
786        self.anime_id = value;
787        self
788    }
789
790    pub fn status(mut self, value: UserAnimeListStatus) -> Self {
791        self.status = Some(value);
792        self
793    }
794
795    pub fn is_rewatching(mut self, value: bool) -> Self {
796        self.is_rewatching = Some(value);
797        self
798    }
799
800    pub fn score(mut self, value: u8) -> Self {
801        self.score = Some(value);
802        self
803    }
804
805    pub fn num_watched_episodes(mut self, value: u32) -> Self {
806        self.num_watched_episodes = Some(value);
807        self
808    }
809
810    pub fn priority(mut self, value: u8) -> Self {
811        self.priority = Some(value);
812        self
813    }
814
815    pub fn num_times_rewatched(mut self, value: u32) -> Self {
816        self.num_times_rewatched = Some(value);
817        self
818    }
819
820    pub fn rewatch_value(mut self, value: u8) -> Self {
821        self.rewatch_value = Some(value);
822        self
823    }
824
825    pub fn tags(mut self, value: &str) -> Self {
826        self.tags = Some(value.to_string());
827        self
828    }
829
830    pub fn comments(mut self, value: &str) -> Self {
831        self.comments = Some(value.to_string());
832        self
833    }
834
835    pub fn build(self) -> Result<UpdateMyAnimeListStatus, AnimeApiError> {
836        UpdateMyAnimeListStatus::new(
837            self.anime_id,
838            self.status,
839            self.is_rewatching,
840            self.score,
841            self.num_watched_episodes,
842            self.priority,
843            self.num_times_rewatched,
844            self.rewatch_value,
845            self.tags,
846            self.comments,
847        )
848    }
849}
850
851/// Corresponds to the [Delete my anime list item](https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_my_list_status_delete) endpoint
852#[derive(Debug)]
853pub struct DeleteMyAnimeListItem {
854    pub(crate) anime_id: u32,
855}
856
857impl DeleteMyAnimeListItem {
858    /// Create new `Delete my anime list item` query
859    pub fn new(anime_id: u32) -> Self {
860        Self { anime_id }
861    }
862}
863
864#[derive(Debug, EnumIter, PartialEq)]
865#[allow(non_camel_case_types)]
866pub enum AnimeField {
867    id,
868    title,
869    main_picture,
870    alternative_titles,
871    start_date,
872    end_date,
873    synopsis,
874    mean,
875    rank,
876    popularity,
877    num_list_users,
878    num_scoring_users,
879    nsfw,
880    genres,
881    created_at,
882    updated_at,
883    media_type,
884    status,
885    my_list_status,
886    num_episodes,
887    start_season,
888    broadcast,
889    source,
890    average_episode_duration,
891    rating,
892    studios,
893}
894
895#[derive(Debug, EnumIter, PartialEq)]
896#[allow(non_camel_case_types)]
897pub enum AnimeDetail {
898    // Common fields
899    id,
900    title,
901    main_picture,
902    alternative_titles,
903    start_date,
904    end_date,
905    synopsis,
906    mean,
907    rank,
908    popularity,
909    num_list_users,
910    num_scoring_users,
911    nsfw,
912    genres,
913    created_at,
914    updated_at,
915    media_type,
916    status,
917    my_list_status,
918    num_episodes,
919    start_season,
920    broadcast,
921    source,
922    average_episode_duration,
923    rating,
924    studios,
925
926    // These are the fields specific to AnimeDetails
927    pictures,
928    background,
929    related_anime,
930    related_manga,
931    recommendations,
932    statistics,
933}
934
935/// Wrapper for a vector of valid Anime Common Fields
936#[derive(Debug)]
937pub struct AnimeCommonFields(pub Vec<AnimeField>);
938
939/// Wrapper for a vector of valid Anime Detail Fields
940#[derive(Debug)]
941pub struct AnimeDetailFields(pub Vec<AnimeDetail>);
942
943impl<'a> Into<String> for &'a AnimeCommonFields {
944    fn into(self) -> String {
945        let result = self
946            .0
947            .iter()
948            .map(|e| format!("{:?}", e))
949            .collect::<Vec<String>>()
950            .join(",");
951        result
952    }
953}
954
955impl<'a> Into<String> for &'a AnimeDetailFields {
956    fn into(self) -> String {
957        let result = self
958            .0
959            .iter()
960            .map(|e| format!("{:?}", e))
961            .collect::<Vec<String>>()
962            .join(",");
963        result
964    }
965}
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970    use crate::anime::all_common_fields;
971
972    #[test]
973    fn test_get_anime_list() {
974        let fields = all_common_fields();
975        let query = GetAnimeList::new("".to_string(), false, Some(&fields), Some(100), None);
976        assert!(query.is_err());
977
978        let query = GetAnimeList::new("one".to_string(), false, Some(&fields), Some(999), None);
979        assert!(query.is_ok());
980
981        let query = GetAnimeList::new("one".to_string(), false, Some(&fields), Some(0), None);
982        assert_eq!(query.unwrap().limit, 1);
983
984        let query = GetAnimeList::new("one".to_string(), false, Some(&fields), Some(50), None);
985        assert_eq!(query.unwrap().limit, 50);
986
987        let query = GetAnimeList::new("one".to_string(), false, Some(&fields), None, None);
988        assert!(&query.is_ok());
989        assert_eq!(query.unwrap().limit, 100);
990    }
991
992    #[test]
993    fn test_get_anime_ranking() {
994        let fields = all_common_fields();
995        let query = GetAnimeRanking::new(RankingType::All, false, Some(&fields), Some(1000), None);
996        assert_eq!(query.limit, 500);
997
998        let query = GetAnimeRanking::new(RankingType::All, false, Some(&fields), Some(0), None);
999        assert_eq!(query.limit, 1);
1000
1001        let query = GetAnimeRanking::new(RankingType::All, false, Some(&fields), Some(500), None);
1002        assert_eq!(query.limit, 500);
1003
1004        let query = GetAnimeRanking::new(RankingType::All, false, Some(&fields), None, None);
1005        assert_eq!(query.limit, 100);
1006    }
1007
1008    #[test]
1009    fn test_get_seasonal_anime() {
1010        let fields = all_common_fields();
1011        let query = GetSeasonalAnime::new(
1012            1000,
1013            Season::Spring,
1014            false,
1015            Some(&fields),
1016            Some(SeasonalAnimeSort::AnimeScore),
1017            Some(999),
1018            None,
1019        );
1020        assert_eq!(query.limit, 500);
1021
1022        let query = GetSeasonalAnime::new(
1023            1000,
1024            Season::Spring,
1025            false,
1026            Some(&fields),
1027            Some(SeasonalAnimeSort::AnimeScore),
1028            Some(0),
1029            None,
1030        );
1031        assert_eq!(query.limit, 1);
1032
1033        let query = GetSeasonalAnime::new(
1034            1000,
1035            Season::Spring,
1036            false,
1037            Some(&fields),
1038            Some(SeasonalAnimeSort::AnimeScore),
1039            Some(500),
1040            None,
1041        );
1042        assert_eq!(query.limit, 500);
1043    }
1044
1045    #[test]
1046    fn test_get_suggested_anime() {
1047        let fields = all_common_fields();
1048        let query = GetSuggestedAnime::new(false, Some(&fields), Some(500), None);
1049        assert_eq!(query.limit, 100);
1050
1051        let query = GetSuggestedAnime::new(false, Some(&fields), Some(0), None);
1052        assert_eq!(query.limit, 1);
1053
1054        let query = GetSuggestedAnime::new(false, Some(&fields), Some(10), None);
1055        assert_eq!(query.limit, 10);
1056
1057        let query = GetSuggestedAnime::new(false, Some(&fields), None, None);
1058        assert_eq!(query.limit, 100);
1059    }
1060
1061    #[test]
1062    fn test_get_user_anime_list() {
1063        let fields = all_common_fields();
1064        let query = GetUserAnimeList::new(
1065            "".to_string(),
1066            false,
1067            Some(&fields),
1068            Some(UserAnimeListStatus::Completed),
1069            Some(UserAnimeListSort::AnimeTitle),
1070            Some(1001),
1071            None,
1072        );
1073        assert!(query.is_err());
1074
1075        let query = GetUserAnimeList::new(
1076            "hello".to_string(),
1077            false,
1078            Some(&fields),
1079            Some(UserAnimeListStatus::Completed),
1080            Some(UserAnimeListSort::AnimeTitle),
1081            Some(0),
1082            None,
1083        );
1084        assert!(&query.is_ok());
1085        assert_eq!(query.unwrap().limit, 1);
1086
1087        let query = GetUserAnimeList::new(
1088            "hello".to_string(),
1089            false,
1090            Some(&fields),
1091            Some(UserAnimeListStatus::Completed),
1092            Some(UserAnimeListSort::AnimeTitle),
1093            Some(1000),
1094            None,
1095        );
1096        assert!(query.is_ok());
1097
1098        let query = GetUserAnimeList::new(
1099            "hello".to_string(),
1100            false,
1101            Some(&fields),
1102            Some(UserAnimeListStatus::Completed),
1103            Some(UserAnimeListSort::AnimeTitle),
1104            None,
1105            None,
1106        );
1107        assert!(query.is_ok());
1108        assert_eq!(query.unwrap().limit, 100);
1109    }
1110
1111    #[test]
1112    fn test_update_my_anime_list() {
1113        let query = UpdateMyAnimeListStatus::new(
1114            1234, None, None, None, None, None, None, None, None, None,
1115        );
1116        assert!(query.is_err());
1117
1118        let query = UpdateMyAnimeListStatus::new(
1119            1234,
1120            Some(UserAnimeListStatus::Dropped),
1121            None,
1122            Some(11),
1123            None,
1124            None,
1125            None,
1126            None,
1127            None,
1128            None,
1129        );
1130        assert!(query.is_err());
1131
1132        let query = UpdateMyAnimeListStatus::new(
1133            1234,
1134            Some(UserAnimeListStatus::Dropped),
1135            None,
1136            None,
1137            None,
1138            Some(3),
1139            None,
1140            None,
1141            None,
1142            None,
1143        );
1144        assert!(query.is_err());
1145
1146        let query = UpdateMyAnimeListStatus::new(
1147            1234,
1148            Some(UserAnimeListStatus::Dropped),
1149            None,
1150            None,
1151            None,
1152            None,
1153            None,
1154            Some(6),
1155            None,
1156            None,
1157        );
1158        assert!(query.is_err());
1159
1160        let query = UpdateMyAnimeListStatus::new(
1161            1234,
1162            Some(UserAnimeListStatus::Completed),
1163            None,
1164            Some(10),
1165            None,
1166            Some(2),
1167            None,
1168            Some(5),
1169            None,
1170            None,
1171        );
1172        assert!(query.is_ok());
1173    }
1174}