splits_io_api/
race.rs

1//! The race module handles retrieving Races. A Race is a competition between multiple Runners.
2//!
3//! [API Documentation](https://github.com/glacials/splits-io/blob/master/docs/api.md#race)
4
5use crate::{
6    get_json, get_response,
7    wrapper::{
8        ContainsChatMessage, ContainsChatMessages, ContainsEntries, ContainsEntry, ContainsRace,
9        ContainsRaces,
10    },
11    Attachment, ChatMessage, Client, Entry, Error, Race, Visibility,
12};
13use reqwest::Url;
14use std::ops::Deref;
15use uuid::Uuid;
16
17impl Race {
18    /// Gets all the currently active Races on splits.io.
19    pub async fn get_active(client: &Client) -> Result<Vec<Race>, Error> {
20        self::get_active(client).await
21    }
22
23    /// Gets a Race by its ID.
24    pub async fn get(client: &Client, id: Uuid) -> Result<Race, Error> {
25        self::get(client, id).await
26    }
27
28    /// Creates a new Race.
29    pub async fn create(client: &Client, settings: Settings<'_>) -> Result<Race, Error> {
30        self::create(client, settings).await
31    }
32
33    /// Updates the Race.
34    pub async fn update(
35        &self,
36        client: &Client,
37        settings: UpdateSettings<'_>,
38    ) -> Result<Race, Error> {
39        self::update(client, self.id, settings).await
40    }
41
42    /// Gets all of the entries for the Race.
43    pub async fn entries(&self, client: &Client) -> Result<Vec<Entry>, Error> {
44        self::get_entries(client, self.id).await
45    }
46
47    /// Gets the entry in the Race that is associated with the current user.
48    pub async fn my_entry(&self, client: &Client) -> Result<Entry, Error> {
49        self::get_entry(client, self.id).await
50    }
51
52    /// Joins the Race for the given entry.
53    pub async fn join(
54        &self,
55        client: &Client,
56        join_as: JoinAs<'_>,
57        join_token: Option<&str>,
58    ) -> Result<Entry, Error> {
59        self::join(client, self.id, join_as, join_token).await
60    }
61
62    /// Leaves the Race for the given entry.
63    pub async fn leave(&self, client: &Client, entry_id: Uuid) -> Result<(), Error> {
64        self::leave(client, self.id, entry_id).await
65    }
66
67    /// Declares the given entry as ready for the Race.
68    pub async fn ready_up(&self, client: &Client, entry_id: Uuid) -> Result<Entry, Error> {
69        self::ready_up(client, self.id, entry_id).await
70    }
71
72    /// Undoes a ready for the given entry in th Race.
73    pub async fn unready(&self, client: &Client, entry_id: Uuid) -> Result<Entry, Error> {
74        self::unready(client, self.id, entry_id).await
75    }
76
77    /// Finishes the Race for the given entry.
78    pub async fn finish(&self, client: &Client, entry_id: Uuid) -> Result<Entry, Error> {
79        self::finish(client, self.id, entry_id).await
80    }
81
82    /// Undoes a finish for the given entry in the Race.
83    pub async fn undo_finish(&self, client: &Client, entry_id: Uuid) -> Result<Entry, Error> {
84        self::undo_finish(client, self.id, entry_id).await
85    }
86
87    /// Forfeits the Race for the given entry.
88    pub async fn forfeit(&self, client: &Client, entry_id: Uuid) -> Result<Entry, Error> {
89        self::forfeit(client, self.id, entry_id).await
90    }
91
92    /// Undoes a forfeit for the given entry in the Race.
93    pub async fn undo_forfeit(&self, client: &Client, entry_id: Uuid) -> Result<Entry, Error> {
94        self::undo_forfeit(client, self.id, entry_id).await
95    }
96
97    /// Gets all of the chat messages for the Race.
98    pub async fn chat_messages(&self, client: &Client) -> Result<Vec<ChatMessage>, Error> {
99        self::get_chat(client, self.id).await
100    }
101
102    /// Sends a message in the chat for the Race.
103    pub async fn send_chat_message(
104        &self,
105        client: &Client,
106        message: &str,
107    ) -> Result<ChatMessage, Error> {
108        self::send_chat_message(client, self.id, message).await
109    }
110}
111
112impl Attachment {
113    /// Downloads the attachment.
114    pub async fn download(&self, client: &Client) -> Result<impl Deref<Target = [u8]>, Error> {
115        get_response(client, client.client.get(&*self.url))
116            .await?
117            .bytes()
118            .await
119            .map_err(|source| Error::Download { source })
120    }
121}
122
123/// Gets all the currently active Races on splits.io.
124pub async fn get_active(client: &Client) -> Result<Vec<Race>, Error> {
125    let ContainsRaces { races } =
126        get_json(client, client.client.get("https://splits.io/api/v4/races")).await?;
127
128    Ok(races)
129}
130
131// FIXME: get_all
132
133/// Gets a Race by its ID.
134pub async fn get(client: &Client, id: Uuid) -> Result<Race, Error> {
135    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
136    url.path_segments_mut()
137        .unwrap()
138        .push(id.hyphenated().encode_lower(&mut Uuid::encode_buffer()));
139
140    let ContainsRace { race } = get_json(client, client.client.get(url)).await?;
141
142    Ok(race)
143}
144
145/// The settings for a Race.
146#[derive(Default, serde_derive::Serialize)]
147pub struct Settings<'a> {
148    /// The ID of the Game that is being raced.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub game_id: Option<&'a str>,
151    /// The ID of the Category that is being raced.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub category_id: Option<&'a str>,
154    /// Any notes that are associated with the Race.
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub notes: Option<&'a str>,
157    /// The visibility of the Race.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub visibility: Option<Visibility>,
160}
161
162/// The type of update to perform on the given property.
163#[derive(Default)]
164pub enum Update<T> {
165    /// Keep the previous value of the property.
166    #[default]
167    Keep,
168    /// Clear the value of the property.
169    Clear,
170    /// Change the value of the property.
171    Set(T),
172}
173
174impl<T> Update<T> {
175    const fn is_keep(&self) -> bool {
176        matches!(self, Update::Keep)
177    }
178}
179
180impl<T: serde::Serialize> serde::Serialize for Update<T> {
181    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182    where
183        S: serde::Serializer,
184    {
185        match self {
186            Update::Set(val) => serializer.serialize_some(val),
187            _ => serializer.serialize_none(),
188        }
189    }
190}
191
192/// The new properties to use for a Race when performing an update.
193#[derive(Default, serde_derive::Serialize)]
194pub struct UpdateSettings<'a> {
195    /// The update to perform for the ID of the Game that is being raced.
196    #[serde(skip_serializing_if = "Update::is_keep")]
197    pub game_id: Update<&'a str>,
198    /// The update to perform for the ID of the Category that is being raced.
199    #[serde(skip_serializing_if = "Update::is_keep")]
200    pub category_id: Update<&'a str>,
201    /// The update to perform for any notes that are associated with the Race.
202    #[serde(skip_serializing_if = "Update::is_keep")]
203    pub notes: Update<&'a str>,
204    /// The update to perform for the visibility of the Race.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub visibility: Option<Visibility>,
207}
208
209/// Creates a new Race.
210pub async fn create(client: &Client, settings: Settings<'_>) -> Result<Race, Error> {
211    let ContainsRace { race } = get_json(
212        client,
213        client
214            .client
215            .post("https://splits.io/api/v4/races")
216            .json(&settings),
217    )
218    .await?;
219
220    Ok(race)
221}
222
223/// Updates a Race.
224pub async fn update(
225    client: &Client,
226    id: Uuid,
227    settings: UpdateSettings<'_>,
228) -> Result<Race, Error> {
229    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
230    url.path_segments_mut()
231        .unwrap()
232        .push(id.hyphenated().encode_lower(&mut Uuid::encode_buffer()));
233
234    let ContainsRace { race } = get_json(client, client.client.patch(url).json(&settings)).await?;
235
236    Ok(race)
237}
238
239/// Gets all of the entries for a Race.
240pub async fn get_entries(client: &Client, id: Uuid) -> Result<Vec<Entry>, Error> {
241    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
242    url.path_segments_mut().unwrap().extend(&[
243        id.hyphenated().encode_lower(&mut Uuid::encode_buffer()),
244        "entries",
245    ]);
246
247    let ContainsEntries { entries } = get_json(client, client.client.get(url)).await?;
248
249    Ok(entries)
250}
251
252/// Gets the entry in a Race that is associated with the current user.
253pub async fn get_entry(client: &Client, id: Uuid) -> Result<Entry, Error> {
254    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
255    url.path_segments_mut().unwrap().extend(&[
256        id.hyphenated().encode_lower(&mut Uuid::encode_buffer()),
257        "entry",
258    ]);
259
260    let ContainsEntry { entry } = get_json(client, client.client.get(url)).await?;
261
262    Ok(entry)
263}
264
265/// The type of racer to join the Race as.
266pub enum JoinAs<'a> {
267    /// Join the Race as a regular user.
268    Myself,
269    /// Join the Race as a ghost of a past Run.
270    Ghost(&'a str),
271}
272
273#[derive(serde_derive::Serialize)]
274struct JoinToken<'a> {
275    #[serde(skip_serializing_if = "Option::is_none")]
276    join_token: Option<&'a str>,
277    #[serde(skip_serializing_if = "Option::is_none")]
278    entry: Option<JoinEntry<'a>>,
279}
280
281#[derive(serde_derive::Serialize)]
282struct JoinEntry<'a> {
283    run_id: &'a str,
284}
285
286/// Joins the Race for the given entry.
287pub async fn join(
288    client: &Client,
289    race_id: Uuid,
290    join_as: JoinAs<'_>,
291    join_token: Option<&str>,
292) -> Result<Entry, Error> {
293    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
294    url.path_segments_mut().unwrap().extend(&[
295        race_id
296            .hyphenated()
297            .encode_lower(&mut Uuid::encode_buffer()),
298        "entries",
299    ]);
300
301    let ContainsEntry { entry } = get_json(
302        client,
303        client.client.post(url).json(&JoinToken {
304            join_token,
305            entry: match join_as {
306                JoinAs::Myself => None,
307                JoinAs::Ghost(run_id) => Some(JoinEntry { run_id }),
308            },
309        }),
310    )
311    .await?;
312
313    Ok(entry)
314}
315
316/// Leaves the Race for the given entry.
317pub async fn leave(client: &Client, race_id: Uuid, entry_id: Uuid) -> Result<(), Error> {
318    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
319    url.path_segments_mut().unwrap().extend(&[
320        race_id
321            .hyphenated()
322            .encode_lower(&mut Uuid::encode_buffer()),
323        "entries",
324        entry_id
325            .hyphenated()
326            .encode_lower(&mut Uuid::encode_buffer()),
327    ]);
328
329    get_response(client, client.client.delete(url)).await?;
330
331    Ok(())
332}
333
334#[derive(serde_derive::Serialize)]
335struct UpdateEntry<T> {
336    entry: T,
337}
338
339#[derive(serde_derive::Serialize)]
340struct ReadyState {
341    readied_at: Option<&'static str>,
342}
343
344#[derive(serde_derive::Serialize)]
345struct FinishState {
346    finished_at: Option<&'static str>,
347}
348
349#[derive(serde_derive::Serialize)]
350struct ForfeitState {
351    forfeited_at: Option<&'static str>,
352}
353
354/// Declares the given entry as ready for a Race.
355pub async fn ready_up(client: &Client, race_id: Uuid, entry_id: Uuid) -> Result<Entry, Error> {
356    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
357    url.path_segments_mut().unwrap().extend(&[
358        race_id
359            .hyphenated()
360            .encode_lower(&mut Uuid::encode_buffer()),
361        "entries",
362        entry_id
363            .hyphenated()
364            .encode_lower(&mut Uuid::encode_buffer()),
365    ]);
366
367    let ContainsEntry { entry } = get_json(
368        client,
369        client.client.patch(url).json(&UpdateEntry {
370            entry: ReadyState {
371                readied_at: Some("now"),
372            },
373        }),
374    )
375    .await?;
376
377    Ok(entry)
378}
379
380/// Undoes a ready for the given entry in a Race.
381pub async fn unready(client: &Client, race_id: Uuid, entry_id: Uuid) -> Result<Entry, Error> {
382    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
383    url.path_segments_mut().unwrap().extend(&[
384        race_id
385            .hyphenated()
386            .encode_lower(&mut Uuid::encode_buffer()),
387        "entries",
388        entry_id
389            .hyphenated()
390            .encode_lower(&mut Uuid::encode_buffer()),
391    ]);
392
393    let ContainsEntry { entry } = get_json(
394        client,
395        client.client.patch(url).json(&UpdateEntry {
396            entry: ReadyState { readied_at: None },
397        }),
398    )
399    .await?;
400
401    Ok(entry)
402}
403
404/// Finishes the Race for the given entry.
405pub async fn finish(client: &Client, race_id: Uuid, entry_id: Uuid) -> Result<Entry, Error> {
406    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
407    url.path_segments_mut().unwrap().extend(&[
408        race_id
409            .hyphenated()
410            .encode_lower(&mut Uuid::encode_buffer()),
411        "entries",
412        entry_id
413            .hyphenated()
414            .encode_lower(&mut Uuid::encode_buffer()),
415    ]);
416
417    let ContainsEntry { entry } = get_json(
418        client,
419        client.client.patch(url).json(&UpdateEntry {
420            entry: FinishState {
421                finished_at: Some("now"),
422            },
423        }),
424    )
425    .await?;
426
427    Ok(entry)
428}
429
430/// Undoes a finish for the given entry in a Race.
431pub async fn undo_finish(client: &Client, race_id: Uuid, entry_id: Uuid) -> Result<Entry, Error> {
432    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
433    url.path_segments_mut().unwrap().extend(&[
434        race_id
435            .hyphenated()
436            .encode_lower(&mut Uuid::encode_buffer()),
437        "entries",
438        entry_id
439            .hyphenated()
440            .encode_lower(&mut Uuid::encode_buffer()),
441    ]);
442
443    let ContainsEntry { entry } = get_json(
444        client,
445        client.client.patch(url).json(&UpdateEntry {
446            entry: FinishState { finished_at: None },
447        }),
448    )
449    .await?;
450
451    Ok(entry)
452}
453
454/// Forfeits the Race for the given entry.
455pub async fn forfeit(client: &Client, race_id: Uuid, entry_id: Uuid) -> Result<Entry, Error> {
456    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
457    url.path_segments_mut().unwrap().extend(&[
458        race_id
459            .hyphenated()
460            .encode_lower(&mut Uuid::encode_buffer()),
461        "entries",
462        entry_id
463            .hyphenated()
464            .encode_lower(&mut Uuid::encode_buffer()),
465    ]);
466
467    let ContainsEntry { entry } = get_json(
468        client,
469        client.client.patch(url).json(&UpdateEntry {
470            entry: ForfeitState {
471                forfeited_at: Some("now"),
472            },
473        }),
474    )
475    .await?;
476
477    Ok(entry)
478}
479
480/// Undoes a forfeit for the given entry in a Race.
481pub async fn undo_forfeit(client: &Client, race_id: Uuid, entry_id: Uuid) -> Result<Entry, Error> {
482    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
483    url.path_segments_mut().unwrap().extend(&[
484        race_id
485            .hyphenated()
486            .encode_lower(&mut Uuid::encode_buffer()),
487        "entries",
488        entry_id
489            .hyphenated()
490            .encode_lower(&mut Uuid::encode_buffer()),
491    ]);
492
493    let ContainsEntry { entry } = get_json(
494        client,
495        client.client.patch(url).json(&UpdateEntry {
496            entry: ForfeitState { forfeited_at: None },
497        }),
498    )
499    .await?;
500
501    Ok(entry)
502}
503
504/// Gets all of the chat messages for a Race.
505pub async fn get_chat(client: &Client, id: Uuid) -> Result<Vec<ChatMessage>, Error> {
506    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
507    url.path_segments_mut().unwrap().extend(&[
508        id.hyphenated().encode_lower(&mut Uuid::encode_buffer()),
509        "chat",
510    ]);
511
512    let ContainsChatMessages { chat_messages } = get_json(client, client.client.get(url)).await?;
513
514    Ok(chat_messages)
515}
516
517#[derive(serde_derive::Serialize)]
518struct SendMessage<'a> {
519    chat_message: SendMessageBody<'a>,
520}
521
522#[derive(serde_derive::Serialize)]
523struct SendMessageBody<'a> {
524    body: &'a str,
525}
526
527/// Sends a message in the chat for a Race.
528pub async fn send_chat_message(
529    client: &Client,
530    id: Uuid,
531    message: &str,
532) -> Result<ChatMessage, Error> {
533    let mut url = Url::parse("https://splits.io/api/v4/races").unwrap();
534    url.path_segments_mut().unwrap().extend(&[
535        id.hyphenated().encode_lower(&mut Uuid::encode_buffer()),
536        "chat",
537    ]);
538
539    let ContainsChatMessage { chat_message } = get_json(
540        client,
541        client.client.post(url).json(&SendMessage {
542            chat_message: SendMessageBody { body: message },
543        }),
544    )
545    .await?;
546
547    Ok(chat_message)
548}