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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

static BASE_URL: &str = "https://raider.io/api/v1";

pub mod gear;
pub mod mythic_plus;
pub mod player;
pub mod raid;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("http request failed")]
    Http(#[from] reqwest::Error),
    #[error("api error: {message}")]
    Api {
        status: u64,
        error: String,
        message: String,
    },
}

#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub enum Region {
    #[serde(rename = "us")]
    UnitedStates,
    #[serde(rename = "eu")]
    Europe,
    #[serde(rename = "kr")]
    Korea,
    #[serde(rename = "tw")]
    Taiwan,
}

#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize)]
pub enum Expansion {
    BattleForAzeroth,
}
impl Expansion {
    fn text(&self) -> &'static str {
        match self {
            Expansion::BattleForAzeroth => "bfa",
        }
    }
}

pub struct Client {
    http_client: reqwest::Client,
}
impl Client {
    pub fn new() -> Client {
        Client {
            http_client: reqwest::Client::new(),
        }
    }
    pub fn character_details<'s, 'i: 's>(
        &'s self,
        region: Region,
        name: &'i str,
        realm: &'i str,
    ) -> CharacterDetailsRequest<'s> {
        CharacterDetailsRequest {
            client: self,
            name,
            region,
            realm,
            fields: CharacterDetailsFields::default(),
        }
    }
}

struct CharacterDetailsFields {
    gear: bool,
    guild: bool,
    raid_progression: bool,
    mythic_plus_by_season: Option<HashSet<mythic_plus::Season>>,
    mythic_plus_ranks: bool,
    mythic_plus_recent_runs: bool,
    mythic_plus_best_runs: Option<mythic_plus::SeasonBestRuns>,
    mythic_plus_highest_runs: bool,
    mythic_plus_weekly_higest_runs: bool,
    mythic_plus_previous_week_highest_runs: bool,
    mythic_plus_previous_week_ranking: bool,
    raid_achievement_meta: Option<Vec<u8>>,
    raid_achievement_curve: Option<Vec<u8>>,
}

impl Default for CharacterDetailsFields {
    fn default() -> Self {
        CharacterDetailsFields {
            gear: false,
            guild: false,
            raid_progression: false,
            mythic_plus_ranks: false,
            mythic_plus_recent_runs: false,
            mythic_plus_weekly_higest_runs: false,
            mythic_plus_best_runs: None,
            mythic_plus_highest_runs: false,
            mythic_plus_previous_week_ranking: false,
            mythic_plus_previous_week_highest_runs: false,
            mythic_plus_by_season: None,
            raid_achievement_curve: None,
            raid_achievement_meta: None,
        }
    }
}
impl CharacterDetailsFields {
    fn text(&self) -> String {
        CharacterDetailsFieldsIter {
            fields: self,
            index: 0,
        }
        .join(",")
    }
}

macro_rules! s_str {
    ($e:expr) => {
        Some($e.to_owned())
    };
}
struct CharacterDetailsFieldsIter<'c> {
    fields: &'c CharacterDetailsFields,
    index: u8,
}
impl<'c> Iterator for CharacterDetailsFieldsIter<'c> {
    type Item = String;
    fn next(&mut self) -> Option<Self::Item> {
        if self.index > 12 {
            return None;
        }
        loop {
            let index = self.index;
            self.index += 1;
            match index {
                0 if self.fields.gear => return s_str!("gear"),
                1 if self.fields.guild => return s_str!("guild"),
                2 if self.fields.raid_progression => return s_str!("raid_progression"),
                3 if self.fields.mythic_plus_by_season.is_some() => {
                    let mut seasons = String::new();
                    for season in self.fields.mythic_plus_by_season.clone().unwrap() {
                        match season {
                            mythic_plus::Season::Previous => {
                                seasons = format!("{}:previous", seasons);
                            }
                            mythic_plus::Season::Current => {
                                seasons = format!("{}:current", seasons);
                            }
                            mythic_plus::Season::Specific { expansion, number } => {
                                seasons =
                                    format!("{}:season-{}-{}", seasons, expansion.text(), number);
                            }
                        }
                    }
                    return Some(format!("mythic_plus_scores_by_season{}", seasons));
                }
                4 if self.fields.mythic_plus_ranks => return s_str!("mythic_plus_ranks"),
                5 if self.fields.mythic_plus_recent_runs => {
                    return s_str!("mythic_plus_recent_runs")
                }
                6 if self.fields.mythic_plus_best_runs.is_some() => {
                    let mut field = "mythic_plus_best_runs".to_owned();
                    if let mythic_plus::SeasonBestRuns::All =
                        self.fields.mythic_plus_best_runs.unwrap()
                    {
                        field = format!("{}:all", field);
                    }
                    return Some(field);
                }
                7 if self.fields.mythic_plus_highest_runs => {
                    return s_str!("mythic_plus_highest_level_runs")
                }
                8 if self.fields.mythic_plus_weekly_higest_runs => {
                    return s_str!("mythic_plus_weekly_highest_level_runs")
                }
                9 if self.fields.mythic_plus_previous_week_highest_runs => {
                    return s_str!("mythic_plus_previous_weekly_highest_level_runs")
                }
                10 if self.fields.mythic_plus_previous_week_ranking => {
                    return s_str!("previous_mythic_plus_ranks")
                }
                11 if self.fields.raid_achievement_meta.is_some() => todo!(),
                12 if self.fields.raid_achievement_curve.is_some() => todo!(),
                0..=12 => continue,
                _ => return None,
            }
        }
    }
}
pub struct CharacterDetailsRequest<'c> {
    client: &'c Client,
    name: &'c str,
    region: Region,
    realm: &'c str,
    fields: CharacterDetailsFields,
}

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct CharacterDetails {
    pub name: String,
    pub race: player::Race,
    pub class: player::Class,
    pub active_spec_name: player::Spec,
    pub active_spec_role: player::Role,
    pub gender: player::Gender,
    pub faction: player::Faction,
    pub region: Region,
    pub realm: String,
    pub profile_url: String,
    pub achievement_points: u64,
    pub honorable_kills: u64,
    pub thumbnail_url: String,
    pub gear: Option<gear::Gear>,
    pub guild: Option<player::Guild>,
    pub raid_progression: Option<raid::RaidProgression>,
    pub mythic_plus_ranks: Option<mythic_plus::MythicPlusRanks>,
    pub mythic_plus_scores_by_season: Option<Vec<mythic_plus::MythicPlusScores>>,
    pub mythic_plus_recent_runs: Option<Vec<mythic_plus::KeystoneRun>>,
    pub mythic_plus_best_runs: Option<Vec<mythic_plus::KeystoneRun>>,
    pub mythic_plus_highest_level_runs: Option<Vec<mythic_plus::KeystoneRun>>,
    pub mythic_plus_weekly_highest_level_runs: Option<Vec<mythic_plus::KeystoneRun>>,
    pub mythic_plus_previous_weekly_highest_level_runs: Option<Vec<mythic_plus::KeystoneRun>>,
    pub previous_mythic_plus_ranks: Option<mythic_plus::MythicPlusRanks>,
}

#[derive(Serialize, Deserialize, Debug)]
struct CharacterQuery<'i> {
    name: &'i str,
    region: Region,
    realm: &'i str,
    fields: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
struct ApiError {
    #[serde(rename = "statusCode")]
    status_code: u64,
    error: String,
    message: String,
}

impl<'c> CharacterDetailsRequest<'c> {
    /// Clear all the fields from the request
    pub fn clear(mut self) -> Self {
        self.fields = Default::default();
        self
    }
    /// retrieve basic information about guild the player is in
    pub fn guild(mut self) -> Self {
        self.fields.guild = true;
        self
    }
    /// retrieve high level item information for player
    pub fn gear(mut self) -> Self {
        self.fields.gear = true;
        self
    }
    /// retrieve scores by mythic plus season
    pub fn mythic_plus_score_by_season(mut self, season: mythic_plus::Season) -> Self {
        match &mut self.fields.mythic_plus_by_season {
            None => {
                let mut set = HashSet::new();
                set.insert(season);
                self.fields.mythic_plus_by_season = Some(set);
            }
            Some(ref mut set) => {
                set.insert(season);
            }
        };
        self
    }

    /// current season mythic plus rankings for player.
    pub fn mythic_plus_ranks(mut self) -> Self {
        self.fields.mythic_plus_ranks = true;
        self
    }

    /// retrieve raid progression data for character
    pub fn raid_progression(mut self) -> Self {
        self.fields.raid_progression = true;
        self
    }

    /// retrieve three most recent mythic plus runs for player (current season only).
    pub fn mythic_plus_recent_runs(mut self) -> Self {
        self.fields.mythic_plus_recent_runs = true;
        self
    }

    /// retrieve all of a character's best runs for the season
    pub fn mythic_plus_all_best_runs(mut self) -> Self {
        self.fields.mythic_plus_best_runs = Some(mythic_plus::SeasonBestRuns::All);
        self
    }
    /// retrieve three most high scoring mythic plus runs for player (current season only).
    pub fn mythic_plus_three_best_runs(mut self) -> Self {
        self.fields.mythic_plus_best_runs = Some(mythic_plus::SeasonBestRuns::Three);
        self
    }
    /// retrieve the player's three highest Mythic+ runs by Mythic+ level (current season only)
    pub fn mythic_plus_highest_runs(mut self) -> Self {
        self.fields.mythic_plus_highest_runs = true;
        self
    }
    /// retrieve the player's three highest Mythic+ runs by Mythic+ level for the current raid week (current season only)
    pub fn mythic_plus_weekly_highest_level_runs(mut self) -> Self {
        self.fields.mythic_plus_weekly_higest_runs = true;
        self
    }
    /// retrieve the player's three highest Mythic+ runs by Mythic+ level for the previous raid week (current season only)
    pub fn mythic_plus_previous_weekly_highest_level_runs(mut self) -> Self {
        self.fields.mythic_plus_previous_week_highest_runs = true;
        self
    }
    /// retrieve mythic plus rankings for player.
    pub fn previous_mythic_plus_ranks(mut self) -> Self {
        self.fields.mythic_plus_previous_week_ranking = true;
        self
    }

    /// Execute the query on the raider.io website
    pub async fn get(&self) -> Result<CharacterDetails, Error> {
        let fields = self.fields.text();
        let fields = if fields.is_empty() {
            None
        } else {
            Some(fields)
        };

        let query = CharacterQuery {
            name: self.name,
            region: self.region,
            realm: self.realm,
            fields,
        };
        let response = self
            .client
            .http_client
            .get(&format!("{}/characters/profile", BASE_URL))
            .query(&query)
            .send()
            .await?;

        if response.status().is_client_error() {
            let response: ApiError = response.json().await?;
            Err(Error::Api {
                status: response.status_code,
                error: response.error,
                message: response.message,
            })
        } else if response.status().is_success() {
            response
                .json::<CharacterDetails>()
                .await
                .map_err(From::from)
        } else {
            panic!("Unhandled status code");
        }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}