1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
pub mod generic_stats;

pub use self::generic_stats::GenericStats;
use std::collections::HashMap;
use time::OffsetDateTime;
use url::Url;

/// Api Response
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct ApiResponse<T> {
    pub data: T,
}

/// User Data
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct UserData {
    pub avatar_banned: bool,
    pub avatar_url_146: Url,
    pub avatar_url_256: Url,
    pub claimed: bool,

    #[serde(rename = "genericStats")]
    pub generic_stats: Option<GenericStats>,

    #[serde(
        deserialize_with = "time::serde::rfc3339::deserialize",
        serialize_with = "time::serde::rfc3339::serialize"
    )]
    pub last_updated: OffsetDateTime,
    pub platform: String,

    #[serde(rename = "progressionStats")]
    pub progression_stats: Option<ProgressionStats>,

    #[serde(rename = "seasonalStats")]
    pub seasonal_stats: Option<SeasonalStats>,

    pub ubisoft_id: String,
    pub uplay_id: String,

    pub username: String,

    #[serde(flatten)]
    pub unknown: HashMap<String, serde_json::Value>,
}

impl UserData {
    pub fn kd(&self) -> Option<f64> {
        Some(self.generic_stats.as_ref()?.general.kd)
    }

    pub fn wl(&self) -> Option<f64> {
        Some(self.generic_stats.as_ref()?.general.wl)
    }

    pub fn mmr(&self) -> Option<u32> {
        Some(self.seasonal_stats.as_ref()?.mmr as u32)
    }
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct ProgressionStats {
    pub level: u32,
    pub lootbox_probability: u32,
    pub total_xp: u64,

    #[serde(flatten)]
    pub unknown: HashMap<String, serde_json::Value>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct SeasonalStats {
    pub abandons: u32,
    pub champions_rank_position: Option<u32>,
    #[serde(
        deserialize_with = "time::serde::rfc3339::deserialize",
        serialize_with = "time::serde::rfc3339::serialize"
    )]
    pub created_at: OffsetDateTime,
    #[serde(
        deserialize_with = "time::serde::rfc3339::deserialize",
        serialize_with = "time::serde::rfc3339::serialize"
    )]
    pub created_for_date: OffsetDateTime,
    pub deaths: Option<u32>,
    pub kills: Option<u32>,
    pub last_match_mmr_change: Option<i32>,
    pub last_match_skill_mean_change: Option<f64>,
    pub last_match_skill_standard_deviation_change: Option<f64>,
    pub losses: u32,
    pub max_mmr: f64,
    pub max_rank: u32,
    pub mmr: f64,
    pub next_rank_mmr: u32,
    pub prev_rank_mmr: u32,
    pub rank: u32,
    pub region: String,
    pub skill_mean: f64,
    pub skill_standard_deviation: f64,
    #[serde(
        deserialize_with = "time::serde::rfc3339::deserialize",
        serialize_with = "time::serde::rfc3339::serialize"
    )]
    pub updated_at: OffsetDateTime,
    pub wins: u32,

    #[serde(flatten)]
    pub unknown: HashMap<String, serde_json::Value>,
}

#[cfg(test)]
mod test {
    use super::*;

    const VALID_DATA: &str = include_str!("../../test_data/search_data_valid.json");
    const SEARCH_ASDF_DATA: &str = include_str!("../../test_data/search_asdf.json");

    #[tokio::test]
    async fn parse_valid() {
        let valid = serde_json::from_str::<Vec<UserData>>(VALID_DATA)
            .expect("failed to parse valid search data");
        dbg!(&valid);
    }

    #[tokio::test]
    async fn parse_asdf() {
        let valid = serde_json::from_str::<Vec<UserData>>(SEARCH_ASDF_DATA).unwrap();
        dbg!(&valid);
    }
}