Skip to main content

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}