speedrun_api/api/
games.rs

1//! # Games
2//!
3//! Endpoints available for games
4
5use std::{borrow::Cow, collections::BTreeSet, fmt::Display};
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use super::{
11    categories::CategoryEmbeds, developers::DeveloperId, endpoint::Endpoint, engines::EngineId,
12    error::BodyError, gametypes::GameTypeId, genres::GenreId, leaderboards::LeaderboardEmbeds,
13    platforms::PlatformId, publishers::PublisherId, query_params::QueryParams, regions::RegionId,
14    users::UserId, CategoriesSorting, Direction, Pageable, VariablesSorting,
15};
16
17/// Embeds available for games
18///
19/// NOTE: Embeds can be nested. That is not handled by this API.
20#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
21pub enum GameEmbeds {
22    /// Embed all levels defined for the game.
23    Levels,
24    /// Embed all categories defined for the game.
25    Categories,
26    /// Embed moderators as full user resources.
27    Moderators,
28    /// Embed all assigned gametypes.
29    Gametypes,
30    /// Embed all assigned platforms.
31    Platforms,
32    /// Embed all assigned regions.
33    Regions,
34    /// Embed all assigned genres.
35    Genres,
36    /// Embed all assigned engines.
37    Engines,
38    /// Embed all assigned developers.
39    Developers,
40    /// Embed all assigned publishers.
41    Publishers,
42    /// Embed all variables defined for the game.
43    Variables,
44}
45
46/// Sorting options for games
47#[derive(Debug, Serialize, Clone, Copy)]
48#[serde(rename_all = "kebab-case")]
49pub enum GamesSorting {
50    /// Sorts alphanumerically by the international name (default)
51    #[serde(rename = "name.int")]
52    NameInternational,
53    /// Sorts alphanumerically by the Japanese name
54    #[serde(rename = "name.jap")]
55    NameJapanese,
56    /// Sorts alphanumerically by the abbreviation
57    Abbreviation,
58    /// Sorts by the release date
59    Released,
60    /// Sorts by the date the game was added to speedrun.com
61    Created,
62    /// Sorts by string similarity. *Only available when searching games by
63    /// name* (default when searching by name)
64    Similarity,
65}
66
67// Does this belong here?
68/// Sorting options for levels
69#[derive(Debug, Serialize, Clone, Copy)]
70#[serde(rename_all = "kebab-case")]
71pub enum LevelsSorting {
72    /// Sorts alphanumerically by the level name
73    Name,
74    /// Sorts by the order defined by the game moderator (default)
75    Pos,
76}
77
78// Does this belong here?
79/// Type of leaderboard to return.
80#[derive(Debug, Serialize, Clone, Copy)]
81#[serde(rename_all = "kebab-case")]
82pub enum LeaderboardScope {
83    /// Only return full-game categories.
84    FullGame,
85    /// Only return individual levels.
86    Levels,
87    /// Return all (default)
88    All,
89}
90
91/// Error type for GameDerivedGamesBuilder
92#[derive(Debug, Error)]
93pub enum GameDerivedGamesBuilderError {
94    /// Uninitialized Field
95    #[error("{0} must be initialized")]
96    UninitializedField(&'static str),
97    /// Error from the inner GamesBuilder
98    #[error(transparent)]
99    Inner(#[from] GamesBuilderError),
100}
101
102/// Represents a game ID
103#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
104pub struct GameId<'a>(Cow<'a, str>);
105
106impl<'a> GameId<'a> {
107    /// Create a new [`GameId`]
108    pub fn new<T>(id: T) -> Self
109    where
110        T: Into<Cow<'a, str>>,
111    {
112        Self(id.into())
113    }
114}
115
116impl<'a, T> From<T> for GameId<'a>
117where
118    T: Into<Cow<'a, str>>,
119{
120    fn from(value: T) -> Self {
121        Self::new(value)
122    }
123}
124
125impl Display for GameId<'_> {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(f, "{}", &self.0)
128    }
129}
130
131/// Retrievs a lists of all games.
132#[derive(Default, Debug, Builder, Serialize, Clone)]
133#[builder(default, setter(into, strip_option))]
134#[serde(rename_all = "kebab-case")]
135pub struct Games<'a> {
136    #[doc = r"Performs a fuzzy search across game names and abbreviations."]
137    name: Option<Cow<'a, str>>,
138    #[doc = r"Perform an exact-match search for this abbreviation."]
139    abbreviation: Option<Cow<'a, str>>,
140    #[doc = r"Restrict results to games released in the given year."]
141    released: Option<i64>,
142    #[doc = r"Restrict results to the given game type."]
143    gametype: Option<GameTypeId<'a>>,
144    #[doc = r"Restrict results to the given platform."]
145    platform: Option<PlatformId<'a>>,
146    #[doc = r"Restrict results to the given region."]
147    region: Option<RegionId<'a>>,
148    #[doc = r"Restrict results to the given genre."]
149    genre: Option<GenreId<'a>>,
150    #[doc = r"Restrict results to the given engine."]
151    engine: Option<EngineId<'a>>,
152    #[doc = r"Restrict results to the given developer."]
153    developer: Option<DeveloperId<'a>>,
154    #[doc = r"Restrict results to the given publisher."]
155    publisher: Option<PublisherId<'a>>,
156    #[doc = r"Only return games moderated by the given user."]
157    moderator: Option<UserId<'a>>,
158    #[doc = r"Enable bulk access."]
159    #[serde(rename = "_bulk")]
160    bulk: Option<bool>,
161    #[doc = r"Sorting options for results."]
162    orderby: Option<GamesSorting>,
163    #[doc = r"Sort direction."]
164    direction: Option<Direction>,
165    #[builder(setter(name = "_embed"), private)]
166    #[serde(serialize_with = "super::utils::serialize_as_csv")]
167    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
168    embed: BTreeSet<GameEmbeds>,
169}
170
171/// Retrieves a single game, identified by ID.
172#[derive(Debug, Builder, Clone)]
173#[builder(setter(into, strip_option))]
174pub struct Game<'a> {
175    #[doc = r"`ID` of the game."]
176    id: GameId<'a>,
177}
178
179/// Retrieve all categories for the given game.
180#[derive(Debug, Builder, Serialize, Clone)]
181#[builder(setter(into, strip_option))]
182#[serde(rename_all = "kebab-case")]
183pub struct GameCategories<'a> {
184    #[doc = r"`ID` of the game to retrieve categories for."]
185    #[serde(skip)]
186    id: GameId<'a>,
187    #[doc = r"Filter miscellaneous categories."]
188    #[builder(default)]
189    miscellaneous: Option<bool>,
190    #[doc = r"Sorting options for results."]
191    #[builder(default)]
192    orderby: Option<CategoriesSorting>,
193    #[doc = r"Sort direction."]
194    #[builder(default)]
195    direction: Option<Direction>,
196    #[builder(setter(name = "_embed"), private, default)]
197    #[serde(serialize_with = "super::utils::serialize_as_csv")]
198    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
199    embed: BTreeSet<CategoryEmbeds>,
200}
201
202impl GameCategoriesBuilder<'_> {
203    /// Add an embedded resource to this result
204    pub fn embed(&mut self, embed: CategoryEmbeds) -> &mut Self {
205        self.embed.get_or_insert_with(BTreeSet::new).insert(embed);
206        self
207    }
208
209    /// Add multiple embedded resources to this result
210    pub fn embeds<I>(&mut self, iter: I) -> &mut Self
211    where
212        I: Iterator<Item = CategoryEmbeds>,
213    {
214        self.embed.get_or_insert_with(BTreeSet::new).extend(iter);
215        self
216    }
217}
218
219/// Retrieves all levels for the given game.
220#[derive(Debug, Builder, Serialize, Clone)]
221#[builder(setter(into, strip_option))]
222#[serde(rename_all = "kebab-case")]
223pub struct GameLevels<'a> {
224    #[doc = r"`ID` of the game to retrieve levels for."]
225    #[serde(skip)]
226    id: GameId<'a>,
227    #[doc = r"Sorting options for results."]
228    #[builder(default)]
229    orderby: Option<LevelsSorting>,
230    #[doc = r"Sort direction."]
231    #[builder(default)]
232    direction: Option<Direction>,
233}
234
235/// Retrieves all variables for the given game.
236#[derive(Debug, Builder, Serialize, Clone)]
237#[builder(setter(into, strip_option))]
238#[serde(rename_all = "kebab-case")]
239pub struct GameVariables<'a> {
240    #[doc = r"`ID` of the game to retrieve variables for."]
241    #[serde(skip)]
242    id: GameId<'a>,
243    #[doc = r"Sorting options for results."]
244    #[builder(default)]
245    orderby: Option<VariablesSorting>,
246    #[doc = r"Sort direction."]
247    #[builder(default)]
248    direction: Option<Direction>,
249}
250
251/// Builder for [`GameDerivedGames`].
252#[derive(Default, Clone)]
253pub struct GameDerivedGamesBuilder<'a> {
254    id: Option<GameId<'a>>,
255    inner: GamesBuilder<'a>,
256}
257
258/// Retrieves all games that have the given game as their base game.
259#[derive(Debug, Clone)]
260pub struct GameDerivedGames<'a> {
261    id: GameId<'a>,
262    inner: Games<'a>,
263}
264
265/// Retrieves all records (top 3 places) for every (category/level) combonation
266/// of the given game.
267#[derive(Debug, Builder, Serialize, Clone)]
268#[builder(setter(into, strip_option))]
269#[serde(rename_all = "kebab-case")]
270pub struct GameRecords<'a> {
271    #[doc = r"`ID` of the game to retrieve records for."]
272    id: GameId<'a>,
273    #[doc = r"Return the `top` *places* (this can result in more than `top` runs!). Defaults to 3."]
274    #[builder(default)]
275    top: Option<i64>,
276    #[doc = r"When set to [`LeaderboardScope::FullGame`], only full-game categories will be included. When set to [`LeaderboardScope::Levels`] only individual levels are returned. Defaults to [`LeaderboardScope::All`]."]
277    #[builder(default)]
278    scope: Option<LeaderboardScope>,
279    #[doc = r"When `false`, miscellaneous categories will not be included in the results."]
280    #[builder(default)]
281    miscellaneous: Option<bool>,
282    #[doc = r"When `true`, empty leaderboards will not be included in the results."]
283    #[builder(default)]
284    skip_empty: Option<bool>,
285    #[builder(setter(name = "_embed"), private, default)]
286    #[serde(serialize_with = "super::utils::serialize_as_csv")]
287    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
288    embed: BTreeSet<LeaderboardEmbeds>,
289}
290
291impl Games<'_> {
292    /// Create a builder for this endpoint.
293    pub fn builder<'a>() -> GamesBuilder<'a> {
294        GamesBuilder::default()
295    }
296}
297
298impl GamesBuilder<'_> {
299    /// Add an embedded resource to this result.
300    pub fn embed(&mut self, embed: GameEmbeds) -> &mut Self {
301        self.embed.get_or_insert_with(BTreeSet::new).insert(embed);
302        self
303    }
304
305    /// Add multiple embedded resources to this result.
306    pub fn embeds<I>(&mut self, iter: I) -> &mut Self
307    where
308        I: Iterator<Item = GameEmbeds>,
309    {
310        self.embed.get_or_insert_with(BTreeSet::new).extend(iter);
311        self
312    }
313}
314
315impl Game<'_> {
316    /// Create a builder for this endpoint.
317    pub fn builder<'a>() -> GameBuilder<'a> {
318        GameBuilder::default()
319    }
320}
321
322impl GameCategories<'_> {
323    /// Create a builder for this endpoint.
324    pub fn builder<'a>() -> GameCategoriesBuilder<'a> {
325        GameCategoriesBuilder::default()
326    }
327}
328
329impl GameLevels<'_> {
330    /// Create a builder for this endpoint.
331    pub fn builder<'a>() -> GameLevelsBuilder<'a> {
332        GameLevelsBuilder::default()
333    }
334}
335
336impl GameVariables<'_> {
337    /// Create a builder for this endpoint.
338    pub fn builder<'a>() -> GameVariablesBuilder<'a> {
339        GameVariablesBuilder::default()
340    }
341}
342
343impl<'a> GameDerivedGamesBuilder<'a> {
344    /// `ID` of the base game to retrieve games for.
345    pub fn id<S>(&mut self, value: S) -> &mut Self
346    where
347        S: Into<GameId<'a>>,
348    {
349        self.id = Some(value.into());
350        self
351    }
352
353    /// Performs a fuzzy search across game names and abbreviations.
354    pub fn name<S>(&mut self, value: S) -> &mut Self
355    where
356        S: Into<Cow<'a, str>>,
357    {
358        self.inner.name(value);
359        self
360    }
361
362    /// Perform an exact-match search for this abbreviation.
363    pub fn abbreviation<S>(&mut self, value: S) -> &mut Self
364    where
365        S: Into<Cow<'a, str>>,
366    {
367        self.inner.abbreviation(value);
368        self
369    }
370
371    /// Restrict results to games released in the given year.
372    pub fn released<T>(&mut self, value: T) -> &mut Self
373    where
374        T: Into<i64>,
375    {
376        self.inner.released(value);
377        self
378    }
379
380    /// Restrict results to the given game type.
381    pub fn gametype<S>(&mut self, value: S) -> &mut Self
382    where
383        S: Into<GameTypeId<'a>>,
384    {
385        self.inner.gametype(value);
386        self
387    }
388
389    /// Restrict results to the given platform.
390    pub fn platform<S>(&mut self, value: S) -> &mut Self
391    where
392        S: Into<PlatformId<'a>>,
393    {
394        self.inner.platform(value);
395        self
396    }
397
398    /// Restrict results to the given region.
399    pub fn region<S>(&mut self, value: S) -> &mut Self
400    where
401        S: Into<RegionId<'a>>,
402    {
403        self.inner.region(value);
404        self
405    }
406
407    /// Restrict results to the given genre.
408    pub fn genre<S>(&mut self, value: S) -> &mut Self
409    where
410        S: Into<GenreId<'a>>,
411    {
412        self.inner.genre(value);
413        self
414    }
415
416    /// Restrict results to the given engine.
417    pub fn engine<S>(&mut self, value: S) -> &mut Self
418    where
419        S: Into<EngineId<'a>>,
420    {
421        self.inner.engine(value);
422        self
423    }
424
425    /// Restrict results to the given developer.
426    pub fn developer<S>(&mut self, value: S) -> &mut Self
427    where
428        S: Into<DeveloperId<'a>>,
429    {
430        self.inner.developer(value);
431        self
432    }
433
434    /// Restrict results to the given publisher.
435    pub fn publisher<S>(&mut self, value: S) -> &mut Self
436    where
437        S: Into<PublisherId<'a>>,
438    {
439        self.inner.publisher(value);
440        self
441    }
442
443    /// Only return games moderated by the given user.
444    pub fn moderator<S>(&mut self, value: S) -> &mut Self
445    where
446        S: Into<UserId<'a>>,
447    {
448        self.inner.moderator(value);
449        self
450    }
451
452    /// Enable bulk access.
453    pub fn bulk<T>(&mut self, value: T) -> &mut Self
454    where
455        T: Into<bool>,
456    {
457        self.inner.bulk(value);
458        self
459    }
460
461    /// Sorting options for results.
462    pub fn orderby<V>(&mut self, value: V) -> &mut Self
463    where
464        V: Into<GamesSorting>,
465    {
466        self.inner.orderby(value);
467        self
468    }
469
470    /// Sort direction.
471    pub fn direction<V>(&mut self, value: V) -> &mut Self
472    where
473        V: Into<Direction>,
474    {
475        self.inner.direction(value);
476        self
477    }
478
479    /// Builds a new [`GameDerivedGames`].
480    ///
481    /// # Errors
482    ///
483    /// If a required field has not been initialized.
484    pub fn build(&self) -> Result<GameDerivedGames<'a>, GameDerivedGamesBuilderError> {
485        let inner = self.inner.build()?;
486        Ok(GameDerivedGames {
487            id: self
488                .id
489                .as_ref()
490                .cloned()
491                .ok_or(GameDerivedGamesBuilderError::UninitializedField("id"))?,
492            inner,
493        })
494    }
495}
496
497impl GameDerivedGames<'_> {
498    /// Create a builder for this endpoint.
499    pub fn builder<'a>() -> GameDerivedGamesBuilder<'a> {
500        GameDerivedGamesBuilder::default()
501    }
502}
503
504impl GameRecords<'_> {
505    /// Create a builder for this endpoint.
506    pub fn builder<'a>() -> GameRecordsBuilder<'a> {
507        GameRecordsBuilder::default()
508    }
509}
510
511impl GameRecordsBuilder<'_> {
512    /// Add an embedded resource to this result.
513    pub fn embed(&mut self, embed: LeaderboardEmbeds) -> &mut Self {
514        self.embed.get_or_insert_with(BTreeSet::new).insert(embed);
515        self
516    }
517
518    /// Add multiple embedded resources to this result.
519    pub fn embeds<I>(&mut self, iter: I) -> &mut Self
520    where
521        I: Iterator<Item = LeaderboardEmbeds>,
522    {
523        self.embed.get_or_insert_with(BTreeSet::new).extend(iter);
524        self
525    }
526}
527
528impl GameEmbeds {
529    fn as_str(&self) -> &'static str {
530        match self {
531            GameEmbeds::Levels => "levels",
532            GameEmbeds::Categories => "categories",
533            GameEmbeds::Moderators => "moderators",
534            GameEmbeds::Gametypes => "gametypes",
535            GameEmbeds::Platforms => "platforms",
536            GameEmbeds::Regions => "regions",
537            GameEmbeds::Genres => "genres",
538            GameEmbeds::Engines => "engines",
539            GameEmbeds::Developers => "developers",
540            GameEmbeds::Publishers => "publishers",
541            GameEmbeds::Variables => "variables",
542        }
543    }
544}
545
546impl Default for LevelsSorting {
547    fn default() -> Self {
548        Self::Pos
549    }
550}
551
552impl Endpoint for Games<'_> {
553    fn endpoint(&self) -> Cow<'static, str> {
554        "games".into()
555    }
556
557    fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
558        QueryParams::with(self)
559    }
560}
561
562impl Endpoint for Game<'_> {
563    fn endpoint(&self) -> Cow<'static, str> {
564        format!("/games/{}", self.id).into()
565    }
566}
567
568impl Endpoint for GameCategories<'_> {
569    fn endpoint(&self) -> Cow<'static, str> {
570        format!("/games/{}/categories", self.id).into()
571    }
572
573    fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
574        QueryParams::with(self)
575    }
576}
577
578impl Endpoint for GameLevels<'_> {
579    fn endpoint(&self) -> Cow<'static, str> {
580        format!("/games/{}/levels", self.id).into()
581    }
582
583    fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
584        QueryParams::with(self)
585    }
586}
587
588impl Endpoint for GameVariables<'_> {
589    fn endpoint(&self) -> Cow<'static, str> {
590        format!("/games/{}/variables", self.id).into()
591    }
592
593    fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
594        QueryParams::with(self)
595    }
596}
597
598impl Endpoint for GameDerivedGames<'_> {
599    fn endpoint(&self) -> Cow<'static, str> {
600        format!("/games/{}/derived-games", self.id).into()
601    }
602
603    fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
604        QueryParams::with(&self.inner)
605    }
606}
607
608impl Endpoint for GameRecords<'_> {
609    fn endpoint(&self) -> Cow<'static, str> {
610        format!("/games/{}/records", self.id).into()
611    }
612
613    fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
614        QueryParams::with(self)
615    }
616}
617
618impl From<&GameEmbeds> for &'static str {
619    fn from(value: &GameEmbeds) -> Self {
620        value.as_str()
621    }
622}
623
624impl Pageable for GameDerivedGames<'_> {}
625
626impl Pageable for Games<'_> {}
627
628impl Pageable for GameRecords<'_> {}