modio/types/
mods.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use serde::de::{Deserialize, Deserializer, IgnoredAny, MapAccess, SeqAccess, Visitor};
5use serde_derive::Deserialize;
6use url::Url;
7
8use super::files::File;
9use super::id::{CommentId, EventId, FileId, GameId, MemberId, ModId, ResourceId, UserId};
10use super::{deserialize_empty_object, utils, DeserializeField, MissingField, TargetPlatform};
11use super::{Logo, Status, Timestamp, User};
12
13/// See the [Mod Object](https://docs.mod.io/restapiref/#mod-object) docs for more information.
14#[derive(Debug, Deserialize)]
15#[non_exhaustive]
16pub struct Mod {
17    pub id: ModId,
18    pub game_id: GameId,
19    pub status: Status,
20    pub visible: Visibility,
21    pub submitted_by: User,
22    pub date_added: Timestamp,
23    pub date_updated: Timestamp,
24    pub date_live: Timestamp,
25    pub maturity_option: MaturityOption,
26    pub community_options: CommunityOptions,
27    pub credit_options: CreditOptions,
28    pub price: f32,
29    pub tax: u32,
30    pub logo: Logo,
31    #[serde(with = "utils::url::opt")]
32    pub homepage_url: Option<Url>,
33    pub name: String,
34    pub name_id: String,
35    pub summary: String,
36    pub description: Option<String>,
37    pub description_plaintext: Option<String>,
38    pub metadata_blob: Option<String>,
39    #[serde(with = "utils::url")]
40    pub profile_url: Url,
41    #[serde(default, deserialize_with = "deserialize_empty_object")]
42    pub modfile: Option<File>,
43    pub media: Media,
44    #[serde(rename = "metadata_kvp")]
45    pub metadata: MetadataMap,
46    pub tags: Vec<Tag>,
47    pub dependencies: bool,
48    pub stats: Statistics,
49    pub platforms: Vec<Platform>,
50}
51
52newtype_enum! {
53    /// See [Status & Visibility](https://docs.mod.io/restapiref/#status-amp-visibility) docs for more information.
54    pub struct Visibility: u8 {
55        const HIDDEN = 0;
56        const PUBLIC = 1;
57    }
58}
59
60bitflags! {
61    /// Community options a mod can enable.
62    pub struct CommunityOptions: u16 {
63        /// Comments enabled.
64        const COMMENTS = 1;
65        /// Previews enabled.
66        const PREVIEWS = 64;
67        /// Preview URLs enabled.
68        const PREVIEW_URLS = 128;
69        /// Allow mod dependencies
70        const ALLOW_DEPENDENCIES = 1024;
71    }
72
73    /// Maturity options a mod can be flagged.
74    ///
75    /// This is only relevant if the parent game allows mods to be labelled as mature.
76    pub struct MaturityOption: u8 {
77        const ALCOHOL   = 1;
78        const DRUGS     = 2;
79        const VIOLENCE  = 4;
80        const EXPLICIT  = 8;
81    }
82
83    /// Credit options a mod can enable.
84    pub struct CreditOptions: u16 {
85        const SHOW_CREDITS_SECTION               = 1;
86        /// Mark with original or permitted assets.
87        const MARK_WITH_ORIGNAL_PERMITTED_ASSETS = 2;
88        const ALLOW_REDISTRIBUTION_WITH_CREDIT   = 4;
89        const ALLOW_PORTING_WITH_CREDIT          = 8;
90        const ALLOW_PATCHING_WITHOUT_CREDIT      = 16;
91        const ALLOW_PATCHING_WITH_CREDIT         = 32;
92        const ALLOW_PATCHING_WITH_PERMISSION     = 64;
93        const ALLOW_REPACKING_WITHOUT_CREDIT     = 128;
94        const ALLOW_REPACKING_WITH_CREDIT        = 256;
95        const ALLOW_REPACKING_WITH_PERMISSION    = 512;
96        const ALLOW_USERS_TO_RESELL              = 1024;
97    }
98}
99
100/// See the [Mod Event Object](https://docs.mod.io/restapiref/#mod-event-object) docs for more information.
101#[derive(Debug, Deserialize)]
102#[non_exhaustive]
103pub struct Event {
104    pub id: EventId,
105    pub mod_id: ModId,
106    pub user_id: UserId,
107    pub date_added: Timestamp,
108    pub event_type: EventType,
109}
110
111newtype_enum! {
112    /// Type of mod event that was triggered.
113    #[derive(Deserialize)]
114    #[serde(transparent)]
115    pub struct EventType<24> {
116        /// Primary file changed, the mod should be updated.
117        const MODFILE_CHANGED     = b"MODFILE_CHANGED";
118        /// Mod is marked as accepted and public.
119        const MOD_AVAILABLE       = b"MOD_AVAILABLE";
120        /// Mod is marked as not accepted, deleted or hidden.
121        const MOD_UNAVAILABLE     = b"MOD_UNAVAILABLE";
122        /// Mod has been updated.
123        const MOD_EDITED          = b"MOD_EDITED";
124        /// Mod has been permanently deleted.
125        const MOD_DELETED         = b"MOD_DELETED";
126        /// User has joined or left the mod team.
127        const MOD_TEAM_CHANGED    = b"MOD_TEAM_CHANGED";
128        /// A comment has been published for a mod.
129        const MOD_COMMENT_ADDED   = b"MOD_COMMENT_ADDED";
130        /// A comment has been deleted from a mod.
131        const MOD_COMMENT_DELETED = b"MOD_COMMENT_DELETED";
132    }
133}
134
135impl fmt::Display for EventType {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.write_str(self.as_str())
138    }
139}
140
141/// See the [Mod Dependency Object](https://docs.mod.io/restapiref/#mod-dependencies-object) docs for more
142/// information.
143#[derive(Debug, Deserialize)]
144#[non_exhaustive]
145pub struct Dependency {
146    pub mod_id: ModId,
147    pub date_added: Timestamp,
148}
149
150/// See the [Mod Media Object](https://docs.mod.io/restapiref/#mod-media-object) docs for more
151/// information.
152#[derive(Debug, Deserialize)]
153#[non_exhaustive]
154pub struct Media {
155    #[serde(default = "Vec::new")]
156    pub youtube: Vec<String>,
157    #[serde(default = "Vec::new")]
158    pub sketchfab: Vec<String>,
159    #[serde(default = "Vec::new")]
160    pub images: Vec<Image>,
161}
162
163/// See the [Image Object](https://docs.mod.io/restapiref/#image-object) docs for more information.
164#[derive(Deserialize)]
165#[non_exhaustive]
166pub struct Image {
167    pub filename: String,
168    #[serde(with = "utils::url")]
169    pub original: Url,
170    #[serde(with = "utils::url")]
171    pub thumb_320x180: Url,
172}
173
174impl fmt::Debug for Image {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        f.debug_struct("Image")
177            .field("filename", &self.filename)
178            .field("original", &self.original.as_str())
179            .field("thumb_320x180", &self.thumb_320x180.as_str())
180            .finish()
181    }
182}
183
184/// See the [Statistics Object](https://docs.mod.io/restapiref/#mod-stats-object) docs for more
185/// information.
186#[derive(Debug)]
187#[non_exhaustive]
188pub struct Statistics {
189    pub mod_id: ModId,
190    pub downloads_today: u32,
191    pub downloads_total: u32,
192    pub subscribers_total: u32,
193    pub popularity: Popularity,
194    pub ratings: Ratings,
195    pub date_expires: Timestamp,
196}
197
198impl<'de> Deserialize<'de> for Statistics {
199    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
200        #[derive(Deserialize)]
201        #[serde(field_identifier, rename_all = "snake_case")]
202        enum Field {
203            ModId,
204            DownloadsToday,
205            DownloadsTotal,
206            SubscribersTotal,
207            PopularityRankPosition,
208            PopularityRankTotalMods,
209            RatingsTotal,
210            RatingsPositive,
211            RatingsNegative,
212            RatingsPercentagePositive,
213            RatingsWeightedAggregate,
214            RatingsDisplayText,
215            DateExpires,
216            #[allow(dead_code)]
217            Other(String),
218        }
219
220        struct StatisticsVisitor;
221
222        impl<'de> Visitor<'de> for StatisticsVisitor {
223            type Value = Statistics;
224
225            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
226                formatter.write_str("struct Statistics")
227            }
228
229            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
230                let mut mod_id = None;
231                let mut downloads_today = None;
232                let mut downloads_total = None;
233                let mut subscribers_total = None;
234                let mut rank_position = None;
235                let mut rank_total = None;
236                let mut ratings_total = None;
237                let mut ratings_positive = None;
238                let mut ratings_negative = None;
239                let mut ratings_percentage_positive = None;
240                let mut ratings_weighted_aggregate = None;
241                let mut ratings_display_text = None;
242                let mut date_expires = None;
243
244                while let Some(key) = map.next_key()? {
245                    match key {
246                        Field::ModId => {
247                            mod_id.deserialize_value("mod_id", &mut map)?;
248                        }
249                        Field::DownloadsToday => {
250                            downloads_today.deserialize_value("downloads_today", &mut map)?;
251                        }
252                        Field::DownloadsTotal => {
253                            downloads_total.deserialize_value("downloads_total", &mut map)?;
254                        }
255                        Field::SubscribersTotal => {
256                            subscribers_total.deserialize_value("subscribers_total", &mut map)?;
257                        }
258                        Field::PopularityRankPosition => {
259                            rank_position
260                                .deserialize_value("popularity_rank_position", &mut map)?;
261                        }
262                        Field::PopularityRankTotalMods => {
263                            rank_total.deserialize_value("popularity_rank_total_mods", &mut map)?;
264                        }
265                        Field::RatingsTotal => {
266                            ratings_total.deserialize_value("ratings_total", &mut map)?;
267                        }
268                        Field::RatingsPositive => {
269                            ratings_positive.deserialize_value("ratings_positive", &mut map)?;
270                        }
271                        Field::RatingsNegative => {
272                            ratings_negative.deserialize_value("ratings_negative", &mut map)?;
273                        }
274                        Field::RatingsPercentagePositive => {
275                            ratings_percentage_positive
276                                .deserialize_value("ratings_percentage_positive", &mut map)?;
277                        }
278                        Field::RatingsWeightedAggregate => {
279                            ratings_weighted_aggregate
280                                .deserialize_value("ratings_weighted_aggregate", &mut map)?;
281                        }
282                        Field::RatingsDisplayText => {
283                            ratings_display_text
284                                .deserialize_value("ratings_display_text", &mut map)?;
285                        }
286                        Field::DateExpires => {
287                            date_expires.deserialize_value("date_expires", &mut map)?;
288                        }
289                        Field::Other(_) => {
290                            map.next_value::<IgnoredAny>()?;
291                        }
292                    }
293                }
294
295                let mod_id = mod_id.missing_field("mod_id")?;
296                let downloads_today = downloads_today.missing_field("downloads_today")?;
297                let downloads_total = downloads_total.missing_field("downloads_total")?;
298                let subscribers_total = subscribers_total.missing_field("subscribers_total")?;
299                let rank_position = rank_position.missing_field("popularity_rank_position")?;
300                let rank_total = rank_total.missing_field("popularity_rank_total_mods")?;
301                let ratings_total = ratings_total.missing_field("ratings_total")?;
302                let ratings_positive = ratings_positive.missing_field("ratings_positive")?;
303                let ratings_negative = ratings_negative.missing_field("ratings_negative")?;
304                let ratings_percentage_positive =
305                    ratings_percentage_positive.missing_field("ratings_percentage_positive")?;
306                let ratings_weighted_aggregate =
307                    ratings_weighted_aggregate.missing_field("ratings_weighted_aggregate")?;
308                let ratings_display_text =
309                    ratings_display_text.missing_field("ratings_display_text")?;
310                let date_expires = date_expires.missing_field("date_expires")?;
311
312                Ok(Statistics {
313                    mod_id,
314                    downloads_today,
315                    downloads_total,
316                    subscribers_total,
317                    popularity: Popularity {
318                        rank_position,
319                        rank_total,
320                    },
321                    ratings: Ratings {
322                        total: ratings_total,
323                        positive: ratings_positive,
324                        negative: ratings_negative,
325                        percentage_positive: ratings_percentage_positive,
326                        weighted_aggregate: ratings_weighted_aggregate,
327                        display_text: ratings_display_text,
328                    },
329                    date_expires,
330                })
331            }
332        }
333
334        deserializer.deserialize_map(StatisticsVisitor)
335    }
336}
337
338/// Part of [`Statistics`]
339#[derive(Debug)]
340#[non_exhaustive]
341pub struct Popularity {
342    pub rank_position: u32,
343    pub rank_total: u32,
344}
345
346/// Part of [`Statistics`]
347#[derive(Debug)]
348#[non_exhaustive]
349pub struct Ratings {
350    pub total: u32,
351    pub positive: u32,
352    pub negative: u32,
353    pub percentage_positive: u32,
354    pub weighted_aggregate: f32,
355    pub display_text: String,
356}
357
358/// See the [Rating Object](https://docs.mod.io/restapiref/#rating-object) docs for more information.
359#[derive(Debug)]
360#[non_exhaustive]
361pub enum Rating {
362    Positive {
363        game_id: GameId,
364        mod_id: ModId,
365        date_added: Timestamp,
366    },
367    Negative {
368        game_id: GameId,
369        mod_id: ModId,
370        date_added: Timestamp,
371    },
372}
373
374impl<'de> Deserialize<'de> for Rating {
375    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
376    where
377        D: Deserializer<'de>,
378    {
379        use serde::de::Error;
380
381        #[derive(Deserialize)]
382        struct R {
383            game_id: GameId,
384            mod_id: ModId,
385            rating: i8,
386            date_added: Timestamp,
387        }
388
389        match R::deserialize(deserializer) {
390            Ok(R {
391                game_id,
392                mod_id,
393                rating: 1,
394                date_added,
395            }) => Ok(Self::Positive {
396                game_id,
397                mod_id,
398                date_added,
399            }),
400            Ok(R {
401                game_id,
402                mod_id,
403                rating: -1,
404                date_added,
405            }) => Ok(Self::Negative {
406                game_id,
407                mod_id,
408                date_added,
409            }),
410            Ok(R { rating, .. }) => {
411                Err(D::Error::custom(format!("invalid rating value: {rating}")))
412            }
413            Err(e) => Err(e),
414        }
415    }
416}
417
418/// See the [Mod Platforms Object](https://docs.mod.io/restapiref/#mod-platforms-object) docs for more information.
419#[derive(Debug, Deserialize)]
420#[non_exhaustive]
421pub struct Platform {
422    #[serde(rename = "platform")]
423    pub target: TargetPlatform,
424    /// The unique id of the modfile that is currently live on the platform specified in the
425    /// `target` field.
426    #[serde(rename = "modfile_live")]
427    pub modfile_id: FileId,
428}
429
430/// See the [Mod Tag Object](https://docs.mod.io/restapiref/#mod-tag-object) docs for more information.
431#[derive(Debug, Deserialize)]
432#[non_exhaustive]
433pub struct Tag {
434    pub name: String,
435    pub date_added: Timestamp,
436}
437
438impl fmt::Display for Tag {
439    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
440        self.name.fmt(fmt)
441    }
442}
443
444/// See the [Metadata KVP Object](https://docs.mod.io/restapiref/#metadata-kvp-object) docs for more
445/// information.
446#[derive(Debug, Clone, Default, PartialEq)]
447pub struct MetadataMap(HashMap<String, Vec<String>>);
448
449impl MetadataMap {
450    pub fn new() -> Self {
451        Self::default()
452    }
453
454    pub fn with_capacity(capacity: usize) -> Self {
455        Self(HashMap::with_capacity(capacity))
456    }
457}
458
459impl std::ops::Deref for MetadataMap {
460    type Target = HashMap<String, Vec<String>>;
461
462    fn deref(&self) -> &Self::Target {
463        &self.0
464    }
465}
466
467impl std::ops::DerefMut for MetadataMap {
468    fn deref_mut(&mut self) -> &mut Self::Target {
469        &mut self.0
470    }
471}
472
473/// Deserialize a sequence of key-value objects to a `MetadataMap`.
474///
475/// ## Input
476///
477/// ```json
478/// [
479///     {"metakey": "pistol-dmg", "metavalue": "800"},
480///     {"metakey": "smg-dmg", "metavalue": "1200"},
481///     {"metakey": "pistol-dmg", "metavalue": "850"}
482/// ]
483/// ```
484///
485/// ## Result
486///
487/// ```text
488/// MetadataMap({
489///     "pistol-dmg": [
490///         "800",
491///         "850",
492///     ],
493///     "smg-dmg": [
494///         "1200",
495///     ],
496/// })
497/// ```
498impl<'de> Deserialize<'de> for MetadataMap {
499    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
500        #[derive(Debug)]
501        enum ListField {
502            Data,
503            Other,
504        }
505
506        impl<'de> Deserialize<'de> for ListField {
507            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
508                struct FieldVisitor;
509
510                impl Visitor<'_> for FieldVisitor {
511                    type Value = ListField;
512
513                    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
514                        formatter.write_str("`data` field")
515                    }
516
517                    fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
518                        match value {
519                            "data" => Ok(ListField::Data),
520                            _ => Ok(ListField::Other),
521                        }
522                    }
523                }
524
525                deserializer.deserialize_identifier(FieldVisitor)
526            }
527        }
528
529        struct MetadataVisitor;
530
531        impl<'de> Visitor<'de> for MetadataVisitor {
532            type Value = MetadataMap;
533
534            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
535                formatter.write_str("metadata kvp")
536            }
537
538            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
539                let mut data = None;
540
541                while let Some(key) = map.next_key()? {
542                    match key {
543                        ListField::Data => {
544                            data.deserialize_value("data", &mut map)?;
545                        }
546                        ListField::Other => {
547                            map.next_value::<IgnoredAny>()?;
548                        }
549                    }
550                }
551
552                let map = data.missing_field("data")?;
553                Ok(map)
554            }
555
556            fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
557                #[derive(Deserialize)]
558                struct Element {
559                    metakey: String,
560                    metavalue: String,
561                }
562
563                let mut map = seq
564                    .size_hint()
565                    .map_or_else(MetadataMap::new, MetadataMap::with_capacity);
566
567                while let Some(Element { metakey, metavalue }) = seq.next_element()? {
568                    map.entry(metakey).or_default().push(metavalue);
569                }
570
571                Ok(map)
572            }
573        }
574
575        deserializer.deserialize_any(MetadataVisitor)
576    }
577}
578
579/// See the [Comment Object](https://docs.mod.io/restapiref/#comment-object) docs for more information.
580#[derive(Debug, Deserialize)]
581#[non_exhaustive]
582pub struct Comment {
583    pub id: CommentId,
584    pub resource_id: ResourceId,
585    pub user: User,
586    pub date_added: Timestamp,
587    pub reply_id: CommentId,
588    pub thread_position: String,
589    pub karma: i32,
590    pub content: String,
591}
592
593/// See the [Team Member Object](https://docs.mod.io/restapiref/#team-member-object) docs for more
594/// information.
595#[derive(Debug, Deserialize)]
596#[non_exhaustive]
597pub struct TeamMember {
598    pub id: MemberId,
599    pub user: User,
600    pub level: TeamLevel,
601    pub date_added: Timestamp,
602    pub position: String,
603}
604
605newtype_enum! {
606    /// Defines the role of a team member.
607    pub struct TeamLevel: u8 {
608        const MODERATOR = 1;
609        const CREATOR   = 4;
610        const ADMIN     = 8;
611    }
612}
613
614#[cfg(test)]
615mod tests {
616    use serde_test::{assert_de_tokens, Token};
617
618    use super::{EventType, MetadataMap};
619    use crate::types::List;
620
621    #[test]
622    fn metadata_from_result_list_serde() {
623        #[derive(Debug, PartialEq, serde_derive::Deserialize)]
624        struct Entry {
625            metakey: String,
626            metavalue: String,
627        }
628
629        let list = List {
630            data: vec![
631                Entry {
632                    metakey: "foo".to_owned(),
633                    metavalue: "bar".to_owned(),
634                },
635                Entry {
636                    metakey: "foo".to_owned(),
637                    metavalue: "baz".to_owned(),
638                },
639            ],
640            count: 2,
641            offset: 0,
642            limit: 100,
643            total: 2,
644        };
645
646        let mut map = MetadataMap::new();
647        map.entry("foo".to_owned())
648            .or_insert(vec!["bar".to_owned(), "baz".to_owned()]);
649
650        let tokens = &[
651            Token::Struct {
652                name: "List",
653                len: 5,
654            },
655            Token::Str("data"),
656            Token::Seq { len: Some(2) },
657            Token::Map { len: Some(2) },
658            Token::Str("metakey"),
659            Token::Str("foo"),
660            Token::Str("metavalue"),
661            Token::Str("bar"),
662            Token::MapEnd,
663            Token::Map { len: Some(2) },
664            Token::Str("metakey"),
665            Token::Str("foo"),
666            Token::Str("metavalue"),
667            Token::Str("baz"),
668            Token::MapEnd,
669            Token::SeqEnd,
670            Token::Str("result_count"),
671            Token::U64(2),
672            Token::Str("result_offset"),
673            Token::U64(0),
674            Token::Str("result_limit"),
675            Token::U64(100),
676            Token::Str("result_total"),
677            Token::U64(2),
678            Token::StructEnd,
679        ];
680
681        assert_de_tokens(&list, tokens);
682        assert_de_tokens(&map, tokens);
683    }
684
685    #[test]
686    fn metadata_from_mod_serde() {
687        #[derive(Debug, PartialEq, serde_derive::Deserialize)]
688        struct Mod {
689            id: u32,
690            #[serde(rename = "metadata_kvp")]
691            metadata: MetadataMap,
692        }
693
694        let mut map = MetadataMap::new();
695        map.entry("foo".to_owned())
696            .or_insert(vec!["bar".to_owned(), "baz".to_owned()]);
697
698        let mod_ = Mod {
699            id: 2,
700            metadata: map,
701        };
702        assert_de_tokens(
703            &mod_,
704            &[
705                Token::Struct {
706                    name: "Mod",
707                    len: 1,
708                },
709                Token::Str("id"),
710                Token::U32(2),
711                Token::Str("metadata_kvp"),
712                Token::Seq { len: Some(2) },
713                Token::Map { len: Some(2) },
714                Token::Str("metakey"),
715                Token::Str("foo"),
716                Token::Str("metavalue"),
717                Token::Str("bar"),
718                Token::MapEnd,
719                Token::Map { len: Some(2) },
720                Token::Str("metakey"),
721                Token::Str("foo"),
722                Token::Str("metavalue"),
723                Token::Str("baz"),
724                Token::MapEnd,
725                Token::SeqEnd,
726                Token::StructEnd,
727            ],
728        );
729    }
730
731    #[test]
732    fn mod_event_type_serde() {
733        assert_de_tokens(
734            &EventType::MODFILE_CHANGED,
735            &[Token::Str("MODFILE_CHANGED")],
736        );
737        assert_de_tokens(&EventType::MOD_AVAILABLE, &[Token::Str("MOD_AVAILABLE")]);
738        assert_de_tokens(
739            &EventType::MOD_UNAVAILABLE,
740            &[Token::Str("MOD_UNAVAILABLE")],
741        );
742        assert_de_tokens(&EventType::MOD_EDITED, &[Token::Str("MOD_EDITED")]);
743        assert_de_tokens(&EventType::MOD_DELETED, &[Token::Str("MOD_DELETED")]);
744        assert_de_tokens(
745            &EventType::MOD_TEAM_CHANGED,
746            &[Token::Str("MOD_TEAM_CHANGED")],
747        );
748        assert_de_tokens(
749            &EventType::MOD_COMMENT_ADDED,
750            &[Token::Str("MOD_COMMENT_ADDED")],
751        );
752        assert_de_tokens(
753            &EventType::MOD_COMMENT_DELETED,
754            &[Token::Str("MOD_COMMENT_DELETED")],
755        );
756        assert_de_tokens(&EventType::from_bytes(b"foo"), &[Token::Str("foo")]);
757    }
758}