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