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