speedrun_api/api/
series.rs

1//! # Series
2//!
3//! Endpoints available for series.
4use std::{borrow::Cow, collections::BTreeSet, fmt::Display};
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9use super::{
10    developers::DeveloperId,
11    endpoint::Endpoint,
12    engines::EngineId,
13    error::BodyError,
14    games::{Games, GamesBuilder, GamesBuilderError, GamesSorting},
15    gametypes::GameTypeId,
16    genres::GenreId,
17    platforms::PlatformId,
18    publishers::PublisherId,
19    query_params::QueryParams,
20    regions::RegionId,
21    users::UserId,
22    Direction, Pageable,
23};
24
25/// Embeds available for series.
26#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
27pub enum SeriesEmbeds {
28    /// Embed moderators a full user resources.
29    Moderators,
30}
31
32/// Sorting options for game series
33#[derive(Debug, Serialize, Clone, Copy)]
34#[serde(rename_all = "kebab-case")]
35pub enum SeriesSorting {
36    /// Sorts alphanumerically by the international name (default)
37    #[serde(rename = "name.int")]
38    NameInternational,
39    /// Sorts alphanumerically by the Japanese name
40    #[serde(rename = "name.jap")]
41    NameJapanese,
42    /// Sorts alphanumerically by the abbreviation
43    Abbreviation,
44    /// Sorts by the date the series was added to speedrun.com
45    Created,
46}
47
48/// Error type for [`SeriesGameBuilder`]
49#[derive(Debug, Error)]
50pub enum SeriesGamesBuilderError {
51    /// Uninitialized field
52    #[error("{0} must be initialized")]
53    UninitializedField(&'static str),
54    /// Error from the inner type
55    #[error(transparent)]
56    Inner(#[from] GamesBuilderError),
57}
58
59/// Represents a series ID
60#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
61pub struct SeriesId<'a>(Cow<'a, str>);
62
63impl<'a> SeriesId<'a> {
64    /// Create a new [`SeriesId`]
65    pub fn new<T>(id: T) -> Self
66    where
67        T: Into<Cow<'a, str>>,
68    {
69        Self(id.into())
70    }
71}
72
73impl<'a, T> From<T> for SeriesId<'a>
74where
75    T: Into<Cow<'a, str>>,
76{
77    fn from(value: T) -> Self {
78        Self::new(value)
79    }
80}
81
82impl Display for SeriesId<'_> {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}", &self.0)
85    }
86}
87
88/// Retrieves a list of all series
89#[derive(Default, Debug, Builder, Serialize, Clone)]
90#[builder(default, setter(into, strip_option))]
91#[serde(rename_all = "kebab-case")]
92pub struct ListSeries<'a> {
93    #[doc = r"When given, performs a fuzzy search across all series names and abbreviations."]
94    name: Option<Cow<'a, str>>,
95    #[doc = r"When given, performs an exact-match search for `abbreviation`."]
96    abbreviation: Option<Cow<'a, str>>,
97    #[doc = r"When given, only return series moderated by [`UserId`]"]
98    moderator: Option<UserId<'a>>,
99    #[doc = r"Sorting options for results."]
100    orderby: Option<SeriesSorting>,
101    #[doc = r"Sort direction"]
102    direction: Option<Direction>,
103    #[builder(setter(name = "_embed"), private)]
104    #[serde(serialize_with = "super::utils::serialize_as_csv")]
105    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
106    embed: BTreeSet<SeriesEmbeds>,
107}
108
109/// Retrieves a single series
110#[derive(Debug, Builder, Clone)]
111#[builder(setter(into, strip_option))]
112pub struct Series<'a> {
113    #[doc = r"Series ID or abbreviation"]
114    id: SeriesId<'a>,
115}
116
117/// Retrieves all games for a given series
118#[derive(Debug, Clone)]
119pub struct SeriesGames<'a> {
120    id: SeriesId<'a>,
121    inner: Games<'a>,
122}
123
124/// Builder for [`SeriesGames`]
125#[derive(Default, Clone)]
126pub struct SeriesGamesBuilder<'a> {
127    /// Series ID or abbreviation
128    id: Option<SeriesId<'a>>,
129    inner: GamesBuilder<'a>,
130}
131
132impl ListSeries<'_> {
133    /// Create a builder for this endpoint.
134    pub fn builder<'a>() -> ListSeriesBuilder<'a> {
135        ListSeriesBuilder::default()
136    }
137}
138
139impl ListSeriesBuilder<'_> {
140    /// Add an embedded resource to this result
141    pub fn embed(&mut self, embed: SeriesEmbeds) -> &mut Self {
142        self.embed.get_or_insert_with(BTreeSet::new).insert(embed);
143        self
144    }
145
146    /// Add multiple embedded resources to this result
147    pub fn embeds<I>(&mut self, iter: I) -> &mut Self
148    where
149        I: Iterator<Item = SeriesEmbeds>,
150    {
151        self.embed.get_or_insert_with(BTreeSet::new).extend(iter);
152        self
153    }
154}
155
156impl Series<'_> {
157    /// Create a builder for this endpoint.
158    pub fn builder<'a>() -> SeriesBuilder<'a> {
159        SeriesBuilder::default()
160    }
161}
162
163impl SeriesGames<'_> {
164    /// Create a builder for this endpoint.
165    pub fn builder<'a>() -> SeriesGamesBuilder<'a> {
166        SeriesGamesBuilder::default()
167    }
168}
169
170impl<'a> SeriesGamesBuilder<'a> {
171    /// Create a new [`SeriesGamesBuilder`]
172    pub fn new() -> Self {
173        Self::default()
174    }
175
176    /// Series ID or abbreviation
177    pub fn id<S>(&mut self, id: S) -> &mut Self
178    where
179        S: Into<SeriesId<'a>>,
180    {
181        self.id = Some(id.into());
182        self
183    }
184
185    #[doc = r"Performs a fuzzy search across game names and abbreviations."]
186    pub fn name<S>(&mut self, value: S) -> &mut Self
187    where
188        S: Into<Cow<'a, str>>,
189    {
190        self.inner.name(value);
191        self
192    }
193
194    #[doc = r"Perform an exact-match search for this abbreviation."]
195    pub fn abbreviation<S>(&mut self, value: S) -> &mut Self
196    where
197        S: Into<Cow<'a, str>>,
198    {
199        self.inner.abbreviation(value);
200        self
201    }
202
203    #[doc = r"Restrict results to games released in the given year."]
204    pub fn released(&mut self, value: i64) -> &mut Self {
205        self.inner.released(value);
206        self
207    }
208
209    #[doc = r"Restrict results to the given game type."]
210    pub fn gametype<S>(&mut self, value: S) -> &mut Self
211    where
212        S: Into<GameTypeId<'a>>,
213    {
214        self.inner.gametype(value);
215        self
216    }
217
218    #[doc = r"Restrict results to the given platform."]
219    pub fn platform<S>(&mut self, value: S) -> &mut Self
220    where
221        S: Into<PlatformId<'a>>,
222    {
223        self.inner.platform(value);
224        self
225    }
226
227    #[doc = r"Restrict results to the given region."]
228    pub fn region<S>(&mut self, value: S) -> &mut Self
229    where
230        S: Into<RegionId<'a>>,
231    {
232        self.inner.region(value);
233        self
234    }
235
236    #[doc = r"Restrict results to the given genre."]
237    pub fn genre<S>(&mut self, value: S) -> &mut Self
238    where
239        S: Into<GenreId<'a>>,
240    {
241        self.inner.genre(value);
242        self
243    }
244
245    #[doc = r"Restrict results to the given engine."]
246    pub fn engine<S>(&mut self, value: S) -> &mut Self
247    where
248        S: Into<EngineId<'a>>,
249    {
250        self.inner.engine(value);
251        self
252    }
253
254    #[doc = r"Restrict results to the given developer."]
255    pub fn developer<S>(&mut self, value: S) -> &mut Self
256    where
257        S: Into<DeveloperId<'a>>,
258    {
259        self.inner.developer(value);
260        self
261    }
262
263    #[doc = r"Restrict results to the given publisher."]
264    pub fn publisher<S>(&mut self, value: S) -> &mut Self
265    where
266        S: Into<PublisherId<'a>>,
267    {
268        self.inner.publisher(value);
269        self
270    }
271
272    #[doc = r"Only return games moderated by the given user."]
273    pub fn moderator<S>(&mut self, value: S) -> &mut Self
274    where
275        S: Into<UserId<'a>>,
276    {
277        self.inner.moderator(value);
278        self
279    }
280
281    #[doc = r"Enable bulk access."]
282    pub fn bulk(&mut self, value: bool) -> &mut Self {
283        self.inner.bulk(value);
284        self
285    }
286
287    #[doc = r"Sorting options for results."]
288    pub fn orderby(&mut self, value: GamesSorting) -> &mut Self {
289        self.inner.orderby(value);
290        self
291    }
292
293    #[doc = r"Sort direction."]
294    pub fn direction(&mut self, value: Direction) -> &mut Self {
295        self.inner.direction(value);
296        self
297    }
298
299    /// Builds a new [`SeriesGames`]
300    pub fn build(&self) -> Result<SeriesGames<'a>, SeriesGamesBuilderError> {
301        let inner = self.inner.build()?;
302        Ok(SeriesGames {
303            id: self
304                .id
305                .as_ref()
306                .cloned()
307                .ok_or(SeriesGamesBuilderError::UninitializedField("id"))?,
308            inner,
309        })
310    }
311}
312
313impl SeriesEmbeds {
314    fn as_str(&self) -> &'static str {
315        match self {
316            SeriesEmbeds::Moderators => "moderators",
317        }
318    }
319}
320
321impl Default for SeriesSorting {
322    fn default() -> Self {
323        Self::NameInternational
324    }
325}
326
327impl Endpoint for ListSeries<'_> {
328    fn endpoint(&self) -> Cow<'static, str> {
329        "/series".into()
330    }
331
332    fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
333        QueryParams::with(self)
334    }
335}
336
337impl Endpoint for Series<'_> {
338    fn endpoint(&self) -> Cow<'static, str> {
339        format!("/series/{}", self.id).into()
340    }
341}
342
343impl Endpoint for SeriesGames<'_> {
344    fn endpoint(&self) -> Cow<'static, str> {
345        format!("/series/{}/games", self.id).into()
346    }
347
348    fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
349        QueryParams::with(&self.inner)
350    }
351}
352
353impl From<&SeriesEmbeds> for &'static str {
354    fn from(value: &SeriesEmbeds) -> Self {
355        value.as_str()
356    }
357}
358
359impl Pageable for ListSeries<'_> {}
360
361impl Pageable for SeriesGames<'_> {}