1use std::collections::HashMap;
2use std::fmt;
3
4use serde::de::{Deserialize, Deserializer, IgnoredAny, MapAccess, SeqAccess, Visitor};
5use serde_derive::Deserialize;
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#[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 price: f32,
28 pub tax: u32,
29 pub logo: Logo,
30 #[serde(with = "utils::url::opt")]
31 pub homepage_url: Option<Url>,
32 pub name: String,
33 pub name_id: String,
34 pub summary: String,
35 pub description: Option<String>,
36 pub description_plaintext: Option<String>,
37 pub metadata_blob: Option<String>,
38 #[serde(with = "utils::url")]
39 pub profile_url: Url,
40 #[serde(default, deserialize_with = "deserialize_empty_object")]
41 pub modfile: Option<File>,
42 pub media: Media,
43 #[serde(rename = "metadata_kvp")]
44 pub metadata: MetadataMap,
45 pub tags: Vec<Tag>,
46 pub dependencies: bool,
47 pub stats: Statistics,
48 pub platforms: Vec<Platform>,
49}
50
51newtype_enum! {
52 pub struct Visibility: u8 {
54 const HIDDEN = 0;
55 const PUBLIC = 1;
56 }
57}
58
59bitflags! {
60 pub struct CommunityOptions: u16 {
62 const COMMENTS = 1;
64 const PREVIEWS = 64;
66 const PREVIEW_URLS = 128;
68 const ALLOW_DEPENDENCIES = 1024;
70 }
71
72 pub struct MaturityOption: u8 {
76 const ALCOHOL = 1;
77 const DRUGS = 2;
78 const VIOLENCE = 4;
79 const EXPLICIT = 8;
80 }
81}
82
83#[derive(Debug, Deserialize)]
85#[non_exhaustive]
86pub struct Event {
87 pub id: EventId,
88 pub mod_id: ModId,
89 pub user_id: UserId,
90 pub date_added: Timestamp,
91 pub event_type: EventType,
92}
93
94newtype_enum! {
95 #[derive(Deserialize)]
97 #[serde(transparent)]
98 pub struct EventType<24> {
99 const MODFILE_CHANGED = b"MODFILE_CHANGED";
101 const MOD_AVAILABLE = b"MOD_AVAILABLE";
103 const MOD_UNAVAILABLE = b"MOD_UNAVAILABLE";
105 const MOD_EDITED = b"MOD_EDITED";
107 const MOD_DELETED = b"MOD_DELETED";
109 const MOD_TEAM_CHANGED = b"MOD_TEAM_CHANGED";
111 const MOD_COMMENT_ADDED = b"MOD_COMMENT_ADDED";
113 const MOD_COMMENT_DELETED = b"MOD_COMMENT_DELETED";
115 }
116}
117
118impl fmt::Display for EventType {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 f.write_str(self.as_str())
121 }
122}
123
124#[derive(Debug, Deserialize)]
127#[non_exhaustive]
128pub struct Dependency {
129 pub mod_id: ModId,
130 pub date_added: Timestamp,
131}
132
133#[derive(Debug, Deserialize)]
136#[non_exhaustive]
137pub struct Media {
138 #[serde(default = "Vec::new")]
139 pub youtube: Vec<String>,
140 #[serde(default = "Vec::new")]
141 pub sketchfab: Vec<String>,
142 #[serde(default = "Vec::new")]
143 pub images: Vec<Image>,
144}
145
146#[derive(Deserialize)]
148#[non_exhaustive]
149pub struct Image {
150 pub filename: String,
151 #[serde(with = "utils::url")]
152 pub original: Url,
153 #[serde(with = "utils::url")]
154 pub thumb_320x180: Url,
155}
156
157impl fmt::Debug for Image {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 f.debug_struct("Image")
160 .field("filename", &self.filename)
161 .field("original", &self.original.as_str())
162 .field("thumb_320x180", &self.thumb_320x180.as_str())
163 .finish()
164 }
165}
166
167#[derive(Debug)]
170#[non_exhaustive]
171pub struct Statistics {
172 pub mod_id: ModId,
173 pub downloads_today: u32,
174 pub downloads_total: u32,
175 pub subscribers_total: u32,
176 pub popularity: Popularity,
177 pub ratings: Ratings,
178 pub date_expires: Timestamp,
179}
180
181impl<'de> Deserialize<'de> for Statistics {
182 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
183 #[derive(Deserialize)]
184 #[serde(field_identifier, rename_all = "snake_case")]
185 enum Field {
186 ModId,
187 DownloadsToday,
188 DownloadsTotal,
189 SubscribersTotal,
190 PopularityRankPosition,
191 PopularityRankTotalMods,
192 RatingsTotal,
193 RatingsPositive,
194 RatingsNegative,
195 RatingsPercentagePositive,
196 RatingsWeightedAggregate,
197 RatingsDisplayText,
198 DateExpires,
199 #[allow(dead_code)]
200 Other(String),
201 }
202
203 struct StatisticsVisitor;
204
205 impl<'de> Visitor<'de> for StatisticsVisitor {
206 type Value = Statistics;
207
208 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
209 formatter.write_str("struct Statistics")
210 }
211
212 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
213 let mut mod_id = None;
214 let mut downloads_today = None;
215 let mut downloads_total = None;
216 let mut subscribers_total = None;
217 let mut rank_position = None;
218 let mut rank_total = None;
219 let mut ratings_total = None;
220 let mut ratings_positive = None;
221 let mut ratings_negative = None;
222 let mut ratings_percentage_positive = None;
223 let mut ratings_weighted_aggregate = None;
224 let mut ratings_display_text = None;
225 let mut date_expires = None;
226
227 while let Some(key) = map.next_key()? {
228 match key {
229 Field::ModId => {
230 mod_id.deserialize_value("mod_id", &mut map)?;
231 }
232 Field::DownloadsToday => {
233 downloads_today.deserialize_value("downloads_today", &mut map)?;
234 }
235 Field::DownloadsTotal => {
236 downloads_total.deserialize_value("downloads_total", &mut map)?;
237 }
238 Field::SubscribersTotal => {
239 subscribers_total.deserialize_value("subscribers_total", &mut map)?;
240 }
241 Field::PopularityRankPosition => {
242 rank_position
243 .deserialize_value("popularity_rank_position", &mut map)?;
244 }
245 Field::PopularityRankTotalMods => {
246 rank_total.deserialize_value("popularity_rank_total_mods", &mut map)?;
247 }
248 Field::RatingsTotal => {
249 ratings_total.deserialize_value("ratings_total", &mut map)?;
250 }
251 Field::RatingsPositive => {
252 ratings_positive.deserialize_value("ratings_positive", &mut map)?;
253 }
254 Field::RatingsNegative => {
255 ratings_negative.deserialize_value("ratings_negative", &mut map)?;
256 }
257 Field::RatingsPercentagePositive => {
258 ratings_percentage_positive
259 .deserialize_value("ratings_percentage_positive", &mut map)?;
260 }
261 Field::RatingsWeightedAggregate => {
262 ratings_weighted_aggregate
263 .deserialize_value("ratings_weighted_aggregate", &mut map)?;
264 }
265 Field::RatingsDisplayText => {
266 ratings_display_text
267 .deserialize_value("ratings_display_text", &mut map)?;
268 }
269 Field::DateExpires => {
270 date_expires.deserialize_value("date_expires", &mut map)?;
271 }
272 Field::Other(_) => {
273 map.next_value::<IgnoredAny>()?;
274 }
275 }
276 }
277
278 let mod_id = mod_id.missing_field("mod_id")?;
279 let downloads_today = downloads_today.missing_field("downloads_today")?;
280 let downloads_total = downloads_total.missing_field("downloads_total")?;
281 let subscribers_total = subscribers_total.missing_field("subscribers_total")?;
282 let rank_position = rank_position.missing_field("popularity_rank_position")?;
283 let rank_total = rank_total.missing_field("popularity_rank_total_mods")?;
284 let ratings_total = ratings_total.missing_field("ratings_total")?;
285 let ratings_positive = ratings_positive.missing_field("ratings_positive")?;
286 let ratings_negative = ratings_negative.missing_field("ratings_negative")?;
287 let ratings_percentage_positive =
288 ratings_percentage_positive.missing_field("ratings_percentage_positive")?;
289 let ratings_weighted_aggregate =
290 ratings_weighted_aggregate.missing_field("ratings_weighted_aggregate")?;
291 let ratings_display_text =
292 ratings_display_text.missing_field("ratings_display_text")?;
293 let date_expires = date_expires.missing_field("date_expires")?;
294
295 Ok(Statistics {
296 mod_id,
297 downloads_today,
298 downloads_total,
299 subscribers_total,
300 popularity: Popularity {
301 rank_position,
302 rank_total,
303 },
304 ratings: Ratings {
305 total: ratings_total,
306 positive: ratings_positive,
307 negative: ratings_negative,
308 percentage_positive: ratings_percentage_positive,
309 weighted_aggregate: ratings_weighted_aggregate,
310 display_text: ratings_display_text,
311 },
312 date_expires,
313 })
314 }
315 }
316
317 deserializer.deserialize_map(StatisticsVisitor)
318 }
319}
320
321#[derive(Debug)]
323#[non_exhaustive]
324pub struct Popularity {
325 pub rank_position: u32,
326 pub rank_total: u32,
327}
328
329#[derive(Debug)]
331#[non_exhaustive]
332pub struct Ratings {
333 pub total: u32,
334 pub positive: u32,
335 pub negative: u32,
336 pub percentage_positive: u32,
337 pub weighted_aggregate: f32,
338 pub display_text: String,
339}
340
341#[derive(Debug)]
343#[non_exhaustive]
344pub enum Rating {
345 Positive {
346 game_id: GameId,
347 mod_id: ModId,
348 date_added: Timestamp,
349 },
350 Negative {
351 game_id: GameId,
352 mod_id: ModId,
353 date_added: Timestamp,
354 },
355}
356
357impl<'de> Deserialize<'de> for Rating {
358 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
359 where
360 D: Deserializer<'de>,
361 {
362 use serde::de::Error;
363
364 #[derive(Deserialize)]
365 struct R {
366 game_id: GameId,
367 mod_id: ModId,
368 rating: i8,
369 date_added: Timestamp,
370 }
371
372 match R::deserialize(deserializer) {
373 Ok(R {
374 game_id,
375 mod_id,
376 rating: 1,
377 date_added,
378 }) => Ok(Self::Positive {
379 game_id,
380 mod_id,
381 date_added,
382 }),
383 Ok(R {
384 game_id,
385 mod_id,
386 rating: -1,
387 date_added,
388 }) => Ok(Self::Negative {
389 game_id,
390 mod_id,
391 date_added,
392 }),
393 Ok(R { rating, .. }) => {
394 Err(D::Error::custom(format!("invalid rating value: {rating}")))
395 }
396 Err(e) => Err(e),
397 }
398 }
399}
400
401#[derive(Debug, Deserialize)]
403#[non_exhaustive]
404pub struct Platform {
405 #[serde(rename = "platform")]
406 pub target: TargetPlatform,
407 #[serde(rename = "modfile_live")]
410 pub modfile_id: FileId,
411}
412
413#[derive(Debug, Deserialize)]
415#[non_exhaustive]
416pub struct Tag {
417 pub name: String,
418 pub date_added: Timestamp,
419}
420
421impl fmt::Display for Tag {
422 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
423 self.name.fmt(fmt)
424 }
425}
426
427#[derive(Debug, Clone, Default, PartialEq)]
430pub struct MetadataMap(HashMap<String, Vec<String>>);
431
432impl MetadataMap {
433 pub fn new() -> Self {
434 Self::default()
435 }
436
437 pub fn with_capacity(capacity: usize) -> Self {
438 Self(HashMap::with_capacity(capacity))
439 }
440}
441
442impl std::ops::Deref for MetadataMap {
443 type Target = HashMap<String, Vec<String>>;
444
445 fn deref(&self) -> &Self::Target {
446 &self.0
447 }
448}
449
450impl std::ops::DerefMut for MetadataMap {
451 fn deref_mut(&mut self) -> &mut Self::Target {
452 &mut self.0
453 }
454}
455
456impl<'de> Deserialize<'de> for MetadataMap {
482 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
483 #[derive(Debug)]
484 enum ListField {
485 Data,
486 Other,
487 }
488
489 impl<'de> Deserialize<'de> for ListField {
490 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
491 struct FieldVisitor;
492
493 impl Visitor<'_> for FieldVisitor {
494 type Value = ListField;
495
496 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
497 formatter.write_str("`data` field")
498 }
499
500 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
501 match value {
502 "data" => Ok(ListField::Data),
503 _ => Ok(ListField::Other),
504 }
505 }
506 }
507
508 deserializer.deserialize_identifier(FieldVisitor)
509 }
510 }
511
512 struct MetadataVisitor;
513
514 impl<'de> Visitor<'de> for MetadataVisitor {
515 type Value = MetadataMap;
516
517 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
518 formatter.write_str("metadata kvp")
519 }
520
521 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
522 let mut data = None;
523
524 while let Some(key) = map.next_key()? {
525 match key {
526 ListField::Data => {
527 data.deserialize_value("data", &mut map)?;
528 }
529 ListField::Other => {
530 map.next_value::<IgnoredAny>()?;
531 }
532 }
533 }
534
535 let map = data.missing_field("data")?;
536 Ok(map)
537 }
538
539 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
540 #[derive(Deserialize)]
541 struct Element {
542 metakey: String,
543 metavalue: String,
544 }
545
546 let mut map = seq
547 .size_hint()
548 .map_or_else(MetadataMap::new, MetadataMap::with_capacity);
549
550 while let Some(Element { metakey, metavalue }) = seq.next_element()? {
551 map.entry(metakey).or_default().push(metavalue);
552 }
553
554 Ok(map)
555 }
556 }
557
558 deserializer.deserialize_any(MetadataVisitor)
559 }
560}
561
562#[derive(Debug, Deserialize)]
564#[non_exhaustive]
565pub struct Comment {
566 pub id: CommentId,
567 pub resource_id: ResourceId,
568 pub user: User,
569 pub date_added: Timestamp,
570 pub reply_id: CommentId,
571 pub thread_position: String,
572 pub karma: i32,
573 pub content: String,
574}
575
576#[derive(Debug, Deserialize)]
579#[non_exhaustive]
580pub struct TeamMember {
581 pub id: MemberId,
582 pub user: User,
583 pub level: TeamLevel,
584 pub date_added: Timestamp,
585 pub position: String,
586}
587
588newtype_enum! {
589 pub struct TeamLevel: u8 {
591 const MODERATOR = 1;
592 const CREATOR = 4;
593 const ADMIN = 8;
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use serde_test::{assert_de_tokens, Token};
600
601 use super::{EventType, MetadataMap};
602 use crate::types::List;
603
604 #[test]
605 fn metadata_from_result_list_serde() {
606 #[derive(Debug, PartialEq, serde_derive::Deserialize)]
607 struct Entry {
608 metakey: String,
609 metavalue: String,
610 }
611
612 let list = List {
613 data: vec![
614 Entry {
615 metakey: "foo".to_owned(),
616 metavalue: "bar".to_owned(),
617 },
618 Entry {
619 metakey: "foo".to_owned(),
620 metavalue: "baz".to_owned(),
621 },
622 ],
623 count: 2,
624 offset: 0,
625 limit: 100,
626 total: 2,
627 };
628
629 let mut map = MetadataMap::new();
630 map.entry("foo".to_owned())
631 .or_insert(vec!["bar".to_owned(), "baz".to_owned()]);
632
633 let tokens = &[
634 Token::Struct {
635 name: "List",
636 len: 5,
637 },
638 Token::Str("data"),
639 Token::Seq { len: Some(2) },
640 Token::Map { len: Some(2) },
641 Token::Str("metakey"),
642 Token::Str("foo"),
643 Token::Str("metavalue"),
644 Token::Str("bar"),
645 Token::MapEnd,
646 Token::Map { len: Some(2) },
647 Token::Str("metakey"),
648 Token::Str("foo"),
649 Token::Str("metavalue"),
650 Token::Str("baz"),
651 Token::MapEnd,
652 Token::SeqEnd,
653 Token::Str("result_count"),
654 Token::U64(2),
655 Token::Str("result_offset"),
656 Token::U64(0),
657 Token::Str("result_limit"),
658 Token::U64(100),
659 Token::Str("result_total"),
660 Token::U64(2),
661 Token::StructEnd,
662 ];
663
664 assert_de_tokens(&list, tokens);
665 assert_de_tokens(&map, tokens);
666 }
667
668 #[test]
669 fn metadata_from_mod_serde() {
670 #[derive(Debug, PartialEq, serde_derive::Deserialize)]
671 struct Mod {
672 id: u32,
673 #[serde(rename = "metadata_kvp")]
674 metadata: MetadataMap,
675 }
676
677 let mut map = MetadataMap::new();
678 map.entry("foo".to_owned())
679 .or_insert(vec!["bar".to_owned(), "baz".to_owned()]);
680
681 let mod_ = Mod {
682 id: 2,
683 metadata: map,
684 };
685 assert_de_tokens(
686 &mod_,
687 &[
688 Token::Struct {
689 name: "Mod",
690 len: 1,
691 },
692 Token::Str("id"),
693 Token::U32(2),
694 Token::Str("metadata_kvp"),
695 Token::Seq { len: Some(2) },
696 Token::Map { len: Some(2) },
697 Token::Str("metakey"),
698 Token::Str("foo"),
699 Token::Str("metavalue"),
700 Token::Str("bar"),
701 Token::MapEnd,
702 Token::Map { len: Some(2) },
703 Token::Str("metakey"),
704 Token::Str("foo"),
705 Token::Str("metavalue"),
706 Token::Str("baz"),
707 Token::MapEnd,
708 Token::SeqEnd,
709 Token::StructEnd,
710 ],
711 );
712 }
713
714 #[test]
715 fn mod_event_type_serde() {
716 assert_de_tokens(
717 &EventType::MODFILE_CHANGED,
718 &[Token::Str("MODFILE_CHANGED")],
719 );
720 assert_de_tokens(&EventType::MOD_AVAILABLE, &[Token::Str("MOD_AVAILABLE")]);
721 assert_de_tokens(
722 &EventType::MOD_UNAVAILABLE,
723 &[Token::Str("MOD_UNAVAILABLE")],
724 );
725 assert_de_tokens(&EventType::MOD_EDITED, &[Token::Str("MOD_EDITED")]);
726 assert_de_tokens(&EventType::MOD_DELETED, &[Token::Str("MOD_DELETED")]);
727 assert_de_tokens(
728 &EventType::MOD_TEAM_CHANGED,
729 &[Token::Str("MOD_TEAM_CHANGED")],
730 );
731 assert_de_tokens(
732 &EventType::MOD_COMMENT_ADDED,
733 &[Token::Str("MOD_COMMENT_ADDED")],
734 );
735 assert_de_tokens(
736 &EventType::MOD_COMMENT_DELETED,
737 &[Token::Str("MOD_COMMENT_DELETED")],
738 );
739 assert_de_tokens(&EventType::from_bytes(b"foo"), &[Token::Str("foo")]);
740 }
741}