1#![warn(
2 clippy::complexity,
3 clippy::correctness,
4 clippy::perf,
5 clippy::style,
6 clippy::missing_const_for_fn,
7 clippy::undocumented_unsafe_blocks,
8 missing_docs,
9 rust_2018_idioms
10)]
11
12use std::fmt;
60
61use reqwest::{header::AUTHORIZATION, RequestBuilder, Response, StatusCode};
62
63pub mod category;
64pub mod game;
66pub mod race;
67pub mod run;
68pub mod runner;
69mod schema;
70mod wrapper;
71pub use schema::*;
72
73pub use uuid;
74
75pub struct Client {
78 client: reqwest::Client,
79 access_token: Option<String>,
80}
81
82impl Default for Client {
83 fn default() -> Self {
84 #[allow(unused_mut)]
85 let mut builder = reqwest::Client::builder();
86 #[cfg(not(target_family = "wasm"))]
87 {
88 builder = builder.http2_prior_knowledge();
89 #[cfg(feature = "rustls")]
90 {
91 builder = builder.use_rustls_tls();
92 }
93 }
94
95 Client {
96 client: builder.build().unwrap(),
97 access_token: None,
98 }
99 }
100}
101
102impl Client {
103 pub fn new() -> Self {
105 Self::default()
106 }
107
108 pub fn set_access_token(&mut self, access_token: &str) {
110 let buf = self.access_token.get_or_insert_with(String::new);
111 buf.clear();
112 buf.push_str("Bearer ");
113 buf.push_str(access_token);
114 }
115}
116
117#[derive(Debug)]
118pub enum Error {
120 Status {
122 status: StatusCode,
124 },
125 Api {
127 status: StatusCode,
129 message: Box<str>,
131 },
132 Download {
134 source: reqwest::Error,
136 },
137 UnidentifiableResource,
140}
141
142impl fmt::Display for Error {
143 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match self {
145 Error::Status { status } => {
146 write!(
147 fmt,
148 "HTTP Status: {}",
149 status.canonical_reason().unwrap_or_else(|| status.as_str()),
150 )
151 }
152 Error::Api { message, .. } => fmt::Display::fmt(message, fmt),
153 Error::Download { .. } => {
154 fmt::Display::fmt("Failed downloading the response.", fmt)
155 }
156 Error::UnidentifiableResource => {
157 fmt::Display::fmt(
158 "The resource can not be sufficiently identified for finding resources attached to it.",
159 fmt,
160 )
161 }
162 }
163 }
164}
165
166impl std::error::Error for Error {
167 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
168 match self {
169 Error::Status { .. } => None,
170 Error::Api { .. } => None,
171 Error::Download { source, .. } => Some(source),
172 Error::UnidentifiableResource => None,
173 }
174 }
175}
176
177async fn get_response_unchecked(
178 client: &Client,
179 mut request: RequestBuilder,
180) -> Result<Response, Error> {
181 if let Some(access_token) = &client.access_token {
183 request = request.header(AUTHORIZATION, access_token);
184 }
185
186 request
187 .send()
188 .await
189 .map_err(|source| Error::Download { source })
190}
191
192async fn get_response(client: &Client, request: RequestBuilder) -> Result<Response, Error> {
193 let response = get_response_unchecked(client, request).await?;
194 let status = response.status();
195 if !status.is_success() {
196 if let Ok(error) = response.json::<ApiError>().await {
197 return Err(Error::Api {
198 status,
199 message: error.error,
200 });
201 }
202 return Err(Error::Status { status });
203 }
204 Ok(response)
205}
206
207async fn get_json<T: serde::de::DeserializeOwned>(
208 client: &Client,
209 request: RequestBuilder,
210) -> Result<T, Error> {
211 let response = get_response(client, request).await?;
212 response
213 .json()
214 .await
215 .map_err(|source| Error::Download { source })
216}
217
218#[derive(serde_derive::Deserialize)]
219struct ApiError {
220 #[serde(alias = "message")]
221 error: Box<str>,
222}