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#[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 #[serde(deserialize_with = "deserialize_empty_object")]
43 pub stats: Option<Statistics>,
44 #[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 pub struct PresentationOption: u8 {
55 const GRID_VIEW = 0;
57 const TABLE_VIEW = 1;
59 }
60
61 pub struct SubmissionOption: u8 {
63 const API_ONLY = 0;
65 const ANYWHERE = 1;
67 }
68
69 pub struct DependencyOption: u8 {
71 const DISALLOWED = 0;
73 const OPT_IN = 1;
75 const OPT_OUT = 2;
77 const NO_RESTRICTION = 3;
79 }
80
81 pub struct CurationOption: u8 {
83 const NO_CURATION = 0;
85 const PAID = 1;
88 const FULL = 2;
90 }
91}
92
93bitflags! {
94 pub struct CommunityOptions: u16 {
96 #[deprecated(note = "Flag is replaced by `ALLOW_COMMENTS`")]
98 const DISCUSSIONS = 1;
99 const ALLOW_COMMENTS = 1;
101 #[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 const PREVIEWS = 64;
111 const PREVIEW_URLS = 128;
113 const NEGATIVE_RATINGS = 256;
115 const WEB_EDIT_MODS = 512;
117 const ALLOW_MOD_DEPENDENCIES = 1024;
118 const ALLOW_GUIDE_COMMENTS = 2048;
120 }
121
122 pub struct ApiAccessOptions: u8 {
124 const ALLOW_THIRD_PARTY = 1;
126 const ALLOW_DIRECT_DOWNLOAD = 2;
128 const CHECK_AUTHORIZATION = 4;
131 const CHECK_OWNERSHIP = 8;
134 }
135
136 pub struct MaturityOptions: u8 {
138 const NOT_ALLOWED = 0;
139 const ALLOWED = 1;
141 const ADULT_ONLY = 2;
143 }
144}
145
146#[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#[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#[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#[derive(Debug)]
299#[non_exhaustive]
300pub struct Downloads {
301 pub total: u32,
302 pub today: u32,
303 pub daily_average: u32,
304}
305
306#[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#[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#[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#[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#[derive(Debug, Deserialize)]
369#[non_exhaustive]
370pub struct Platform {
371 #[serde(rename = "platform")]
372 pub target: TargetPlatform,
373 pub moderated: bool,
374 pub locked: bool,
376}