modio/types/
games.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use serde::de::{Deserialize, Deserializer, IgnoredAny, MapAccess, Visitor};
5use serde_derive::{Deserialize, Serialize};
6use url::Url;
7
8use super::id::GameId;
9use super::{deserialize_empty_object, utils, DeserializeField, MissingField};
10use super::{Logo, Status, TargetPlatform, Timestamp};
11
12/// See the [Game Object](https://docs.mod.io/restapiref/#game-object) docs for more information.
13#[derive(Debug, Deserialize)]
14#[non_exhaustive]
15pub struct Game {
16    pub id: GameId,
17    pub status: Status,
18    pub date_added: Timestamp,
19    pub date_updated: Timestamp,
20    pub date_live: Timestamp,
21    pub presentation_option: PresentationOption,
22    pub submission_option: SubmissionOption,
23    pub dependency_option: DependencyOption,
24    pub curation_option: CurationOption,
25    pub community_options: CommunityOptions,
26    pub api_access_options: ApiAccessOptions,
27    pub maturity_options: MaturityOptions,
28    pub ugc_name: String,
29    pub icon: Icon,
30    pub logo: Logo,
31    #[serde(default, deserialize_with = "deserialize_empty_object")]
32    pub header: Option<HeaderImage>,
33    pub name: String,
34    pub name_id: String,
35    pub summary: String,
36    pub instructions: Option<String>,
37    #[serde(with = "utils::url::opt")]
38    pub instructions_url: Option<Url>,
39    #[serde(with = "utils::url")]
40    pub profile_url: Url,
41    /// The field is `None` when the game object is fetched from `/me/games`.
42    #[serde(deserialize_with = "deserialize_empty_object")]
43    pub stats: Option<Statistics>,
44    /// The field is `None` when the game object is fetched from `/me/games`.
45    #[serde(deserialize_with = "deserialize_empty_object")]
46    pub theme: Option<Theme>,
47    pub other_urls: Vec<OtherUrl>,
48    pub tag_options: Vec<TagOption>,
49    pub platforms: Vec<Platform>,
50}
51
52newtype_enum! {
53    /// Presentation style used on the mod.io website.
54    pub struct PresentationOption: u8 {
55        /// Displays mods in a grid.
56        const GRID_VIEW  = 0;
57        /// Displays mods in a table.
58        const TABLE_VIEW = 1;
59    }
60
61    /// Submission process modders must follow.
62    pub struct SubmissionOption: u8 {
63        /// Mod uploads must occur via the API using a tool by the game developers.
64        const API_ONLY = 0;
65        /// Mod uploads can occur from anywhere, include the website and API.
66        const ANYWHERE = 1;
67    }
68
69    /// Dependency option for a game.
70    pub struct DependencyOption: u8 {
71        /// Disallow mod dependencies.
72        const DISALLOWED     = 0;
73        /// Allow mod dependencies, mods must opt in.
74        const OPT_IN         = 1;
75        /// Allow mod dependencies, mods must opt out.
76        const OPT_OUT        = 2;
77        /// Allow mod dependencies with no restrictions.
78        const NO_RESTRICTION = 3;
79    }
80
81    /// Curation process used to approve mods.
82    pub struct CurationOption: u8 {
83        /// No curation: Mods are immediately available to play.
84        const NO_CURATION = 0;
85        /// Paid curation: Mods are immediately to play unless they choose to receive
86        /// donations. These mods must be accepted to be listed.
87        const PAID = 1;
88        /// Full curation: All mods must be accepted by someone to be listed.
89        const FULL = 2;
90    }
91}
92
93bitflags! {
94    /// Community features enabled on the mod.io website.
95    pub struct CommunityOptions: u16 {
96        /// Discussion board enabled.
97        #[deprecated(note = "Flag is replaced by `ALLOW_COMMENTS`")]
98        const DISCUSSIONS       = 1;
99        /// Allow comments on mods.
100        const ALLOW_COMMENTS    = 1;
101        /// Guides & News enabled.
102        #[deprecated(note = "Flag is replaced by `ALLOW_GUIDES`")]
103        const GUIDES_NEWS       = 2;
104        const ALLOW_GUIDES      = 2;
105        const PIN_ON_HOMEPAGE   = 4;
106        const SHOW_ON_HOMEPAGE  = 8;
107        const SHOW_MORE_ON_HOMEPAGE = 16;
108        const ALLOW_CHANGE_STATUS   = 32;
109        /// Previews enabled (Game must be hidden).
110        const PREVIEWS = 64;
111        /// Preview URLs enabled (Previews must be enabled).
112        const PREVIEW_URLS = 128;
113        /// Allow negative ratings
114        const NEGATIVE_RATINGS = 256;
115        /// Allow mods to be edited via web.
116        const WEB_EDIT_MODS = 512;
117        const ALLOW_MOD_DEPENDENCIES = 1024;
118        /// Allow comments on guides.
119        const ALLOW_GUIDE_COMMENTS   = 2048;
120    }
121
122    /// Level of API access allowed by a game.
123    pub struct ApiAccessOptions: u8 {
124        /// Allow third parties to access a game's API endpoints.
125        const ALLOW_THIRD_PARTY     = 1;
126        /// Allow mods to be downloaded directly.
127        const ALLOW_DIRECT_DOWNLOAD = 2;
128        /// Checks authorization on the mods to be downloaded directly (if enabled the consuming
129        /// application must send the user's bearer token)
130        const CHECK_AUTHORIZATION = 4;
131        /// Checks ownerchip on the mods to be downloaded directly (if enabled the consuming
132        /// application must send the user's bearer token)
133        const CHECK_OWNERSHIP = 8;
134    }
135
136    /// Mature content options.
137    pub struct MaturityOptions: u8 {
138        const NOT_ALLOWED = 0;
139        /// Allow flagging mods as mature.
140        const ALLOWED     = 1;
141        /// The game is for mature audiences only.
142        const ADULT_ONLY  = 2;
143    }
144}
145
146/// See the [Icon Object](https://docs.mod.io/restapiref/#icon-object) docs for more information.
147#[derive(Deserialize)]
148#[non_exhaustive]
149pub struct Icon {
150    pub filename: String,
151    #[serde(with = "utils::url")]
152    pub original: Url,
153    #[serde(with = "utils::url")]
154    pub thumb_64x64: Url,
155    #[serde(with = "utils::url")]
156    pub thumb_128x128: Url,
157    #[serde(with = "utils::url")]
158    pub thumb_256x256: Url,
159}
160
161impl fmt::Debug for Icon {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        f.debug_struct("Icon")
164            .field("filename", &self.filename)
165            .field("original", &self.original.as_str())
166            .field("thumb_64x64", &self.thumb_64x64.as_str())
167            .field("thumb_128x128", &self.thumb_128x128.as_str())
168            .field("thumb_256x256", &self.thumb_256x256.as_str())
169            .finish()
170    }
171}
172
173/// See the [Header Image Object](https://docs.mod.io/restapiref/#header-image-object) docs for more
174/// information.
175#[derive(Deserialize)]
176#[non_exhaustive]
177pub struct HeaderImage {
178    pub filename: String,
179    #[serde(with = "utils::url")]
180    pub original: Url,
181}
182
183impl fmt::Debug for HeaderImage {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.debug_struct("HeaderImage")
186            .field("filename", &self.filename)
187            .field("original", &self.original.as_str())
188            .finish()
189    }
190}
191
192/// See the [Game Statistics Object](https://docs.mod.io/restapiref/#game-stats-object) docs for more
193/// information.
194#[derive(Debug)]
195#[non_exhaustive]
196pub struct Statistics {
197    pub game_id: GameId,
198    pub mods_total: u32,
199    pub subscribers_total: u32,
200    pub downloads: Downloads,
201    pub expired_at: Timestamp,
202}
203
204impl<'de> Deserialize<'de> for Statistics {
205    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
206        #[derive(Deserialize)]
207        #[serde(field_identifier, rename_all = "snake_case")]
208        enum Field {
209            GameId,
210            ModsCountTotal,
211            ModsSubscribersTotal,
212            ModsDownloadsTotal,
213            ModsDownloadsToday,
214            ModsDownloadsDailyAverage,
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 game_id = None;
231                let mut mods_total = None;
232                let mut subscribers_total = None;
233                let mut downloads_total = None;
234                let mut downloads_today = None;
235                let mut downloads_daily_average = None;
236                let mut expired_at = None;
237
238                while let Some(key) = map.next_key()? {
239                    match key {
240                        Field::GameId => {
241                            game_id.deserialize_value("game_id", &mut map)?;
242                        }
243                        Field::ModsCountTotal => {
244                            mods_total.deserialize_value("mods_count_total", &mut map)?;
245                        }
246                        Field::ModsSubscribersTotal => {
247                            subscribers_total
248                                .deserialize_value("mods_subscribers_total", &mut map)?;
249                        }
250                        Field::ModsDownloadsToday => {
251                            downloads_today.deserialize_value("mods_downloads_today", &mut map)?;
252                        }
253                        Field::ModsDownloadsTotal => {
254                            downloads_total.deserialize_value("mods_downloads_total", &mut map)?;
255                        }
256                        Field::ModsDownloadsDailyAverage => {
257                            downloads_daily_average
258                                .deserialize_value("mods_downloads_daily_average", &mut map)?;
259                        }
260                        Field::DateExpires => {
261                            expired_at.deserialize_value("date_expires", &mut map)?;
262                        }
263                        Field::Other(_) => {
264                            map.next_value::<IgnoredAny>()?;
265                        }
266                    }
267                }
268
269                let game_id = game_id.missing_field("game_id")?;
270                let mods_total = mods_total.missing_field("mods_count_total")?;
271                let subscribers_total =
272                    subscribers_total.missing_field("mods_subscribers_total")?;
273                let downloads_total = downloads_total.missing_field("mods_downloads_total")?;
274                let downloads_today = downloads_today.missing_field("mods_downloads_today")?;
275                let downloads_daily_average =
276                    downloads_daily_average.missing_field("mods_downloads_daily_average")?;
277                let expired_at = expired_at.missing_field("date_expires")?;
278
279                Ok(Statistics {
280                    game_id,
281                    mods_total,
282                    subscribers_total,
283                    downloads: Downloads {
284                        total: downloads_total,
285                        today: downloads_today,
286                        daily_average: downloads_daily_average,
287                    },
288                    expired_at,
289                })
290            }
291        }
292
293        deserializer.deserialize_map(StatisticsVisitor)
294    }
295}
296
297/// Part of [`Statistics`]
298#[derive(Debug)]
299#[non_exhaustive]
300pub struct Downloads {
301    pub total: u32,
302    pub today: u32,
303    pub daily_average: u32,
304}
305
306/// See the [Game Tag Option Object](https://docs.mod.io/restapiref/#game-tag-option-object) docs for more
307/// information.
308#[derive(Debug, Deserialize)]
309#[non_exhaustive]
310pub struct TagOption {
311    pub name: String,
312    #[serde(rename = "type")]
313    pub kind: TagType,
314    #[serde(rename = "tag_count_map")]
315    pub tag_count: HashMap<String, u32>,
316    pub hidden: bool,
317    pub locked: bool,
318    pub tags: Vec<String>,
319}
320
321/// Defines the type of a tag. See [`TagOption`].
322#[derive(Debug, Deserialize, Serialize)]
323#[serde(rename_all = "lowercase")]
324#[non_exhaustive]
325pub enum TagType {
326    Checkboxes,
327    Dropdown,
328}
329
330impl fmt::Display for TagType {
331    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
332        match self {
333            Self::Checkboxes => fmt.write_str("checkboxes"),
334            Self::Dropdown => fmt.write_str("dropdown"),
335        }
336    }
337}
338
339/// See the [Theme Object](https://docs.mod.io/restapiref/#theme-object) docs for more information.
340#[derive(Debug, Deserialize)]
341pub struct Theme {
342    pub primary: String,
343    pub dark: String,
344    pub light: String,
345    pub success: String,
346    pub warning: String,
347    pub danger: String,
348}
349
350/// See the [Game OtherUrls Object](https://docs.mod.io/restapiref/#game-otherurls-object) docs for more information.
351#[derive(Deserialize)]
352pub struct OtherUrl {
353    pub label: String,
354    #[serde(with = "utils::url")]
355    pub url: Url,
356}
357
358impl fmt::Debug for OtherUrl {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        f.debug_struct("OtherUrl")
361            .field("label", &self.label)
362            .field("url", &self.url.as_str())
363            .finish()
364    }
365}
366
367/// See the [Game Platforms Object](https://docs.mod.io/restapiref/#game-platforms-object) docs for more information.
368#[derive(Debug, Deserialize)]
369#[non_exhaustive]
370pub struct Platform {
371    #[serde(rename = "platform")]
372    pub target: TargetPlatform,
373    pub moderated: bool,
374    /// Indicates if users can upload files for this platform.
375    pub locked: bool,
376}