use crate::api::*;
use reqwest::{header, Client as RClient, Method, Response, StatusCode, Url};
use serde::Deserialize;
use std::collections::HashMap;
use thiserror::Error;
type APIError = String;
#[derive(Error, Debug)]
pub enum BuilderError {
#[error("Failed to convert into a valid URL")]
URLConversion,
#[error("No API key was set")]
NoAPIKey,
}
pub struct Builder {
api_domain: Url,
api_key: Option<String>,
}
impl Builder {
pub fn new() -> Self {
Self {
api_domain: "https://api.freestuffbot.xyz"
.parse()
.expect("Failed to parse default API base URL"),
api_key: None,
}
}
pub fn api_domain<U: TryInto<Url>>(mut self, domain: U) -> Result<Builder, BuilderError> {
self.api_domain = domain.try_into().map_err(|_| BuilderError::URLConversion)?;
Ok(self)
}
pub fn key(mut self, key: &str) -> Self {
self.api_key = Some(key.to_string());
self
}
pub fn build(self) -> Result<Client, BuilderError> {
let api_key = self.api_key.ok_or(BuilderError::NoAPIKey)?;
let http_client = RClient::builder()
.https_only(true)
.build()
.expect("failed to build http client");
Ok(Client {
api_domain: self.api_domain,
api_key,
http_client,
})
}
}
type ClientResult<T> = Result<T, ClientError>;
#[derive(Error, Debug)]
pub enum ClientError {
#[error("HTTP error")]
HTTP(reqwest::Error),
#[error("Invalid response from API")]
InvalidResponse,
#[error("API error: {0}")]
API(APIError),
#[error("Too many requests")]
Ratelimited,
}
pub struct Client {
api_domain: Url,
api_key: String,
http_client: RClient,
}
impl Client {
pub fn builder() -> Builder {
Builder::new()
}
fn api_endpoint(&self, endpoint: &str) -> Url {
self.api_domain
.join(endpoint)
.expect("Failed to construct API endpoint URL")
}
async fn send_request(
&self,
endpoint: &str,
_parameters: Option<()>,
) -> ClientResult<Response> {
let url = self.api_endpoint(endpoint);
let request = self
.http_client
.request(Method::GET, url)
.header(header::AUTHORIZATION, format!("Basic {}", self.api_key))
.build()
.map_err(ClientError::HTTP)?;
self.http_client
.execute(request)
.await
.map_err(ClientError::HTTP)
.and_then(|response| match response.status() {
status if status.is_success() => Ok(response),
StatusCode::TOO_MANY_REQUESTS => Err(ClientError::Ratelimited),
_ => Err(ClientError::InvalidResponse),
})
}
pub async fn ping(&self) -> ClientResult<bool> {
Ok(self
.send_request("/v1/ping", None)
.await?
.status()
.is_success())
}
pub async fn game_list(&self, category: &str) -> ClientResult<Vec<GameId>> {
let path = format!("/v1/games/{category}");
self.send_request(&path, None)
.await?
.json::<ApiResponse<Vec<GameId>>>()
.await
.map_err(ClientError::HTTP)?
.into_data()
.map_err(ClientError::API)
}
pub async fn game_details(&self, games: &[GameId]) -> ClientResult<HashMap<String, GameInfo>> {
if games.len() == 0 {
return Ok(HashMap::new());
}
let ids = games
.iter()
.map(|id| id.to_string())
.reduce(|acc, id| format!("{acc}+{id}"))
.expect("at least one id must be specified");
let path = format!("/v1/game/{ids}/info");
self.send_request(&path, None)
.await?
.json::<ApiResponse<HashMap<String, GameInfo>>>()
.await
.map_err(ClientError::HTTP)?
.into_data()
.map_err(ClientError::API)
}
pub async fn game_detail(&self, game: GameId) -> ClientResult<GameInfo> {
self.game_details(&[game])
.await
.and_then(|map| map.into_values().next().ok_or(ClientError::InvalidResponse))
}
}
#[derive(Debug, Deserialize)]
struct ApiResponse<Data> {
success: bool,
error: Option<String>,
message: Option<String>,
data: Data,
}
impl<Data> ApiResponse<Data>
where
Data: std::fmt::Debug,
{
pub fn into_data(self) -> Result<Data, APIError> {
match (&self.message, &self.error) {
(None, None) if self.success => Ok(self.data),
(Some(message), Some(err)) => Err(format!("{err} ({message})")),
(None, Some(err)) => Err(err.to_string()),
_ => Err("invalid response".to_string()),
}
}
}