speedrun_api/api/
leaderboards.rs

1//! # Leaderboards
2//!
3//! Endpoints available for leaderboards
4
5use std::{
6    borrow::Cow,
7    collections::{BTreeSet, HashMap},
8};
9
10use serde::Serialize;
11
12use crate::{
13    api::{endpoint::Endpoint, error::BodyError},
14    types::TimingMethod,
15};
16
17use super::{
18    categories::CategoryId,
19    games::GameId,
20    levels::LevelId,
21    platforms::PlatformId,
22    query_params::QueryParams,
23    regions::RegionId,
24    variables::{ValueId, VariableId},
25};
26
27/// Embeds available for leaderboards.
28///
29/// NOTE: Embeds can be nested. That is not handled by this API.
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
31pub enum LeaderboardEmbeds {
32    /// Embed the full game resource.
33    Game,
34    /// Embed the category used for the leaderboard.
35    Category,
36    /// Embed the level used for the leaderboard.
37    Level,
38    /// Adds a new `players` element to the leaderboard, containing a flat list
39    /// of all players of all runs on the leaderboard.
40    Players,
41    /// Adds all used regions.
42    Regions,
43    /// Adds all used platforms.
44    Platforms,
45    /// Adds all applicable variables for the chosen level/categories
46    Variables,
47}
48
49/// Retrieves a full-game leaderboard identified by game and category.
50#[derive(Debug, Builder, Serialize, Clone)]
51#[builder(setter(into, strip_option))]
52#[serde(rename_all = "kebab-case")]
53pub struct FullGameLeaderboard<'a> {
54    #[doc = r"Game `ID` or abbreviation."]
55    #[serde(skip)]
56    game: GameId<'a>,
57    #[doc = r"Category `ID` or abbreviation."]
58    #[serde(skip)]
59    category: CategoryId<'a>,
60
61    // NOTE: Make the below fields a common struct for full-games and individual-levels?
62    #[doc = r"Only return `top` places."]
63    #[builder(default)]
64    top: Option<i64>,
65    #[doc = r"Only return runs done on `platform`."]
66    #[builder(default)]
67    platform: Option<PlatformId<'a>>,
68    #[doc = r"Only return runs done in `region`."]
69    #[builder(default)]
70    region: Option<RegionId<'a>>,
71    #[doc = r"When unset, real devices and emulator results are returned. When `true` only emulator runs are returned, otherwise only real deivces are returned."]
72    #[builder(default)]
73    emulators: Option<bool>,
74    #[doc = r"When `true` only runs with videos will be returned. (default: `false`)"]
75    #[builder(default)]
76    video_only: Option<bool>,
77    #[doc = r"What [`TimingMethod`] to use to determine the sorting of runs."]
78    #[builder(default)]
79    timing: Option<TimingMethod>,
80    #[doc = r"Only return runs done on or before this date. [ISO 8601 date string](https://en.wikipedia.org/wiki/ISO_8601#Dates)."]
81    #[builder(default)]
82    date: Option<String>,
83    #[builder(setter(name = "_variables"), private, default)]
84    #[serde(skip)]
85    variables: HashMap<VariableId<'a>, ValueId<'a>>,
86    #[builder(setter(name = "_embed"), private, default)]
87    #[serde(serialize_with = "super::utils::serialize_as_csv")]
88    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
89    embed: BTreeSet<LeaderboardEmbeds>,
90}
91
92/// Retrieves an individual-level leaderboard identified by game, category and
93/// level.
94#[derive(Debug, Builder, Serialize, Clone)]
95#[builder(setter(into, strip_option))]
96#[serde(rename_all = "kebab-case")]
97pub struct IndividualLevelLeaderboard<'a> {
98    #[doc = r"Game `ID` or abbreviation."]
99    #[serde(skip)]
100    game: GameId<'a>,
101    #[doc = r"Level `ID` or abbreviation."]
102    #[serde(skip)]
103    level: LevelId<'a>,
104    #[doc = r"Category `ID` or abbreviation."]
105    #[serde(skip)]
106    category: CategoryId<'a>,
107
108    // NOTE: Make the below fields a common struct for full-games and individual-levels?
109    #[doc = r"Only return `top` places."]
110    #[builder(default)]
111    top: Option<i64>,
112    #[doc = r"Only return runs done on `platform`."]
113    #[builder(default)]
114    platform: Option<PlatformId<'a>>,
115    #[doc = r"Only return runs done in `region`."]
116    #[builder(default)]
117    region: Option<RegionId<'a>>,
118    #[doc = r"When unset, real devices and emulator results are returned. When `true` only emulator runs are returned, otherwise only real deivces are returned."]
119    #[builder(default)]
120    emulators: Option<bool>,
121    #[doc = r"When `true` only runs with videos will be returned. (default: `false`)"]
122    #[builder(default)]
123    video_only: Option<bool>,
124    #[doc = r"What [`TimingMethod`] to use to determine the sorting of runs."]
125    #[builder(default)]
126    timing: Option<TimingMethod>,
127    #[doc = r"Only return runs done on or before this date. [ISO 8601 date string](https://en.wikipedia.org/wiki/ISO_8601#Dates)."]
128    #[builder(default)]
129    date: Option<String>,
130    #[builder(setter(name = "_variables"), private, default)]
131    #[serde(skip)]
132    variables: HashMap<VariableId<'a>, ValueId<'a>>,
133    #[builder(setter(name = "_embed"), private, default)]
134    #[serde(serialize_with = "super::utils::serialize_as_csv")]
135    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
136    embed: BTreeSet<LeaderboardEmbeds>,
137}
138
139impl FullGameLeaderboard<'_> {
140    /// Create a builder for this endpoint.
141    pub fn builder<'a>() -> FullGameLeaderboardBuilder<'a> {
142        FullGameLeaderboardBuilder::default()
143    }
144}
145
146impl<'a> FullGameLeaderboardBuilder<'a> {
147    /// Add an embedded resource to this result
148    pub fn embed(&mut self, embed: LeaderboardEmbeds) -> &mut Self {
149        self.embed.get_or_insert_with(BTreeSet::new).insert(embed);
150        self
151    }
152
153    /// Add multiple embedded resources to this result
154    pub fn embeds<I>(&mut self, iter: I) -> &mut Self
155    where
156        I: Iterator<Item = LeaderboardEmbeds>,
157    {
158        self.embed.get_or_insert_with(BTreeSet::new).extend(iter);
159        self
160    }
161
162    /// Add a single custom variable to filter results by.
163    pub fn variable<Var, Val>(&mut self, variable: Var, value: Val) -> &mut Self
164    where
165        Var: Into<VariableId<'a>>,
166        Val: Into<ValueId<'a>>,
167    {
168        self.variables
169            .get_or_insert_with(HashMap::new)
170            .insert(variable.into(), value.into());
171        self
172    }
173
174    /// Add multiple custom variables to filter results by.
175    pub fn variables<I, Var, Val>(&mut self, iter: I) -> &mut Self
176    where
177        I: IntoIterator<Item = (Var, Val)>,
178        Var: Into<VariableId<'a>>,
179        Val: Into<ValueId<'a>>,
180    {
181        self.variables
182            .get_or_insert_with(HashMap::new)
183            .extend(iter.into_iter().map(|(k, v)| (k.into(), v.into())));
184        self
185    }
186}
187
188impl IndividualLevelLeaderboard<'_> {
189    /// Create a builder for this endpoint.
190    pub fn builder<'a>() -> IndividualLevelLeaderboardBuilder<'a> {
191        IndividualLevelLeaderboardBuilder::default()
192    }
193}
194
195impl<'a> IndividualLevelLeaderboardBuilder<'a> {
196    /// Add an embedded resource to this result
197    pub fn embed(&mut self, embed: LeaderboardEmbeds) -> &mut Self {
198        self.embed.get_or_insert_with(BTreeSet::new).insert(embed);
199        self
200    }
201
202    /// Add multiple embedded resources to this result
203    pub fn embeds<I>(&mut self, iter: I) -> &mut Self
204    where
205        I: Iterator<Item = LeaderboardEmbeds>,
206    {
207        self.embed.get_or_insert_with(BTreeSet::new).extend(iter);
208        self
209    }
210
211    /// Add a single custom variable to filter results by.
212    pub fn variable<Var, Val>(&mut self, variable: Var, value: Val) -> &mut Self
213    where
214        Var: Into<VariableId<'a>>,
215        Val: Into<ValueId<'a>>,
216    {
217        self.variables
218            .get_or_insert_with(HashMap::new)
219            .insert(variable.into(), value.into());
220        self
221    }
222
223    /// Add multiple custom variables to filter results by.
224    pub fn variables<I, Var, Val>(&mut self, iter: I) -> &mut Self
225    where
226        I: IntoIterator<Item = (Var, Val)>,
227        Var: Into<VariableId<'a>>,
228        Val: Into<ValueId<'a>>,
229    {
230        self.variables
231            .get_or_insert_with(HashMap::new)
232            .extend(iter.into_iter().map(|(k, v)| (k.into(), v.into())));
233        self
234    }
235}
236
237impl LeaderboardEmbeds {
238    fn as_str(&self) -> &'static str {
239        match self {
240            LeaderboardEmbeds::Game => "game",
241            LeaderboardEmbeds::Category => "category",
242            LeaderboardEmbeds::Level => "level",
243            LeaderboardEmbeds::Players => "players",
244            LeaderboardEmbeds::Regions => "regions",
245            LeaderboardEmbeds::Platforms => "platforms",
246            LeaderboardEmbeds::Variables => "variables",
247        }
248    }
249}
250
251impl Endpoint for FullGameLeaderboard<'_> {
252    fn endpoint(&self) -> Cow<'static, str> {
253        format!("/leaderboards/{}/category/{}", self.game, self.category).into()
254    }
255
256    fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
257        let mut params = QueryParams::with(self)?;
258        params.extend_pairs(
259            self.variables
260                .iter()
261                .map(|(var, val)| (format!("var-{var}"), val.to_string())),
262        );
263        Ok(params)
264    }
265}
266
267impl Endpoint for IndividualLevelLeaderboard<'_> {
268    fn endpoint(&self) -> Cow<'static, str> {
269        format!(
270            "/leaderboards/{}/level/{}/{}",
271            self.game, self.level, self.category
272        )
273        .into()
274    }
275
276    fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
277        let mut params = QueryParams::with(self)?;
278        params.extend_pairs(
279            self.variables
280                .iter()
281                .map(|(var, val)| (format!("var-{var}"), val.to_string())),
282        );
283        Ok(params)
284    }
285}
286
287impl From<&LeaderboardEmbeds> for &'static str {
288    fn from(value: &LeaderboardEmbeds) -> Self {
289        value.as_str()
290    }
291}