use std::time::Duration;
use bytes::Bytes;
use reqwest::{header::CONTENT_TYPE, Method, StatusCode, Url};
use serde::{de::DeserializeOwned, Serialize};
use crate::error::{ApiError, Error, Result};
use crate::types::{
Application, ApplicationsData, Channel, CreateShareLinkRequest, DownloadInfo, Envelope,
FilesData, GetDownloadInfoOptions, ListApplicationsOptions, ListVersionsOptions, Meta,
Pagination, ShareLink, Version, VersionFile, VersionsData,
};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const HEADER_X_CHANNEL: &str = "X-Channel";
#[derive(Debug, Clone)]
pub struct Page<T> {
pub items: T,
pub pagination: Option<Pagination>,
pub request_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Client {
base_url: Url,
api_key: String,
http: reqwest::Client,
}
impl Client {
pub fn new(base_url: impl AsRef<str>, api_key: impl Into<String>) -> Result<Self> {
let http = reqwest::Client::builder()
.timeout(DEFAULT_TIMEOUT)
.build()
.map_err(Error::Http)?;
Self::with_http_client(base_url, api_key, http)
}
pub fn with_http_client(
base_url: impl AsRef<str>,
api_key: impl Into<String>,
http: reqwest::Client,
) -> Result<Self> {
let trimmed = base_url.as_ref().trim_end_matches('/');
let base_url = Url::parse(&format!("{trimmed}/"))
.map_err(|e| Error::InvalidUrl(e.to_string()))?;
Ok(Self {
base_url,
api_key: api_key.into(),
http,
})
}
pub fn base_url(&self) -> &Url {
&self.base_url
}
fn url(&self, segments: &[&str]) -> Result<Url> {
let mut url = self.base_url.clone();
url.path_segments_mut()
.map_err(|()| Error::InvalidUrl("base url cannot be a base".to_owned()))?
.extend(segments);
Ok(url)
}
async fn send<B: Serialize + ?Sized>(
&self,
method: Method,
url: Url,
channel: Option<Channel>,
body: Option<&B>,
) -> Result<(StatusCode, Bytes)> {
let mut req = self
.http
.request(method, url)
.bearer_auth(&self.api_key)
.header(CONTENT_TYPE, "application/json");
if let Some(ch) = channel {
req = req.header(HEADER_X_CHANNEL, ch.as_str());
}
if let Some(body) = body {
req = req.json(body);
}
let resp = req.send().await?;
let status = resp.status();
let bytes = resp.bytes().await?;
Ok((status, bytes))
}
async fn request<T, B>(
&self,
method: Method,
url: Url,
channel: Option<Channel>,
body: Option<&B>,
) -> Result<(T, Option<Meta>)>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
let (status, bytes) = self.send(method, url, channel, body).await?;
let parsed: std::result::Result<Envelope<T>, serde_json::Error> =
serde_json::from_slice(&bytes);
if !status.is_success() {
let err = match parsed {
Ok(env) => env
.error
.unwrap_or_else(|| synthesize_error(status, &bytes, None))
.with_http_status(status.as_u16()),
Err(decode_err) => synthesize_error(status, &bytes, Some(decode_err)),
};
return Err(Error::Api(err));
}
let env = parsed?;
if let Some(err) = env.error {
return Err(Error::Api(err.with_http_status(status.as_u16())));
}
let data = env.data.ok_or(Error::MissingData)?;
Ok((data, env.meta))
}
pub async fn list_applications(
&self,
opts: &ListApplicationsOptions,
) -> Result<Page<Vec<Application>>> {
let mut url = self.url(&["v1", "applications"])?;
{
let mut q = url.query_pairs_mut();
if let Some(p) = opts.page {
q.append_pair("page", &p.to_string());
}
if let Some(l) = opts.limit {
q.append_pair("limit", &l.to_string());
}
if let Some(s) = opts.search.as_deref() {
q.append_pair("search", s);
}
if let Some(t) = opts.tag.as_deref() {
q.append_pair("tag", t);
}
if let Some(a) = opts.is_active {
q.append_pair("isActive", if a { "true" } else { "false" });
}
}
let (data, meta) = self
.request::<ApplicationsData, ()>(Method::GET, url, None, None)
.await?;
Ok(Page {
items: data.applications,
pagination: meta.as_ref().and_then(|m| m.pagination.clone()),
request_id: meta.and_then(|m| m.request_id),
})
}
pub async fn get_application(&self, application_id: &str) -> Result<Application> {
let url = self.url(&["v1", "applications", application_id])?;
let (data, _) = self
.request::<Application, ()>(Method::GET, url, None, None)
.await?;
Ok(data)
}
pub async fn list_versions(
&self,
application_id: &str,
opts: &ListVersionsOptions,
) -> Result<Page<Vec<Version>>> {
let mut url = self.url(&["v1", "applications", application_id, "versions"])?;
if let Some(c) = opts.changelog.as_deref() {
url.query_pairs_mut().append_pair("changelog", c);
}
let (data, meta) = self
.request::<VersionsData, ()>(Method::GET, url, opts.channel, None)
.await?;
Ok(Page {
items: data.versions,
pagination: meta.as_ref().and_then(|m| m.pagination.clone()),
request_id: meta.and_then(|m| m.request_id),
})
}
pub async fn list_version_files(
&self,
application_id: &str,
version: &str,
) -> Result<Vec<VersionFile>> {
let url = self.url(&["v1", "applications", application_id, "versions", version, "files"])?;
let (data, _) = self
.request::<FilesData, ()>(Method::GET, url, None, None)
.await?;
Ok(data.files)
}
pub async fn get_download_info(
&self,
application_id: &str,
version: &str,
opts: &GetDownloadInfoOptions,
) -> Result<DownloadInfo> {
let mut url = self.url(&["v1", "downloads", "url"])?;
{
let mut q = url.query_pairs_mut();
q.append_pair("applicationId", application_id);
q.append_pair("version", version);
if let Some(fid) = opts.file_id.as_deref() {
q.append_pair("fileId", fid);
}
}
let (data, _) = self
.request::<DownloadInfo, ()>(Method::GET, url, opts.channel, None)
.await?;
Ok(data)
}
pub async fn create_share_link(&self, req: &CreateShareLinkRequest) -> Result<ShareLink> {
let url = self.url(&["v1", "downloads", "share"])?;
let (data, _) = self
.request::<ShareLink, _>(Method::POST, url, None, Some(req))
.await?;
Ok(data)
}
pub async fn public_download(&self, token: &str) -> Result<reqwest::Response> {
let url = self.url(&["v1", "downloads", "d", token])?;
let resp = self.http.get(url).send().await?;
if !resp.status().is_success() {
let status = resp.status();
let bytes = resp.bytes().await.unwrap_or_default();
return Err(Error::Api(
synthesize_error(status, &bytes, None).with_http_status(status.as_u16()),
));
}
Ok(resp)
}
pub async fn download(
&self,
application_id: &str,
version: &str,
opts: &GetDownloadInfoOptions,
) -> Result<(DownloadInfo, Bytes)> {
let info = self
.get_download_info(application_id, version, opts)
.await?;
let resp = self.http.get(&info.url).send().await?;
let status = resp.status();
if !status.is_success() {
let bytes = resp.bytes().await.unwrap_or_default();
return Err(Error::Api(
synthesize_error(status, &bytes, None).with_http_status(status.as_u16()),
));
}
let bytes = resp.bytes().await?;
Ok((info, bytes))
}
}
fn synthesize_error(
status: StatusCode,
body: &[u8],
decode_err: Option<serde_json::Error>,
) -> ApiError {
let mut message = String::new();
if decode_err.is_none() {
#[derive(serde::Deserialize)]
struct Bare {
#[serde(default)]
message: Option<String>,
#[serde(default)]
error: Option<String>,
}
if let Ok(bare) = serde_json::from_slice::<Bare>(body) {
if let Some(m) = bare.message.filter(|s| !s.is_empty()) {
message = m;
} else if let Some(m) = bare.error.filter(|s| !s.is_empty()) {
message = m;
}
}
}
if message.is_empty() {
message = status
.canonical_reason()
.unwrap_or("http error")
.to_owned();
}
ApiError {
code: status_code_slug(status).to_owned(),
message,
http_status: Some(status.as_u16()),
}
}
fn status_code_slug(status: StatusCode) -> &'static str {
match status {
StatusCode::BAD_REQUEST => "bad_request",
StatusCode::UNAUTHORIZED => "unauthorized",
StatusCode::FORBIDDEN => "forbidden",
StatusCode::NOT_FOUND => "not_found",
StatusCode::CONFLICT => "conflict",
StatusCode::TOO_MANY_REQUESTS => "rate_limited",
s if s.is_server_error() => "server_error",
s if s.is_client_error() => "http_error",
_ => "http_error",
}
}