splits_io_api/
run.rs

1//! The run module handles retrieving Runs. A Run maps directly to an uploaded splits file.
2//!
3//! [API Documentation](https://github.com/glacials/splits-io/blob/master/docs/api.md#run)
4
5use crate::{get_json, get_response, schema::Run, wrapper::ContainsRun, Client, Error};
6use reqwest::{
7    header::ACCEPT,
8    multipart::{Form, Part},
9    Url,
10};
11use std::{
12    io::{self, Write},
13    ops::Deref,
14};
15
16impl Run {
17    /// Downloads the splits for the Run.
18    pub async fn download(&self, client: &Client) -> Result<impl Deref<Target = [u8]>, Error> {
19        self::download(
20            client,
21            self.id.as_deref().ok_or(Error::UnidentifiableResource)?,
22        )
23        .await
24    }
25
26    /// Gets a Run.
27    pub async fn get(client: &Client, id: &str, historic: bool) -> Result<Run, Error> {
28        self::get(client, id, historic).await
29    }
30
31    /// Uploads a run to splits.io.
32    pub async fn upload(client: &Client, run: Vec<u8>) -> Result<UploadedRun, Error> {
33        self::upload(client, run).await
34    }
35
36    /// Retrieves the public URL of the run. This may fail if the run is unidentifiable.
37    pub fn url(&self) -> Result<Url, Error> {
38        let mut url = Url::parse("https://splits.io").unwrap();
39        url.path_segments_mut()
40            .unwrap()
41            .push(self.id.as_deref().ok_or(Error::UnidentifiableResource)?);
42        Ok(url)
43    }
44}
45
46/// Downloads the splits for a Run.
47pub async fn download(client: &Client, id: &str) -> Result<impl Deref<Target = [u8]>, Error> {
48    let mut url = Url::parse("https://splits.io/api/v4/runs").unwrap();
49    url.path_segments_mut().unwrap().push(id);
50
51    get_response(
52        client,
53        client
54            .client
55            .get(url)
56            .header(ACCEPT, "application/original-timer"),
57    )
58    .await?
59    .bytes()
60    .await
61    .map_err(|source| Error::Download { source })
62}
63
64/// Gets a Run.
65pub async fn get(client: &Client, id: &str, historic: bool) -> Result<Run, Error> {
66    let mut url = Url::parse("https://splits.io/api/v4/runs").unwrap();
67    url.path_segments_mut().unwrap().push(id);
68    if historic {
69        url.query_pairs_mut().append_pair("historic", "1");
70    }
71
72    let ContainsRun { run } = get_json(client, client.client.get(url)).await?;
73
74    Ok(run)
75}
76
77#[derive(Debug, serde_derive::Deserialize)]
78struct UploadResponse {
79    id: Box<str>,
80    claim_token: Box<str>,
81    presigned_request: PresignedRequest,
82}
83
84#[derive(Debug, serde_derive::Deserialize)]
85struct PresignedRequest {
86    uri: Box<str>,
87    fields: PresignedRequestFields,
88}
89
90#[derive(Debug, serde_derive::Deserialize, serde_derive::Serialize)]
91struct PresignedRequestFields {
92    key: Box<str>,
93    policy: Box<str>,
94    #[serde(rename = "x-amz-credential")]
95    credential: Box<str>,
96    #[serde(rename = "x-amz-algorithm")]
97    algorithm: Box<str>,
98    #[serde(rename = "x-amz-date")]
99    date: Box<str>,
100    #[serde(rename = "x-amz-signature")]
101    signature: Box<str>,
102}
103
104/// A run that was uploaded to splits.io.
105#[derive(Debug)]
106pub struct UploadedRun {
107    /// The unique ID for identifying the run.
108    pub id: Box<str>,
109    /// The token that can be used by the user to claim the run as their own.
110    pub claim_token: Box<str>,
111}
112
113impl UploadedRun {
114    /// Gets the uploaded run.
115    pub async fn get(&self, client: &Client, historic: bool) -> Result<Run, Error> {
116        Run::get(client, &self.id, historic).await
117    }
118
119    /// Retrieves the public URL of the uploaded run.
120    pub fn public_url(&self) -> Url {
121        let mut url = Url::parse("https://splits.io").unwrap();
122        url.path_segments_mut().unwrap().push(&self.id);
123        url
124    }
125
126    /// Retrieves the URL to claim the uploaded run.
127    pub fn claim_url(&self) -> Url {
128        let mut url = self.public_url();
129        url.query_pairs_mut()
130            .append_pair("claim_token", &self.claim_token);
131        url
132    }
133}
134
135/// Handles writing a run to the body of an upload request.
136pub struct RunWriter(Vec<u8>);
137
138impl Write for RunWriter {
139    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
140        Write::write(&mut self.0, buf)
141    }
142    fn flush(&mut self) -> io::Result<()> {
143        Ok(())
144    }
145}
146
147/// Uploads a run to splits.io.
148pub async fn upload(client: &Client, run: Vec<u8>) -> Result<UploadedRun, Error> {
149    let UploadResponse {
150        id,
151        claim_token,
152        presigned_request: PresignedRequest { uri, fields },
153    } = get_json(client, client.client.post("https://splits.io/api/v4/runs")).await?;
154
155    get_response(
156        client,
157        client.client.post(String::from(uri)).multipart(
158            Form::new()
159                .text("key", String::from(fields.key))
160                .text("policy", String::from(fields.policy))
161                .text("x-amz-credential", String::from(fields.credential))
162                .text("x-amz-algorithm", String::from(fields.algorithm))
163                .text("x-amz-date", String::from(fields.date))
164                .text("x-amz-signature", String::from(fields.signature))
165                .part("file", Part::bytes(run)),
166        ),
167    )
168    .await?;
169
170    Ok(UploadedRun { id, claim_token })
171}