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