1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
use std::collections::HashMap;
use std::fmt;

use serde::de::{Deserializer, IgnoredAny, MapAccess, Visitor};
use serde::{Deserialize, Serialize};
use url::Url;

use super::id::GameId;
use super::{deserialize_empty_object, DeserializeField, MissingField};
use super::{Logo, Status, TargetPlatform};

/// See the [Game Object](https://docs.mod.io/#game-object) docs for more information.
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct Game {
    pub id: GameId,
    pub status: Status,
    pub date_added: u64,
    pub date_updated: u64,
    pub date_live: u64,
    pub presentation_option: PresentationOption,
    pub submission_option: SubmissionOption,
    pub curation_option: CurationOption,
    pub community_options: CommunityOptions,
    pub api_access_options: ApiAccessOptions,
    pub maturity_options: MaturityOptions,
    pub ugc_name: String,
    pub icon: Icon,
    pub logo: Logo,
    #[serde(default, deserialize_with = "deserialize_empty_object")]
    pub header: Option<HeaderImage>,
    pub name: String,
    pub name_id: String,
    pub summary: String,
    pub instructions: Option<String>,
    pub instructions_url: Option<Url>,
    pub profile_url: Url,
    /// The field is `None` when the game object is fetched from `/me/games`.
    #[serde(deserialize_with = "deserialize_empty_object")]
    pub stats: Option<Statistics>,
    /// The field is `None` when the game object is fetched from `/me/games`.
    #[serde(deserialize_with = "deserialize_empty_object")]
    pub theme: Option<Theme>,
    pub other_urls: Vec<OtherUrl>,
    pub tag_options: Vec<TagOption>,
    pub platforms: Vec<Platform>,
}

newtype_enum! {
    /// Presentation style used on the mod.io website.
    pub struct PresentationOption: u8 {
        /// Displays mods in a grid.
        const GRID_VIEW  = 0;
        /// Displays mods in a table.
        const TABLE_VIEW = 1;
    }

    /// Submission process modders must follow.
    pub struct SubmissionOption: u8 {
        /// Mod uploads must occur via the API using a tool by the game developers.
        const API_ONLY = 0;
        /// Mod uploads can occur from anywhere, include the website and API.
        const ANYWHERE = 1;
    }

    /// Curation process used to approve mods.
    pub struct CurationOption: u8 {
        /// No curation: Mods are immediately available to play.
        const NO_CURATION = 0;
        /// Paid curation: Mods are immediately to play unless they choose to receive
        /// donations. These mods must be accepted to be listed.
        const PAID = 1;
        /// Full curation: All mods must be accepted by someone to be listed.
        const FULL = 2;
    }
}

bitflags! {
    /// Community features enabled on the mod.io website.
    pub struct CommunityOptions: u16 {
        /// Discussion board enabled.
        const DISCUSSIONS       = 1;
        /// Guides & News enabled.
        const GUIDES_NEWS       = 2;
        const PIN_ON_HOMEPAGE   = 4;
        const SHOW_ON_HOMEPAGE  = 8;
        const SHOW_MORE_ON_HOMEPAGE = 16;
        const ALLOW_CHANGE_STATUS   = 32;
        /// Previews enabled (Game must be hidden).
        const PREVIEWS = 64;
        /// Preview URLs enabled (Previews must be enabled).
        const PREVIEW_URLS = 128;
        /// Allow negative ratings
        const NEGATIVE_RATINGS = 256;
        /// Allow mods to be edited via web.
        const WEB_EDIT_MODS = 512;
    }

    /// Level of API access allowed by a game.
    pub struct ApiAccessOptions: u8 {
        /// Allow third parties to access a game's API endpoints.
        const ALLOW_THIRD_PARTY     = 1;
        /// Allow mods to be downloaded directly.
        const ALLOW_DIRECT_DOWNLOAD = 2;
    }

    /// Mature content options.
    pub struct MaturityOptions: u8 {
        const NOT_ALLOWED = 0;
        /// Allow flagging mods as mature.
        const ALLOWED     = 1;
        /// The game is for mature audiences only.
        const ADULT_ONLY  = 2;
    }
}

/// See the [Icon Object](https://docs.mod.io/#icon-object) docs for more information.
#[derive(Deserialize)]
#[non_exhaustive]
pub struct Icon {
    pub filename: String,
    pub original: Url,
    pub thumb_64x64: Url,
    pub thumb_128x128: Url,
    pub thumb_256x256: Url,
}

impl fmt::Debug for Icon {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Icon")
            .field("filename", &self.filename)
            .field("original", &self.original.as_str())
            .field("thumb_64x64", &self.thumb_64x64.as_str())
            .field("thumb_128x128", &self.thumb_128x128.as_str())
            .field("thumb_256x256", &self.thumb_256x256.as_str())
            .finish()
    }
}

/// See the [Header Image Object](https://docs.mod.io/#header-image-object) docs for more
/// information.
#[derive(Deserialize)]
#[non_exhaustive]
pub struct HeaderImage {
    pub filename: String,
    pub original: Url,
}

impl fmt::Debug for HeaderImage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("HeaderImage")
            .field("filename", &self.filename)
            .field("original", &self.original.as_str())
            .finish()
    }
}

/// See the [Game Statistics Object](https://docs.mod.io/#game-stats-object) docs for more
/// information.
#[derive(Debug)]
#[non_exhaustive]
pub struct Statistics {
    pub game_id: GameId,
    pub mods_total: u32,
    pub subscribers_total: u32,
    pub downloads: Downloads,
    pub expired_at: u64,
}

impl<'de> Deserialize<'de> for Statistics {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        #[derive(Deserialize)]
        #[serde(field_identifier, rename_all = "snake_case")]
        enum Field {
            GameId,
            ModsCountTotal,
            ModsSubscribersTotal,
            ModsDownloadsTotal,
            ModsDownloadsToday,
            ModsDownloadsDailyAverage,
            DateExpires,
            #[allow(dead_code)]
            Other(String),
        }

        struct StatisticsVisitor;

        impl<'de> Visitor<'de> for StatisticsVisitor {
            type Value = Statistics;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str("struct Statistics")
            }

            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
                let mut game_id = None;
                let mut mods_total = None;
                let mut subscribers_total = None;
                let mut downloads_total = None;
                let mut downloads_today = None;
                let mut downloads_daily_average = None;
                let mut expired_at = None;

                while let Some(key) = map.next_key()? {
                    match key {
                        Field::GameId => {
                            game_id.deserialize_value("game_id", &mut map)?;
                        }
                        Field::ModsCountTotal => {
                            mods_total.deserialize_value("mods_count_total", &mut map)?;
                        }
                        Field::ModsSubscribersTotal => {
                            subscribers_total
                                .deserialize_value("mods_subscribers_total", &mut map)?;
                        }
                        Field::ModsDownloadsToday => {
                            downloads_today.deserialize_value("mods_downloads_today", &mut map)?;
                        }
                        Field::ModsDownloadsTotal => {
                            downloads_total.deserialize_value("mods_downloads_total", &mut map)?;
                        }
                        Field::ModsDownloadsDailyAverage => {
                            downloads_daily_average
                                .deserialize_value("mods_downloads_daily_average", &mut map)?;
                        }
                        Field::DateExpires => {
                            expired_at.deserialize_value("date_expires", &mut map)?;
                        }
                        Field::Other(_) => {
                            map.next_value::<IgnoredAny>()?;
                        }
                    }
                }

                let game_id = game_id.missing_field("game_id")?;
                let mods_total = mods_total.missing_field("mods_count_total")?;
                let subscribers_total =
                    subscribers_total.missing_field("mods_subscribers_total")?;
                let downloads_total = downloads_total.missing_field("mods_downloads_total")?;
                let downloads_today = downloads_today.missing_field("mods_downloads_today")?;
                let downloads_daily_average =
                    downloads_daily_average.missing_field("mods_downloads_daily_average")?;
                let expired_at = expired_at.missing_field("date_expires")?;

                Ok(Statistics {
                    game_id,
                    mods_total,
                    subscribers_total,
                    downloads: Downloads {
                        total: downloads_total,
                        today: downloads_today,
                        daily_average: downloads_daily_average,
                    },
                    expired_at,
                })
            }
        }

        deserializer.deserialize_map(StatisticsVisitor)
    }
}

/// Part of [`Statistics`]
#[derive(Debug)]
#[non_exhaustive]
pub struct Downloads {
    pub total: u32,
    pub today: u32,
    pub daily_average: u32,
}

/// See the [Game Tag Option Object](https://docs.mod.io/#game-tag-option-object) docs for more
/// information.
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct TagOption {
    pub name: String,
    #[serde(rename = "type")]
    pub kind: TagType,
    #[serde(rename = "tag_count_map")]
    pub tag_count: HashMap<String, u32>,
    pub hidden: bool,
    pub locked: bool,
    pub tags: Vec<String>,
}

/// Defines the type of a tag. See [`TagOption`].
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum TagType {
    Checkboxes,
    Dropdown,
}

impl fmt::Display for TagType {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        match self {
            Self::Checkboxes => fmt.write_str("checkboxes"),
            Self::Dropdown => fmt.write_str("dropdown"),
        }
    }
}

/// See the [Theme Object](https://docs.mod.io/#theme-object) docs for more information.
#[derive(Debug, Deserialize)]
pub struct Theme {
    pub primary: String,
    pub dark: String,
    pub light: String,
    pub success: String,
    pub warning: String,
    pub danger: String,
}

/// See the [Game OtherUrls Object](https://docs.mod.io/#game-otherurls-object) docs for more information.
#[derive(Deserialize)]
pub struct OtherUrl {
    pub label: String,
    pub url: Url,
}

impl fmt::Debug for OtherUrl {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("OtherUrl")
            .field("label", &self.label)
            .field("url", &self.url.as_str())
            .finish()
    }
}

/// See the [Game Platforms Object](https://docs.mod.io/#game-platforms-object) docs for more information.
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct Platform {
    #[serde(rename = "platform")]
    pub target: TargetPlatform,
    pub moderated: bool,
    /// Indicates if users can upload files for this platform.
    pub locked: bool,
}