brawl_api/model/rankings/players.rs
1//! Contains models for the `/rankings/:country_code/players?limit=x` Brawl Stars API endpoint.
2//! Included by the feature `"rankings"`; removing that feature will disable the usage of this module.
3
4use serde::{self, Serialize, Deserialize};
5use crate::traits::{PropLimRouteable, PropLimFetchable};
6use crate::serde::{one_default, oxffffff_default, deserialize_number_from_string};
7use std::ops::{Deref, DerefMut};
8use crate::util::fetch_route;
9use crate::error::Result;
10
11#[cfg(feature = "async")]
12use async_trait::async_trait;
13
14#[cfg(feature = "async")]
15use crate::util::a_fetch_route;
16use crate::http::Client;
17use crate::http::routes::Route;
18
19/// Represents a leaderboard of [`PlayerRanking`]s - the top x players in a regional or global
20/// leaderboard, sorted by total trophies.
21///
22/// **NOTE:** The API only allows fetching up to the top 200 players.
23///
24/// [`PlayerRanking`]: ./struct.PlayerRanking.html
25#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
26pub struct PlayerLeaderboard {
27 /// The players in the ranking.
28 #[serde(default)]
29 pub items: Vec<PlayerRanking>,
30}
31
32impl Deref for PlayerLeaderboard {
33 type Target = Vec<PlayerRanking>;
34
35 /// Obtain the players in the ranking - dereferencing returns the [`items`] field.
36 ///
37 /// # Examples
38 ///
39 /// ```rust,ignore
40 /// use brawl_api::{Client, PlayerLeaderboard, traits::*};
41 ///
42 /// # fn main() -> Result<(), Box<dyn ::std::error::Error>> {
43 /// let client = Client::new("my auth token");
44 /// let top50players = PlayerLeaderboard::fetch(
45 /// &client, // <- the client containing the auth key
46 /// "global", // <- the region of the leaderboard to fetch ("global" - world-wide)
47 /// 50 // <- limit of rankings to fetch (i.e. top 50)
48 /// )?;
49 ///
50 /// assert_eq!(top50players.items, *top50players);
51 ///
52 /// # Ok(())
53 /// # }
54 ///
55 /// ```
56 ///
57 /// [`items`]: #structfield.items
58 fn deref(&self) -> &Vec<PlayerRanking> {
59 &self.items
60 }
61}
62
63impl DerefMut for PlayerLeaderboard {
64 /// Obtain the players in the ranking - dereferencing returns the [`items`] field.
65 ///
66 /// # Examples
67 ///
68 /// ```rust,ignore
69 /// use brawl_api::{Client, PlayerLeaderboard, traits::*};
70 ///
71 /// # fn main() -> Result<(), Box<dyn ::std::error::Error>> {
72 /// let client = Client::new("my auth token");
73 /// let top50players = PlayerLeaderboard::fetch(
74 /// &client, // <- the client containing the auth key
75 /// "global", // <- the region of the leaderboard to fetch ("global" - world-wide)
76 /// 50 // <- limit of rankings to fetch (i.e. top 50)
77 /// )?;
78 ///
79 /// assert_eq!(top50players.items, *top50players);
80 ///
81 /// # Ok(())
82 /// # }
83 ///
84 /// ```
85 ///
86 /// [`items`]: #structfield.items
87 fn deref_mut(&mut self) -> &mut Vec<PlayerRanking> {
88 &mut self.items
89 }
90}
91
92#[cfg_attr(feature = "async", async_trait)]
93impl PropLimFetchable for PlayerLeaderboard {
94 type Property = str;
95 type Limit = u8;
96
97 /// (Sync) Fetches the top `limit` players in the regional (two-letter) `country_code`
98 /// leaderboard (or global leaderboard, if `country_code == "global"`).
99 ///
100 /// # Errors
101 ///
102 /// This function may error:
103 /// - While requesting (will return an [`Error::Request`]);
104 /// - After receiving a bad status code (API or other error - returns an [`Error::Status`]);
105 /// - After a ratelimit is indicated by the API, while also specifying when it is lifted ([`Error::Ratelimited`]);
106 /// - While parsing incoming JSON (will return an [`Error::Json`]).
107 ///
108 /// (All of those, of course, wrapped inside an `Err`.)
109 ///
110 /// # Examples
111 ///
112 /// World-wide player leaderboard:
113 /// ```rust,ignore
114 /// use brawl_api::{PlayerLeaderboard, Client, traits::PropLimFetchable};
115 ///
116 /// # fn main() -> Result<(), Box<dyn ::std::error::Error>> {
117 /// let client = Client::new("my auth key");
118 ///
119 /// // if the fetch is successful, then the variable below will have the global top 100 players
120 /// // in the 'items' field (i.e. '*top100players').
121 /// let top100players: PlayerLeaderboard = PlayerLeaderboard::fetch(&client, "global", 100)?;
122 ///
123 /// // get player ranked #1. The items are usually sorted (i.e. rank 1 on index [0], rank 2
124 /// // on index [1] etc.), but, to make the program absolutely safe, might want to .sort()
125 /// let player1 = &top100players[0];
126 ///
127 /// assert_eq!(player1.rank, 1);
128 ///
129 /// # Ok(())
130 /// # }
131 /// ```
132 ///
133 /// Regional (in this case, zimbabwean) player leaderboard:
134 /// ```rust,ignore
135 /// use brawl_api::{PlayerLeaderboard, Client, traits::PropLimFetchable};
136 ///
137 /// # fn main() -> Result<(), Box<dyn ::std::error::Error>> {
138 /// let client = Client::new("my auth key");
139 ///
140 /// // if the fetch is successful, then the variable below will have the top 100 zimbabwean
141 /// // players in the 'items' field (i.e. '*top100zwplayers').
142 /// let top100zwplayers: PlayerLeaderboard = PlayerLeaderboard::fetch(&client, "ZW", 100)?;
143 ///
144 /// // get player ranked #1. The items are usually sorted (i.e. rank 1 on index [0], rank 2
145 /// // on index [1] etc.), but, to make the program absolutely safe, might want to .sort()
146 /// let player1 = &top100zwplayers[0];
147 ///
148 /// assert_eq!(player1.rank, 1);
149 ///
150 /// # Ok(())
151 /// # }
152 /// ```
153 ///
154 /// [`Error::Request`]: error/enum.Error.html#variant.Request
155 /// [`Error::Status`]: error/enum.Error.html#variant.Status
156 /// [`Error::Ratelimited`]: error/enum.Error.html#variant.Ratelimited
157 /// [`Error::Json`]: error/enum.Error.html#variant.Json
158 fn fetch(client: &Client, country_code: &str, limit: u8) -> Result<PlayerLeaderboard> {
159 let route = PlayerLeaderboard::get_route(country_code, limit);
160 fetch_route::<PlayerLeaderboard>(client, &route)
161 }
162
163 /// (Async) Fetches the top `limit` players in the regional (two-letter) `country_code`
164 /// leaderboard (or global leaderboard, if `country_code == "global"`).
165 ///
166 /// # Errors
167 ///
168 /// This function may error:
169 /// - While requesting (will return an [`Error::Request`]);
170 /// - After receiving a bad status code (API or other error - returns an [`Error::Status`]);
171 /// - After a ratelimit is indicated by the API, while also specifying when it is lifted ([`Error::Ratelimited`]);
172 /// - While parsing incoming JSON (will return an [`Error::Json`]).
173 ///
174 /// (All of those, of course, wrapped inside an `Err`.)
175 ///
176 /// # Examples
177 ///
178 /// World-wide player leaderboard:
179 /// ```rust,ignore
180 /// use brawl_api::{PlayerLeaderboard, Client, traits::PropLimFetchable};
181 ///
182 /// # fn main() -> Result<(), Box<dyn ::std::error::Error>> {
183 /// let client = Client::new("my auth key");
184 ///
185 /// // if the fetch is successful, then the variable below will have the global top 100 players
186 /// // in the 'items' field (i.e. '*top100players').
187 /// let top100players: PlayerLeaderboard = PlayerLeaderboard::a_fetch(&client, "global", 100).await?;
188 ///
189 /// // get player ranked #1. The items are usually sorted (i.e. rank 1 on index [0], rank 2
190 /// // on index [1] etc.), but, to make the program absolutely safe, might want to .sort()
191 /// let player1 = &top100players[0];
192 ///
193 /// assert_eq!(player1.rank, 1);
194 ///
195 /// # Ok(())
196 /// # }
197 /// ```
198 ///
199 /// Regional (in this case, zimbabwean) player leaderboard:
200 /// ```rust,ignore
201 /// use brawl_api::{PlayerLeaderboard, Client, traits::PropLimFetchable};
202 ///
203 /// # async fn main() -> Result<(), Box<dyn ::std::error::Error>> {
204 /// let client = Client::new("my auth key");
205 ///
206 /// // if the fetch is successful, then the variable below will have the top 100 zimbabwean
207 /// // players in the 'items' field (i.e. '*top100zwplayers').
208 /// let top100zwplayers: PlayerLeaderboard = PlayerLeaderboard::a_fetch(&client, "ZW", 100).await?;
209 ///
210 /// // get player ranked #1. The items are usually sorted (i.e. rank 1 on index [0], rank 2
211 /// // on index [1] etc.), but, to make the program absolutely safe, might want to .sort()
212 /// let player1 = &top100zwplayers[0];
213 ///
214 /// assert_eq!(player1.rank, 1);
215 ///
216 /// # Ok(())
217 /// # }
218 /// ```
219 ///
220 /// [`Error::Request`]: error/enum.Error.html#variant.Request
221 /// [`Error::Status`]: error/enum.Error.html#variant.Status
222 /// [`Error::Ratelimited`]: error/enum.Error.html#variant.Ratelimited
223 /// [`Error::Json`]: error/enum.Error.html#variant.Json
224 #[cfg(feature="async")]
225 async fn a_fetch(
226 client: &Client, country_code: &'async_trait str, limit: u8
227 ) -> Result<PlayerLeaderboard>
228 where Self: 'async_trait,
229 Self::Property: 'async_trait,
230 {
231 let route = PlayerLeaderboard::get_route(&country_code, limit);
232 a_fetch_route::<PlayerLeaderboard>(client, &route).await
233 }
234}
235
236impl PropLimRouteable for PlayerLeaderboard {
237 type Property = str;
238 type Limit = u8;
239
240 /// Get the route for fetching the top `limit` players in the regional `country_code`
241 /// leaderboard (or global, if `country_code == "global"`).
242 fn get_route(country_code: &str, limit: u8) -> Route {
243 Route::PlayerRankings {
244 country_code: country_code.to_owned(),
245 limit
246 }
247 }
248}
249
250/// Represents a player's ranking, based on a regional or global leaderboard.
251/// To obtain the player's full data (a [`Player`] instance), see [`Player::fetch_from`].
252///
253/// [`Player`]: ../players/player/struct.Player.html
254/// [`Player::fetch_from`]: ../players/player/struct.Player.html#method.fetch_from
255#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct PlayerRanking {
258 /// The club the player is in - at the moment, only its name is available for this data model.
259 /// To convert to a full [`Club`] object, see the [`PlayerRankingClub`] docs.
260 ///
261 /// [`Player`]: ../../players/struct.Player.html
262 /// [`Player::fetch_from`]: ../../players/struct.Player.html#method.fetch_from
263 #[serde(default)]
264 pub club: PlayerRankingClub,
265
266 /// The player's tag.
267 #[serde(default)]
268 pub tag: String,
269
270 /// The player's name.
271 #[serde(default)]
272 pub name: String,
273
274 /// The player's trophies.
275 #[serde(default)]
276 pub trophies: usize,
277
278 /// The player's rank in the leaderboard.
279 #[serde(default = "one_default")]
280 pub rank: u8,
281
282 /// The player's name color. Defaults to `0xffffff` (white).
283 #[serde(default = "oxffffff_default")]
284 #[serde(deserialize_with = "deserialize_number_from_string")]
285 pub name_color: u64,
286}
287
288/// Represents the club in a player's ranking (a [`PlayerRanking`] object). Since the only data
289/// available at the moment is its name, it cannot be converted into a full [`Club`] object
290/// using a convenient method. For that, one must have the original `PlayerRanking` object,
291/// then convert it into a [`Player`] with [`Player::fetch_from`].
292///
293/// [`PlayerRanking`]: ./struct.PlayerRanking.html
294/// [`Player`]: ../../players/player/struct.Player.html
295/// [`Player::fetch_from`]: ../../players/player/struct.Player.html#method.fetch_from
296#[non_exhaustive]
297#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
298pub struct PlayerRankingClub {
299 /// The club's name.
300 #[serde(default)]
301 pub name: String,
302}
303
304impl PlayerRankingClub {
305 /// Creates a new `PlayerRankingClub` instance with the given name.
306 pub fn new(name: &str) -> PlayerRankingClub {
307 PlayerRankingClub { name: name.to_owned() }
308 }
309}
310
311impl Default for PlayerRankingClub {
312 /// Returns an instance of `PlayerRankingClub` with initial values (empty name).
313 ///
314 /// # Examples
315 ///
316 /// ```rust
317 /// use brawl_api::model::PlayerRankingClub;
318 ///
319 /// assert_eq!(
320 /// PlayerRankingClub::default(),
321 /// PlayerRankingClub::new("")
322 /// );
323 /// ```
324 fn default() -> PlayerRankingClub {
325 PlayerRankingClub {
326 name: String::from(""),
327 }
328 }
329}
330
331/////////////////////////////////// tests ///////////////////////////////////
332
333#[cfg(test)]
334mod tests {
335 use serde_json;
336 use super::{PlayerLeaderboard, PlayerRanking, PlayerRankingClub};
337 use crate::error::Error;
338
339 /// Tests for PlayerLeaderboard deserialization from API-provided JSON.
340 #[test]
341 fn rankings_players_deser() -> Result<(), Box<dyn ::std::error::Error>> {
342
343 let rp_json_s = r##"{
344 "items": [
345 {
346 "tag": "#AAAAAAAAA",
347 "name": "Player",
348 "nameColor": "0xfff05637",
349 "trophies": 30000,
350 "rank": 1,
351 "club": {
352 "name": "Scary Club"
353 }
354 },
355 {
356 "tag": "#EEEEEEE",
357 "name": "Also Player",
358 "nameColor": "0xffa2e3fe",
359 "trophies": 25000,
360 "rank": 2,
361 "club": {
362 "name": "Another Club"
363 }
364 },
365 {
366 "tag": "#QQQQQQQ",
367 "name": "Youtuber",
368 "nameColor": "0xfff05637",
369 "trophies": 23000,
370 "rank": 3,
371 "club": {
372 "name": "Different Club"
373 }
374 },
375 {
376 "tag": "#55555553Q",
377 "name": "Not a valid player",
378 "nameColor": "0xfff9cf08",
379 "trophies": 20000,
380 "rank": 4,
381 "club": {
382 "name": "Different Club"
383 }
384 }
385 ],
386
387 "paging": {
388 "cursors": {}
389 }
390}"##;
391
392 let p_leaders = serde_json::from_str::<PlayerLeaderboard>(rp_json_s)
393 .map_err(Error::Json)?;
394
395 assert_eq!(
396 p_leaders,
397 PlayerLeaderboard {
398 items: vec![
399 PlayerRanking {
400 tag: String::from("#AAAAAAAAA"),
401 name: String::from("Player"),
402 name_color: 0xfff05637,
403 trophies: 30000,
404 rank: 1,
405 club: PlayerRankingClub {
406 name: String::from("Scary Club")
407 }
408 },
409 PlayerRanking {
410 tag: String::from("#EEEEEEE"),
411 name: String::from("Also Player"),
412 name_color: 0xffa2e3fe,
413 trophies: 25000,
414 rank: 2,
415 club: PlayerRankingClub {
416 name: String::from("Another Club")
417 }
418 },
419 PlayerRanking {
420 tag: String::from("#QQQQQQQ"),
421 name: String::from("Youtuber"),
422 name_color: 0xfff05637,
423 trophies: 23000,
424 rank: 3,
425 club: PlayerRankingClub {
426 name: String::from("Different Club")
427 }
428 },
429 PlayerRanking {
430 tag: String::from("#55555553Q"),
431 name: String::from("Not a valid player"),
432 name_color: 0xfff9cf08,
433 trophies: 20000,
434 rank: 4,
435 club: PlayerRankingClub {
436 name: String::from("Different Club")
437 }
438 }
439 ]
440 }
441 );
442
443 Ok(())
444 }
445}