1pub 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#[derive(Debug)]
14pub enum ApiResponse<T> {
15 Valid(T),
17
18 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#[derive(serde::Deserialize, Debug)]
40pub struct ApiError {
41 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 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 pub fn take_valid(self) -> Option<T> {
95 match self {
96 Self::Valid(data) => Some(data),
97 Self::Invalid(_) => None,
98 }
99 }
100
101 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#[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 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 pub id: String,
190
191 #[serde(rename = "type")]
192 pub kind: String,
193
194 pub children: Vec<Season>,
196
197 pub metadata: Metadata,
199
200 pub stats: Vec<Stat>,
202
203 #[serde(flatten)]
205 pub unknown: HashMap<String, serde_json::Value>,
206}
207
208impl UserData {
209 fn get_stat_by_name(&self, name: &str) -> Option<&Stat> {
211 self.stats.iter().find(|s| s.name() == name)
212 }
213
214 pub fn current_mmr(&self) -> Option<u32> {
216 self.get_stat_by_name("MMR").map(|s| s.value as u32)
217 }
218
219 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 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 pub fn season_color(&self) -> &str {
232 &self.metadata.current_season_color
233 }
234
235 pub fn season_color_u32(&self) -> Option<u32> {
237 u32::from_str_radix(self.season_color().get(1..)?, 16).ok()
238 }
239
240 pub fn get_kills(&self) -> Option<u64> {
242 self.get_stat_by_name("Kills").map(|s| s.value as u64)
243 }
244
245 pub fn get_deaths(&self) -> Option<u64> {
247 self.get_stat_by_name("Deaths").map(|s| s.value as u64)
248 }
249
250 pub fn kd(&self) -> Option<f64> {
252 self.get_stat_by_name("KD Ratio").map(|s| s.value)
253 }
254
255 pub fn wl(&self) -> Option<f64> {
257 self.get_stat_by_name("WL Ratio").map(|s| s.value)
258 }
259
260 pub fn name(&self) -> &str {
262 &self.metadata.platform_user_handle
263 }
264
265 pub fn avatar_url(&self) -> &Url {
267 &self.metadata.picture_url
268 }
269
270 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 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}