1pub use isocountry::CountryCode;
6use serde_json::Value;
7
8use std::{
9 collections::{BTreeMap, HashMap},
10 fmt::Display,
11 ops::Deref,
12};
13
14use serde::{Deserialize, Serialize};
15
16use crate::{
17 profile, profile_games,
18 query::{ProfileGamesQuery, ProfileQuery},
19 types::rank::League,
20};
21
22use super::civilization::Civilization;
23
24#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Copy)]
26#[serde(rename_all = "snake_case")]
27#[cfg_attr(test, derive(arbitrary::Arbitrary))]
28#[cfg_attr(test, serde(deny_unknown_fields))]
29pub struct ProfileId(u64);
30
31impl Display for ProfileId {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 self.0.fmt(f)
34 }
35}
36
37impl AsRef<u64> for ProfileId {
38 fn as_ref(&self) -> &u64 {
39 &self.0
40 }
41}
42
43impl From<u64> for ProfileId {
44 fn from(value: u64) -> Self {
45 ProfileId(value)
46 }
47}
48
49impl From<ProfileId> for u64 {
50 fn from(value: ProfileId) -> Self {
51 value.0
52 }
53}
54
55impl From<&u64> for ProfileId {
56 fn from(value: &u64) -> Self {
57 ProfileId(*value)
58 }
59}
60
61impl From<&ProfileId> for u64 {
62 fn from(value: &ProfileId) -> Self {
63 value.0
64 }
65}
66
67impl ProfileId {
68 pub fn profile(&self) -> ProfileQuery {
70 profile(self.0)
71 }
72
73 pub fn games(&self) -> ProfileGamesQuery {
75 profile_games(self.0)
76 }
77}
78
79#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
81#[serde(rename_all = "snake_case")]
82#[cfg_attr(test, derive(arbitrary::Arbitrary))]
83#[cfg_attr(test, serde(deny_unknown_fields))]
84pub struct Profile {
85 pub name: String,
87 pub profile_id: ProfileId,
89 pub steam_id: Option<String>,
91 pub site_url: Option<String>,
93 pub avatars: Option<Avatars>,
95 pub social: Option<Social>,
97 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::option_country))]
99 pub country: Option<CountryCode>,
100 #[serde(alias = "leaderboards")]
102 pub modes: Option<GameModes>,
103 pub last_game_at: Option<chrono::DateTime<chrono::Utc>>,
105}
106
107impl Deref for Profile {
108 type Target = ProfileId;
109
110 fn deref(&self) -> &Self::Target {
111 &self.profile_id
112 }
113}
114
115#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
117#[serde(rename_all = "snake_case")]
118#[cfg_attr(test, derive(arbitrary::Arbitrary))]
119#[cfg_attr(test, serde(deny_unknown_fields))]
120pub struct Avatars {
121 pub small: Option<String>,
123 pub medium: Option<String>,
125 pub full: Option<String>,
127}
128
129#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
131#[serde(rename_all = "snake_case")]
132#[cfg_attr(test, derive(arbitrary::Arbitrary))]
133#[cfg_attr(test, serde(deny_unknown_fields))]
134pub struct Social {
135 pub twitch: Option<String>,
137 pub youtube: Option<String>,
139 pub liquipedia: Option<String>,
141 pub twitter: Option<String>,
143 pub reddit: Option<String>,
145 pub instagram: Option<String>,
147}
148
149#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
151#[serde(rename_all = "snake_case")]
152#[cfg_attr(test, derive(arbitrary::Arbitrary))]
153#[cfg_attr(test, serde(deny_unknown_fields))]
154pub struct GameModes {
155 pub rm_solo: Option<GameModeStats>,
157 pub rm_team: Option<GameModeStats>,
159 #[deprecated = "Use rm_solo instead."]
161 pub rm_1v1: Option<GameModeStats>,
162 pub rm_1v1_elo: Option<GameModeStats>,
164 #[serde(alias = "rm_2v2")]
166 pub rm_2v2_elo: Option<GameModeStats>,
167 #[serde(alias = "rm_3v3")]
169 pub rm_3v3_elo: Option<GameModeStats>,
170 #[serde(alias = "rm_4v4")]
172 pub rm_4v4_elo: Option<GameModeStats>,
173 pub qm_1v1: Option<GameModeStats>,
175 pub qm_2v2: Option<GameModeStats>,
177 pub qm_3v3: Option<GameModeStats>,
179 pub qm_4v4: Option<GameModeStats>,
181 pub qm_1v1_ew: Option<GameModeStats>,
183 pub qm_2v2_ew: Option<GameModeStats>,
185 pub qm_3v3_ew: Option<GameModeStats>,
187 pub qm_4v4_ew: Option<GameModeStats>,
189 pub custom: Option<GameModeStats>,
191}
192
193#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
195#[serde(rename_all = "snake_case")]
196#[cfg_attr(test, derive(arbitrary::Arbitrary))]
197#[cfg_attr(test, serde(deny_unknown_fields))]
198pub struct GameModeStats {
199 #[cfg(test)]
201 _notice_: Option<String>,
202 pub rating: Option<i64>,
204 pub max_rating: Option<i64>,
206 pub max_rating_7d: Option<i64>,
208 pub max_rating_1m: Option<i64>,
210 pub rank: Option<u32>,
212 pub streak: Option<i64>,
214 pub games_count: Option<u32>,
216 pub wins_count: Option<u32>,
218 pub losses_count: Option<u32>,
220 pub disputes_count: Option<u32>,
222 pub drops_count: Option<u32>,
224 pub last_game_at: Option<chrono::DateTime<chrono::Utc>>,
226 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
228 pub win_rate: Option<f64>,
229 pub rank_level: Option<League>,
231 #[serde(default)]
233 pub rating_history: BTreeMap<String, RatingHistoryEntry>,
234 #[serde(default)]
236 pub civilizations: Vec<CivStats>,
237 pub season: Option<u32>,
239 #[serde(default)]
242 pub previous_seasons: Vec<PreviousSeasonStats>,
243}
244
245#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
247#[serde(rename_all = "snake_case")]
248#[cfg_attr(test, derive(arbitrary::Arbitrary))]
249#[cfg_attr(test, serde(deny_unknown_fields))]
250pub struct PreviousSeasonStats {
251 pub rating: Option<u32>,
253 pub rank: Option<u32>,
255 pub streak: Option<i64>,
257 pub games_count: Option<u32>,
259 pub wins_count: Option<u32>,
261 pub losses_count: Option<u32>,
263 pub disputes_count: Option<u32>,
265 pub drops_count: Option<u32>,
267 pub last_game_at: Option<chrono::DateTime<chrono::Utc>>,
269 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
271 pub win_rate: Option<f64>,
272 pub rank_level: Option<League>,
274 pub season: Option<u32>,
276}
277
278#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
280#[serde(rename_all = "snake_case")]
281#[cfg_attr(test, derive(arbitrary::Arbitrary))]
282#[cfg_attr(test, serde(deny_unknown_fields))]
283pub struct RatingHistoryEntry {
284 pub rating: Option<u32>,
286 pub streak: Option<i64>,
288 pub games_count: Option<u32>,
290 pub wins_count: Option<u32>,
292 pub drops_count: Option<u32>,
294 pub disputes_count: Option<u32>,
296 pub orig_rating: Option<u32>,
298}
299
300#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
302#[serde(rename_all = "snake_case")]
303#[cfg_attr(test, derive(arbitrary::Arbitrary))]
304#[cfg_attr(test, serde(deny_unknown_fields))]
305pub struct CivStats {
306 pub civilization: Option<Civilization>,
308 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
310 pub win_rate: Option<f64>,
311 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
313 pub pick_rate: Option<f64>,
314 pub games_count: Option<u32>,
316 pub game_length: Option<CivGameLengthStats>,
318}
319
320#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
322#[serde(rename_all = "snake_case")]
323#[cfg_attr(test, derive(arbitrary::Arbitrary))]
324#[cfg_attr(test, serde(deny_unknown_fields))]
325pub struct CivGameLengthStats {
326 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
328 pub average: Option<f64>,
329 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
331 pub median: Option<f64>,
332 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
334 pub wins_average: Option<f64>,
335 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
337 pub wins_median: Option<f64>,
338 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
340 pub losses_average: Option<f64>,
341 #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
343 pub losses_median: Option<f64>,
344 #[cfg_attr(test, arbitrary(value = Vec::default()))]
346 breakdown: Vec<HashMap<String, Value>>,
347}
348
349#[cfg(test)]
350mod tests {
351 use crate::testutils::{test_json, test_serde_roundtrip_prop};
352
353 use super::*;
354
355 test_serde_roundtrip_prop!(ProfileId);
356 test_serde_roundtrip_prop!(Profile);
357 test_serde_roundtrip_prop!(Avatars);
358 test_serde_roundtrip_prop!(Social);
359 test_serde_roundtrip_prop!(GameModes);
360 test_serde_roundtrip_prop!(GameModeStats);
361 test_serde_roundtrip_prop!(PreviousSeasonStats);
362 test_serde_roundtrip_prop!(RatingHistoryEntry);
363 test_serde_roundtrip_prop!(CivStats);
364 test_serde_roundtrip_prop!(CivGameLengthStats);
365
366 test_json!(
367 Profile,
368 "../../testdata/profile/neptune.json",
369 neptune_profile
370 );
371
372 test_json!(
373 Profile,
374 "../../testdata/profile/housedhorse.json",
375 housedhorse_profile
376 );
377
378 test_json!(Profile, "../../testdata/profile/jigly.json", jigly_profile);
379}