1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
#![warn(
clippy::complexity,
clippy::correctness,
clippy::perf,
clippy::style,
clippy::missing_const_for_fn,
clippy::undocumented_unsafe_blocks,
missing_docs,
rust_2018_idioms
)]
//! splits-io-api is a library that provides bindings for the splits.io API for Rust.
//!
//! ```no_run
//! # use splits_io_api::{Client, Runner};
//! # use anyhow::Context;
//! #
//! # async fn query_api() -> anyhow::Result<()> {
//! // Create a splits.io API client.
//! let client = Client::new();
//!
//! // Search for a runner.
//! let runner = Runner::search(&client, "cryze")
//! .await?
//! .into_iter()
//! .next()
//! .context("There is no runner with that name")?;
//!
//! assert_eq!(&*runner.name, "cryze92");
//!
//! // Get the PBs for the runner.
//! let first_pb = runner.pbs(&client)
//! .await?
//! .into_iter()
//! .next()
//! .context("This runner doesn't have any PBs")?;
//!
//! // Get the game for the PB.
//! let game = first_pb.game.context("There is no game for the PB")?;
//!
//! assert_eq!(&*game.name, "The Legend of Zelda: The Wind Waker");
//!
//! // Get the categories for the game.
//! let categories = game.categories(&client).await?;
//!
//! // Get the runs for the Any% category.
//! let runs = categories
//! .iter()
//! .find(|category| &*category.name == "Any%")
//! .context("Couldn't find category")?
//! .runs(&client)
//! .await?;
//!
//! assert!(!runs.is_empty());
//! # Ok(())
//! # }
//! ```
use std::fmt;
use reqwest::{header::AUTHORIZATION, RequestBuilder, Response, StatusCode};
pub mod category;
// pub mod event;
pub mod game;
pub mod race;
pub mod run;
pub mod runner;
mod schema;
mod wrapper;
pub use schema::*;
pub use uuid;
/// A client that can access the splits.io API. This includes an access token that is used for
/// authentication to all API endpoints.
pub struct Client {
client: reqwest::Client,
access_token: Option<String>,
}
impl Default for Client {
fn default() -> Self {
#[allow(unused_mut)]
let mut builder = reqwest::Client::builder();
#[cfg(not(target_family = "wasm"))]
{
builder = builder.http2_prior_knowledge();
#[cfg(feature = "rustls")]
{
builder = builder.use_rustls_tls();
}
}
Client {
client: builder.build().unwrap(),
access_token: None,
}
}
}
impl Client {
/// Creates a new client.
pub fn new() -> Self {
Self::default()
}
/// Sets the client's access token, which can be used to authenticate to all API endpoints.
pub fn set_access_token(&mut self, access_token: &str) {
let buf = self.access_token.get_or_insert_with(String::new);
buf.clear();
buf.push_str("Bearer ");
buf.push_str(access_token);
}
}
#[derive(Debug)]
/// An error when making an API request.
pub enum Error {
/// An HTTP error outside of the API.
Status {
/// The HTTP status code of the error.
status: StatusCode,
},
/// An error thrown by the API.
Api {
/// The HTTP status code of the error.
status: StatusCode,
/// The error message.
message: Box<str>,
},
/// Failed downloading the response.
Download {
/// The reason why downloading the response failed.
source: reqwest::Error,
},
/// The resource can not be sufficiently identified for finding resources
/// attached to it.
UnidentifiableResource,
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Status { status } => {
write!(
fmt,
"HTTP Status: {}",
status.canonical_reason().unwrap_or_else(|| status.as_str()),
)
}
Error::Api { message, .. } => fmt::Display::fmt(message, fmt),
Error::Download { .. } => {
fmt::Display::fmt("Failed downloading the response.", fmt)
}
Error::UnidentifiableResource => {
fmt::Display::fmt(
"The resource can not be sufficiently identified for finding resources attached to it.",
fmt,
)
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Status { .. } => None,
Error::Api { .. } => None,
Error::Download { source, .. } => Some(source),
Error::UnidentifiableResource => None,
}
}
}
async fn get_response_unchecked(
client: &Client,
mut request: RequestBuilder,
) -> Result<Response, Error> {
// FIXME: Only for requests that need it.
if let Some(access_token) = &client.access_token {
request = request.header(AUTHORIZATION, access_token);
}
request
.send()
.await
.map_err(|source| Error::Download { source })
}
async fn get_response(client: &Client, request: RequestBuilder) -> Result<Response, Error> {
let response = get_response_unchecked(client, request).await?;
let status = response.status();
if !status.is_success() {
if let Ok(error) = response.json::<ApiError>().await {
return Err(Error::Api {
status,
message: error.error,
});
}
return Err(Error::Status { status });
}
Ok(response)
}
async fn get_json<T: serde::de::DeserializeOwned>(
client: &Client,
request: RequestBuilder,
) -> Result<T, Error> {
let response = get_response(client, request).await?;
response
.json()
.await
.map_err(|source| Error::Download { source })
}
#[derive(serde_derive::Deserialize)]
struct ApiError {
#[serde(alias = "message")]
error: Box<str>,
}