use std::fmt::{Debug, Display};
use crate::{util, IntoVersionIdent, Result};
use bytes::Bytes;
use futures_core::Stream;
use futures_util::StreamExt;
use reqwest::Method;
use serde::{de::DeserializeOwned, Serialize};
const DEFAULT_BASE_URL: &str = "https://thunderstore.io";
#[derive(Clone)]
pub struct Client {
pub(crate) base_url: String,
pub(crate) client: reqwest::Client,
pub(crate) token: Option<String>,
}
impl Client {
pub fn new() -> Self {
Self::default()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn set_base_url(&mut self, base_url: impl Into<String>) {
self.base_url = base_url.into();
}
pub fn token(&self) -> Option<&str> {
self.token.as_deref()
}
pub fn clear_token(&mut self) {
self.token = None;
}
pub fn set_token(&mut self, token: impl Into<String>) {
self.token = Some(token.into());
}
pub(crate) async fn request(
&self,
method: reqwest::Method,
url: impl reqwest::IntoUrl,
body: Option<reqwest::Body>,
headers: Option<reqwest::header::HeaderMap>,
) -> Result<reqwest::Response> {
let mut request = self.client.request(method, url);
if let Some(body) = body {
request = request.body(body);
}
if let Some(headers) = headers {
request = request.headers(headers);
}
if let Some(token) = &self.token {
request = request.bearer_auth(token);
}
util::map_reqwest_response(request.send().await)
}
pub(crate) async fn get(&self, url: impl reqwest::IntoUrl) -> Result<reqwest::Response> {
self.request(Method::GET, url, None, None).await
}
pub(crate) async fn get_json<T>(&self, url: impl reqwest::IntoUrl) -> Result<T>
where
T: DeserializeOwned,
{
Ok(self.get(url).await?.json().await?)
}
pub(crate) async fn post(
&self,
url: impl reqwest::IntoUrl,
body: impl Into<reqwest::Body>,
headers: Option<reqwest::header::HeaderMap>,
) -> Result<reqwest::Response> {
self.request(Method::POST, url, Some(body.into()), headers)
.await
}
pub(crate) async fn post_json<T>(
&self,
url: impl reqwest::IntoUrl,
body: &T,
) -> Result<reqwest::Response>
where
T: Serialize,
{
let headers = util::header_map([("Content-Type", "application/json")]);
self.post(url, serde_json::to_string(body)?, Some(headers))
.await
}
pub(crate) fn url(&self, path: impl Display) -> String {
format!("{}/api{}/", self.base_url, path)
}
async fn download_raw(&self, version: impl IntoVersionIdent<'_>) -> Result<reqwest::Response> {
let url = format!(
"{}/package/download/{}",
self.base_url,
version.into_id()?.path()
);
self.get(url).await
}
pub async fn stream_download(
&self,
version: impl IntoVersionIdent<'_>,
) -> Result<impl Stream<Item = Result<Bytes>>> {
let stream = self
.download_raw(version)
.await?
.bytes_stream()
.map(|item| item.map_err(|err| err.into()));
Ok(stream)
}
pub async fn download(&self, version: impl IntoVersionIdent<'_>) -> Result<Bytes> {
let res = self.download_raw(version).await?.bytes().await?;
Ok(res)
}
}
impl Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("base_url", &self.base_url)
.field("client", &self.client)
.field(
"token",
if self.token.is_some() {
&"Some(...)"
} else {
&"None"
},
)
.finish()
}
}
impl Default for Client {
fn default() -> Self {
Self {
base_url: DEFAULT_BASE_URL.to_string(),
client: reqwest::Client::default(),
token: None,
}
}
}
#[derive(Debug, Default)]
pub struct ClientBuilder {
base_url: Option<String>,
client: Option<reqwest::Client>,
token: Option<String>,
}
impl ClientBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn use_dev_repo(self) -> Self {
self.with_base_url("https://thunderstore.dev".to_owned())
}
pub fn with_client(mut self, client: reqwest::Client) -> Self {
self.client = Some(client);
self
}
pub fn with_token(mut self, token: impl Into<String>) -> Self {
self.token = Some(token.into());
self
}
pub fn build(self) -> Result<Client> {
Ok(Client {
base_url: self
.base_url
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
client: self.client.unwrap_or_default(),
token: self.token,
})
}
}