r6tracker/types/
user_data.rs

1/// Season data type
2pub mod season;
3
4pub use self::season::Season;
5use crate::{
6    types::platform::Platform,
7    Stat,
8};
9use std::collections::HashMap;
10use url::Url;
11
12/// A json response from the UserData API.
13#[derive(Debug)]
14pub enum ApiResponse<T> {
15    /// A Valid Response
16    Valid(T),
17
18    /// An Invalid Response
19    Invalid(InvalidApiResponseError),
20}
21
22#[derive(Debug)]
23pub struct InvalidApiResponseError(pub Vec<ApiError>);
24
25impl std::error::Error for InvalidApiResponseError {}
26
27impl std::fmt::Display for InvalidApiResponseError {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        writeln!(f, "the api request failed due to the following: ")?;
30        for error in self.0.iter() {
31            writeln!(f, "    {}", error.message)?;
32        }
33
34        Ok(())
35    }
36}
37
38/// Errors that occured while procesing an API Request
39#[derive(serde::Deserialize, Debug)]
40pub struct ApiError {
41    /// The error message
42    pub message: String,
43}
44
45impl std::fmt::Display for ApiError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "api error ({})", self.message)
48    }
49}
50
51impl std::error::Error for ApiError {}
52
53impl<'de, T> serde::Deserialize<'de> for ApiResponse<T>
54where
55    T: serde::Deserialize<'de>,
56{
57    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
58    where
59        D: serde::Deserializer<'de>,
60    {
61        let mut map = serde_json::Map::deserialize(deserializer)?;
62
63        let data: Option<Result<T, _>> = map
64            .remove("data")
65            .map(|data| serde::Deserialize::deserialize(data).map_err(serde::de::Error::custom));
66        let rest = serde_json::Value::Object(map);
67
68        match data {
69            Some(data) => Ok(Self::Valid(data?)),
70            None => {
71                #[derive(serde::Deserialize)]
72                struct ErrorReason {
73                    errors: Vec<ApiError>,
74                }
75
76                ErrorReason::deserialize(rest)
77                    .map(|e| Self::Invalid(InvalidApiResponseError(e.errors)))
78                    .map_err(serde::de::Error::custom)
79            }
80        }
81    }
82}
83
84impl<T> ApiResponse<T> {
85    /// Convert this into as Result.
86    pub fn into_result(self) -> Result<T, InvalidApiResponseError> {
87        match self {
88            Self::Valid(data) => Ok(data),
89            Self::Invalid(err) => Err(err),
90        }
91    }
92
93    /// Consume self and return the valid variant, or None.
94    pub fn take_valid(self) -> Option<T> {
95        match self {
96            Self::Valid(data) => Some(data),
97            Self::Invalid(_) => None,
98        }
99    }
100
101    /// Consume self and return the invalid variant, or None.
102    pub fn take_invalid(self) -> Option<InvalidApiResponseError> {
103        match self {
104            Self::Valid(_) => None,
105            Self::Invalid(err) => Some(err),
106        }
107    }
108}
109
110#[allow(clippy::upper_case_acronyms)]
111/// An R6 Rank.
112#[derive(Debug, Copy, Clone, Eq, PartialEq)]
113pub enum Rank {
114    Unranked,
115
116    CopperV,
117    CopperIV,
118    CopperIII,
119    CopperII,
120    CopperI,
121
122    BronzeV,
123    BronzeIV,
124    BronzeIII,
125    BronzeII,
126    BronzeI,
127
128    SilverV,
129    SilverIV,
130    SilverIII,
131    SilverII,
132    SilverI,
133
134    GoldIII,
135    GoldII,
136    GoldI,
137
138    PlatinumIII,
139    PlatinumII,
140    PlatinumI,
141
142    Diamond,
143
144    Champion,
145}
146
147impl Rank {
148    /// Get a string rep of this rank
149    pub fn name(self) -> &'static str {
150        match self {
151            Self::Unranked => "Unranked",
152
153            Self::CopperV => "Copper V",
154            Self::CopperIV => "Copper IV",
155            Self::CopperIII => "Copper III",
156            Self::CopperII => "Copper II",
157            Self::CopperI => "Copper I",
158
159            Self::BronzeV => "Bronze V",
160            Self::BronzeIV => "Bronze IV",
161            Self::BronzeIII => "Bronze III",
162            Self::BronzeII => "Bronze II",
163            Self::BronzeI => "Bronze I",
164
165            Self::SilverV => "Silver V",
166            Self::SilverIV => "Silver IV",
167            Self::SilverIII => "Silver III",
168            Self::SilverII => "Silver II",
169            Self::SilverI => "Silver I",
170
171            Self::GoldIII => "Gold III",
172            Self::GoldII => "Gold II",
173            Self::GoldI => "Gold I",
174
175            Self::PlatinumIII => "Platinum III",
176            Self::PlatinumII => "Platinum II",
177            Self::PlatinumI => "Platinum I",
178
179            Self::Diamond => "Diamond",
180
181            Self::Champion => "Champion",
182        }
183    }
184}
185
186#[derive(Debug, serde::Deserialize, serde::Serialize)]
187pub struct UserData {
188    /// Unique user id
189    pub id: String,
190
191    #[serde(rename = "type")]
192    pub kind: String,
193
194    /// Collection of ranked seasons stats
195    pub children: Vec<Season>,
196
197    /// Metadata
198    pub metadata: Metadata,
199
200    /// A collection of all stats
201    pub stats: Vec<Stat>,
202
203    /// Unknown fields
204    #[serde(flatten)]
205    pub unknown: HashMap<String, serde_json::Value>,
206}
207
208impl UserData {
209    /// Utility function to get a stat by name. Currently an O(n) linear search.
210    fn get_stat_by_name(&self, name: &str) -> Option<&Stat> {
211        self.stats.iter().find(|s| s.name() == name)
212    }
213
214    /// Gets top mmr from all servers.
215    pub fn current_mmr(&self) -> Option<u32> {
216        self.get_stat_by_name("MMR").map(|s| s.value as u32)
217    }
218
219    /// Get the image url for the rank this user is at gloablly
220    pub fn current_mmr_image(&self) -> Option<&Url> {
221        self.get_stat_by_name("Global MMR")
222            .and_then(|s| s.icon_url())
223    }
224
225    /// Get the MMR for this user.
226    pub fn current_mmr_america(&self) -> Option<u32> {
227        self.get_stat_by_name("Global MMR").map(|s| s.value as u32)
228    }
229
230    /// Gets this season's color as a string hex value
231    pub fn season_color(&self) -> &str {
232        &self.metadata.current_season_color
233    }
234
235    /// Tries to parse this season's hex color as a u32
236    pub fn season_color_u32(&self) -> Option<u32> {
237        u32::from_str_radix(self.season_color().get(1..)?, 16).ok()
238    }
239
240    /// Get total # of kills
241    pub fn get_kills(&self) -> Option<u64> {
242        self.get_stat_by_name("Kills").map(|s| s.value as u64)
243    }
244
245    /// Get total # of deaths
246    pub fn get_deaths(&self) -> Option<u64> {
247        self.get_stat_by_name("Deaths").map(|s| s.value as u64)
248    }
249
250    /// Get overall K/D
251    pub fn kd(&self) -> Option<f64> {
252        self.get_stat_by_name("KD Ratio").map(|s| s.value)
253    }
254
255    /// Get Overall W/L
256    pub fn wl(&self) -> Option<f64> {
257        self.get_stat_by_name("WL Ratio").map(|s| s.value)
258    }
259
260    /// Get user tag name
261    pub fn name(&self) -> &str {
262        &self.metadata.platform_user_handle
263    }
264
265    /// Get user avatar url
266    pub fn avatar_url(&self) -> &Url {
267        &self.metadata.picture_url
268    }
269
270    /// Get the latest stats for the latest ranked region/season the user has played in
271    pub fn get_latest_season(&self) -> Option<&Season> {
272        let target_id = format!(
273            "region-{}.season-{}",
274            self.metadata.latest_region.unwrap_or(100),
275            self.metadata.latest_season
276        );
277
278        self.children.iter().find(|s| s.id == target_id)
279    }
280
281    /// Get the season where the user attained their max ranking
282    pub fn get_max_season(&self) -> Option<&Season> {
283        self.children
284            .iter()
285            .filter_map(|child| child.max_mmr().map(|mmr| (child, mmr)))
286            .max_by_key(|(_, mmr)| *mmr)
287            .map(|(child, _)| child)
288    }
289}
290
291#[derive(Debug, serde::Deserialize, serde::Serialize)]
292pub struct Metadata {
293    #[serde(rename = "accountId")]
294    pub account_id: String,
295
296    #[serde(rename = "countryCode")]
297    pub country_code: Option<String>,
298
299    #[serde(rename = "currentSeasonColor")]
300    pub current_season_color: String,
301
302    #[serde(rename = "currentSeasonName")]
303    pub current_season_name: String,
304
305    #[serde(rename = "latestRegion")]
306    pub latest_region: Option<u32>,
307
308    #[serde(rename = "latestSeason")]
309    pub latest_season: u32,
310
311    #[serde(rename = "pictureUrl")]
312    pub picture_url: Url,
313
314    #[serde(rename = "platformId")]
315    pub platform_id: Platform,
316
317    #[serde(rename = "platformUserHandle")]
318    pub platform_user_handle: String,
319
320    #[serde(rename = "segmentControls")]
321    pub segment_controls: Vec<serde_json::Value>,
322
323    #[serde(rename = "statsCategoryOrder")]
324    pub stats_category_order: Vec<String>,
325
326    #[serde(flatten)]
327    pub unknown: HashMap<String, serde_json::Value>,
328}
329
330#[cfg(test)]
331mod test {
332    use super::*;
333    use crate::types::ApiResponse;
334
335    const SAMPLE_1: &str = include_str!("../../test_data/user_data_1.json");
336    const SAMPLE_2: &str = include_str!("../../test_data/user_data_2.json");
337    const INVALID_USER_DATA: &str = include_str!("../../test_data/invalid_user_data.json");
338    const SMACK_ASH_USER_DATA: &str = include_str!("../../test_data/smack_ash_user_data.json");
339
340    #[test]
341    fn parse_sample_1() {
342        let data = serde_json::from_str::<ApiResponse<UserData>>(SAMPLE_1)
343            .unwrap()
344            .take_valid()
345            .unwrap();
346        let season = data.get_latest_season().unwrap();
347        dbg!(season);
348
349        let max_season = data.get_max_season().unwrap();
350        dbg!(max_season.max_mmr());
351        dbg!(max_season.max_rank());
352    }
353
354    #[test]
355    fn parse_sample_2() {
356        let data = serde_json::from_str::<ApiResponse<UserData>>(SAMPLE_2)
357            .unwrap()
358            .take_valid()
359            .unwrap();
360        let season = data.get_latest_season().unwrap();
361
362        dbg!(season);
363    }
364
365    #[test]
366    fn parse_smack_ash_user_data() {
367        let data = serde_json::from_str::<ApiResponse<UserData>>(SMACK_ASH_USER_DATA)
368            .unwrap()
369            .take_valid()
370            .unwrap();
371        assert!(data.get_latest_season().is_none());
372    }
373
374    #[test]
375    fn parse_invalid_sample() {
376        let data = serde_json::from_str::<ApiResponse<UserData>>(INVALID_USER_DATA).unwrap();
377
378        dbg!(data);
379    }
380}