1use std::fmt;
4
5use serde::de::{Deserialize, Deserializer};
6use serde_derive::{Deserialize, Serialize};
7use url::Url;
8
9#[macro_use]
10mod macros;
11mod utils;
12
13pub mod auth;
14pub mod files;
15pub mod games;
16pub mod id;
17pub mod mods;
18
19use utils::{DeserializeField, MissingField};
20
21use self::id::{EventId, GameId, ModId, UserId};
22
23#[derive(Debug, Deserialize)]
25#[non_exhaustive]
26pub struct Message {
27 pub code: u16,
28 pub message: String,
29}
30
31#[derive(Debug, Deserialize)]
33#[serde(untagged, expecting = "edited object or 'no new data' message")]
34#[non_exhaustive]
35pub enum Editing<T> {
36 Entity(T),
37 #[serde(deserialize_with = "deserialize_message")]
39 NoChanges,
40}
41
42#[derive(Debug, Deserialize)]
44#[serde(untagged, expecting = "no content or 'no new data' message")]
45#[non_exhaustive]
46pub enum Deletion {
47 Success,
48 #[serde(deserialize_with = "deserialize_message")]
50 NoChanges,
51}
52
53fn deserialize_message<'de, D>(deserializer: D) -> Result<(), D::Error>
54where
55 D: serde::Deserializer<'de>,
56{
57 Message::deserialize(deserializer).map(|_| ())
58}
59
60#[derive(Debug, PartialEq, Deserialize)]
63#[non_exhaustive]
64pub struct List<T> {
65 pub data: Vec<T>,
66 #[serde(rename = "result_count")]
67 pub count: u32,
68 #[serde(rename = "result_total")]
69 pub total: u32,
70 #[serde(rename = "result_limit")]
71 pub limit: u32,
72 #[serde(rename = "result_offset")]
73 pub offset: u32,
74}
75
76#[derive(Debug, Deserialize)]
78#[non_exhaustive]
79pub struct ErrorResponse {
80 pub error: Error,
81}
82
83#[derive(Debug, PartialEq, Deserialize)]
85#[non_exhaustive]
86pub struct Error {
87 pub code: u16,
88 pub error_ref: u16,
89 pub message: String,
90 #[serde(default, deserialize_with = "deserialize_errors")]
91 pub errors: Vec<(String, String)>,
92}
93
94fn deserialize_errors<'de, D: Deserializer<'de>>(
95 deserializer: D,
96) -> Result<Vec<(String, String)>, D::Error> {
97 use serde::de::{MapAccess, Visitor};
98
99 struct MapVisitor;
100 impl<'de> Visitor<'de> for MapVisitor {
101 type Value = Vec<(String, String)>;
102
103 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104 formatter.write_str("errors map")
105 }
106
107 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
108 let mut errors = map.size_hint().map_or_else(Vec::new, Vec::with_capacity);
109 while let Some(entry) = map.next_entry()? {
110 errors.push(entry);
111 }
112 Ok(errors)
113 }
114 }
115
116 deserializer.deserialize_map(MapVisitor)
117}
118
119#[derive(Deserialize)]
121#[non_exhaustive]
122pub struct User {
123 pub id: UserId,
124 pub name_id: String,
125 pub username: String,
126 pub date_online: Timestamp,
127 #[serde(default, deserialize_with = "deserialize_empty_object")]
128 pub avatar: Option<Avatar>,
129 #[serde(with = "utils::url")]
130 pub profile_url: Url,
131}
132
133impl fmt::Debug for User {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.debug_struct("User")
136 .field("id", &self.id)
137 .field("name_id", &self.name_id)
138 .field("username", &self.username)
139 .field("date_online", &self.date_online)
140 .field("avatar", &self.avatar)
141 .field("profile_url", &self.profile_url.as_str())
142 .finish()
143 }
144}
145
146#[derive(Deserialize)]
148#[non_exhaustive]
149pub struct Avatar {
150 pub filename: String,
151 #[serde(with = "utils::url")]
152 pub original: Url,
153 #[serde(with = "utils::url")]
154 pub thumb_50x50: Url,
155 #[serde(with = "utils::url")]
156 pub thumb_100x100: Url,
157}
158
159impl fmt::Debug for Avatar {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 f.debug_struct("Avatar")
162 .field("filename", &self.filename)
163 .field("original", &self.original.as_str())
164 .field("thumb_50x50", &self.thumb_50x50.as_str())
165 .field("thumb_100x100", &self.thumb_100x100.as_str())
166 .finish()
167 }
168}
169
170#[derive(Deserialize)]
172#[non_exhaustive]
173pub struct Logo {
174 pub filename: String,
175 #[serde(with = "utils::url")]
176 pub original: Url,
177 #[serde(with = "utils::url")]
178 pub thumb_320x180: Url,
179 #[serde(with = "utils::url")]
180 pub thumb_640x360: Url,
181 #[serde(with = "utils::url")]
182 pub thumb_1280x720: Url,
183}
184
185impl fmt::Debug for Logo {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 f.debug_struct("Logo")
188 .field("filename", &self.filename)
189 .field("original", &self.original.as_str())
190 .field("thumb_320x180", &self.thumb_320x180.as_str())
191 .field("thumb_640x360", &self.thumb_640x360.as_str())
192 .field("thumb_1280x720", &self.thumb_1280x720.as_str())
193 .finish()
194 }
195}
196
197newtype_enum! {
198 pub struct Status: u8 {
200 const NOT_ACCEPTED = 0;
201 const ACCEPTED = 1;
202 const DELETED = 3;
203 }
204
205 #[derive(Deserialize, Serialize)]
207 pub struct TargetPlatform<16> {
208 const ANDROID = b"android";
209 const IOS = b"ios";
210 const LINUX = b"linux";
211 const MAC = b"mac";
212 const WINDOWS = b"windows";
213 const PS4 = b"ps4";
214 const PS5 = b"ps5";
215 const SOURCE = b"source";
216 const SWITCH = b"switch";
217 const XBOX_ONE = b"xboxone";
218 const XBOX_SERIES_X = b"xboxseriesx";
219 const OCULUS = b"oculus";
220 }
221
222 pub struct TargetPortal<12> {
224 const STEAM = b"steam";
225 const GOG = b"gog";
226 const EGS = b"egs";
227 const ITCHIO = b"itchio";
228 const NINTENDO = b"nintendo";
229 const PSN = b"psn";
230 const XBOX_LIVE = b"xboxlive";
231 const APPLE = b"apple";
232 const GOOGLE = b"google";
233 const FACEBOOK = b"facebook";
234 const DISCORD = b"discord";
235 }
236}
237
238impl TargetPlatform {
239 pub fn display_name(&self) -> &str {
240 match *self {
241 Self::ANDROID => "Android",
242 Self::IOS => "iOS",
243 Self::LINUX => "Linux",
244 Self::MAC => "Mac",
245 Self::WINDOWS => "Windows",
246 Self::PS4 => "PlayStation 4",
247 Self::PS5 => "PlayStation 5",
248 Self::SOURCE => "Source",
249 Self::SWITCH => "Nintendo Switch",
250 Self::XBOX_ONE => "Xbox One",
251 Self::XBOX_SERIES_X => "Xbox Series X/S",
252 Self::OCULUS => "Oculus",
253 _ => self.0.as_str(),
254 }
255 }
256}
257
258impl fmt::Display for TargetPlatform {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 f.write_str(self.display_name())
261 }
262}
263
264#[derive(Debug, Deserialize)]
266#[non_exhaustive]
267pub struct Event {
268 pub id: EventId,
269 pub game_id: GameId,
270 pub mod_id: ModId,
271 pub user_id: UserId,
272 pub date_added: Timestamp,
273 pub event_type: EventType,
274}
275
276newtype_enum! {
277 #[derive(Deserialize)]
279 #[serde(transparent)]
280 pub struct EventType<24> {
281 const USER_TEAM_JOIN = b"USER_TEAM_JOIN";
283 const USER_TEAM_LEAVE = b"USER_TEAM_LEAVE";
285 const USER_SUBSCRIBE = b"USER_SUBSCRIBE";
287 const USER_UNSUBSCRIBE = b"USER_UNSUBSCRIBE";
289 }
290}
291
292impl fmt::Display for EventType {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 f.write_str(self.as_str())
295 }
296}
297
298fn deserialize_empty_object<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
312where
313 D: Deserializer<'de>,
314 T: Deserialize<'de>,
315{
316 #[derive(Deserialize)]
317 #[serde(
318 untagged,
319 deny_unknown_fields,
320 expecting = "object, empty object or null"
321 )]
322 enum Helper<T> {
323 Data(T),
324 Empty {},
325 Null,
326 }
327 match Helper::deserialize(deserializer) {
328 Ok(Helper::Data(data)) => Ok(Some(data)),
329 Ok(_) => Ok(None),
330 Err(e) => Err(e),
331 }
332}
333
334#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
336pub struct Timestamp(i64);
337
338impl Timestamp {
339 pub const fn as_secs(self) -> i64 {
341 self.0
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use serde_derive::Deserialize;
348 use serde_test::{assert_de_tokens, assert_tokens, Token};
349
350 use super::{deserialize_empty_object, Error, EventType, TargetPlatform};
351
352 #[test]
353 fn deserialize_error_no_errors_field() {
354 let value = Error {
355 code: 404,
356 error_ref: 11005,
357 message: "foo".to_owned(),
358 errors: vec![],
359 };
360
361 assert_de_tokens(
362 &value,
363 &[
364 Token::Struct {
365 name: "Error",
366 len: 3,
367 },
368 Token::Str("code"),
369 Token::U16(404),
370 Token::Str("error_ref"),
371 Token::U16(11005),
372 Token::Str("message"),
373 Token::Str("foo"),
374 Token::StructEnd,
375 ],
376 );
377 }
378
379 #[test]
380 fn deserialize_error_empty_errors() {
381 let value = Error {
382 code: 404,
383 error_ref: 11005,
384 message: "foo".to_owned(),
385 errors: vec![],
386 };
387
388 assert_de_tokens(
389 &value,
390 &[
391 Token::Struct {
392 name: "Error",
393 len: 3,
394 },
395 Token::Str("code"),
396 Token::U16(404),
397 Token::Str("error_ref"),
398 Token::U16(11005),
399 Token::Str("message"),
400 Token::Str("foo"),
401 Token::Str("errors"),
402 Token::Map { len: Some(0) },
403 Token::MapEnd,
404 Token::StructEnd,
405 ],
406 );
407 }
408
409 #[test]
410 fn deserialize_error_with_errors() {
411 let value = Error {
412 code: 404,
413 error_ref: 11005,
414 message: "foo".to_owned(),
415 errors: vec![("foo".to_owned(), "bar".to_owned())],
416 };
417
418 assert_de_tokens(
419 &value,
420 &[
421 Token::Struct {
422 name: "Error",
423 len: 3,
424 },
425 Token::Str("code"),
426 Token::U16(404),
427 Token::Str("error_ref"),
428 Token::U16(11005),
429 Token::Str("message"),
430 Token::Str("foo"),
431 Token::Str("errors"),
432 Token::Map { len: Some(1) },
433 Token::Str("foo"),
434 Token::Str("bar"),
435 Token::MapEnd,
436 Token::StructEnd,
437 ],
438 );
439 }
440
441 #[derive(Debug, PartialEq, Deserialize)]
442 struct Game {
443 id: u32,
444 #[serde(default, deserialize_with = "deserialize_empty_object")]
445 header: Option<Header>,
446 }
447
448 #[derive(Debug, PartialEq, Deserialize)]
449 struct Header {
450 filename: String,
451 }
452
453 #[test]
454 fn deserialize_empty_object_full() {
455 let value = Game {
456 id: 1,
457 header: Some(Header {
458 filename: "foobar".to_string(),
459 }),
460 };
461 assert_de_tokens(
462 &value,
463 &[
464 Token::Struct {
465 name: "Game",
466 len: 2,
467 },
468 Token::Str("id"),
469 Token::U8(1),
470 Token::Str("header"),
471 Token::Struct {
472 name: "Header",
473 len: 1,
474 },
475 Token::Str("filename"),
476 Token::Str("foobar"),
477 Token::StructEnd,
478 Token::StructEnd,
479 ],
480 );
481 }
482
483 #[test]
484 fn deserialize_empty_object_empty() {
485 let value = Game {
486 id: 1,
487 header: None,
488 };
489 assert_de_tokens(
490 &value,
491 &[
492 Token::Struct {
493 name: "Game",
494 len: 2,
495 },
496 Token::Str("id"),
497 Token::U8(1),
498 Token::Str("header"),
499 Token::Struct {
500 name: "Header",
501 len: 0,
502 },
503 Token::StructEnd,
504 Token::StructEnd,
505 ],
506 );
507 }
508
509 #[test]
510 fn deserialize_empty_object_null() {
511 let value = Game {
512 id: 1,
513 header: None,
514 };
515 assert_de_tokens(
516 &value,
517 &[
518 Token::Struct {
519 name: "Game",
520 len: 2,
521 },
522 Token::Str("id"),
523 Token::U8(1),
524 Token::Str("header"),
525 Token::None,
526 Token::StructEnd,
527 ],
528 );
529 }
530
531 #[test]
532 fn deserialize_empty_object_absent() {
533 let value = Game {
534 id: 1,
535 header: None,
536 };
537 assert_de_tokens(
538 &value,
539 &[
540 Token::Struct {
541 name: "Game",
542 len: 1,
543 },
544 Token::Str("id"),
545 Token::U8(1),
546 Token::StructEnd,
547 ],
548 );
549 }
550
551 #[test]
552 fn deserialize_empty_object_unknown_values() {
553 let value = Game {
554 id: 1,
555 header: Some(Header {
556 filename: "foobar".to_string(),
557 }),
558 };
559 assert_de_tokens(
560 &value,
561 &[
562 Token::Struct {
563 name: "Game",
564 len: 2,
565 },
566 Token::Str("id"),
567 Token::U8(1),
568 Token::Str("header"),
569 Token::Struct {
570 name: "Header",
571 len: 1,
572 },
573 Token::Str("filename"),
574 Token::Str("foobar"),
575 Token::Str("id"),
576 Token::U8(2),
577 Token::StructEnd,
578 Token::StructEnd,
579 ],
580 );
581 }
582
583 #[test]
584 fn deserialize_empty_object_missing_field() {
585 serde_test::assert_de_tokens_error::<Game>(
586 &[
587 Token::Struct {
588 name: "Game",
589 len: 2,
590 },
591 Token::Str("id"),
592 Token::U8(1),
593 Token::Str("header"),
594 Token::Struct {
595 name: "Header",
596 len: 1,
597 },
598 Token::Str("id"),
599 Token::U8(2),
600 Token::StructEnd,
601 Token::StructEnd,
602 ],
603 "object, empty object or null",
604 );
605 }
606
607 #[test]
608 fn user_event_type_serde() {
609 assert_de_tokens(&EventType::USER_TEAM_JOIN, &[Token::Str("USER_TEAM_JOIN")]);
610 assert_de_tokens(
611 &EventType::USER_TEAM_LEAVE,
612 &[Token::Str("USER_TEAM_LEAVE")],
613 );
614 assert_de_tokens(&EventType::USER_SUBSCRIBE, &[Token::Str("USER_SUBSCRIBE")]);
615 assert_de_tokens(
616 &EventType::USER_UNSUBSCRIBE,
617 &[Token::Str("USER_UNSUBSCRIBE")],
618 );
619 assert_de_tokens(&EventType::from_bytes(b"foo"), &[Token::Str("foo")]);
620 }
621
622 #[test]
623 fn target_platform_compare() {
624 assert_eq!(TargetPlatform::ANDROID, "ANDROID");
625 assert_eq!("android", TargetPlatform::ANDROID);
626 }
627
628 #[test]
629 fn target_platform_serde() {
630 assert_tokens(
631 &TargetPlatform::ANDROID,
632 &[
633 Token::NewtypeStruct {
634 name: "TargetPlatform",
635 },
636 Token::Str("android"),
637 ],
638 );
639 }
640}
641
642