use crate::auth::ApiKey;
use crate::error::{Error, Result};
use crate::jsonapi::ErrorDocument;
use reqwest::{Method, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::sync::Mutex;
use std::time::{Duration, Instant};
const BASE_URL: &str = "https://api.appstoreconnect.apple.com";
const TOKEN_REFRESH_MARGIN: Duration = Duration::from_secs(60);
struct CachedToken {
value: String,
minted_at: Instant,
lifetime: Duration,
}
pub struct Client {
http: reqwest::Client,
api_key: ApiKey,
base_url: String,
token: Mutex<Option<CachedToken>>,
}
impl Client {
pub fn new(api_key: ApiKey) -> Self {
Self {
http: reqwest::Client::new(),
api_key,
base_url: BASE_URL.to_string(),
token: Mutex::new(None),
}
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
fn bearer_token(&self) -> Result<String> {
let mut guard = self
.token
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(cached) = guard.as_ref() {
if cached.minted_at.elapsed() + TOKEN_REFRESH_MARGIN < cached.lifetime {
return Ok(cached.value.clone());
}
}
let value = self.api_key.token()?;
*guard = Some(CachedToken {
value: value.clone(),
minted_at: Instant::now(),
lifetime: Duration::from_secs(self.api_key.lifetime_secs()),
});
Ok(value)
}
pub async fn request<B: Serialize, T: DeserializeOwned>(
&self,
method: Method,
path: &str,
query: &[(&str, &str)],
body: Option<&B>,
) -> Result<T> {
let (status, bytes) = self.send(method, path, query, body).await?;
if !status.is_success() {
return Err(api_error(status, &bytes));
}
Ok(serde_json::from_slice(&bytes)?)
}
pub async fn request_no_content<B: Serialize>(
&self,
method: Method,
path: &str,
query: &[(&str, &str)],
body: Option<&B>,
) -> Result<()> {
let (status, bytes) = self.send(method, path, query, body).await?;
if !status.is_success() {
return Err(api_error(status, &bytes));
}
Ok(())
}
async fn send<B: Serialize>(
&self,
method: Method,
path: &str,
query: &[(&str, &str)],
body: Option<&B>,
) -> Result<(StatusCode, Vec<u8>)> {
let url = format!("{}{}", self.base_url, path);
let token = self.bearer_token()?;
let mut req = self.http.request(method, &url).bearer_auth(token);
if !query.is_empty() {
req = req.query(query);
}
if let Some(b) = body {
req = req.json(b);
}
let response = req.send().await?;
let status = response.status();
let bytes = response.bytes().await?.to_vec();
Ok((status, bytes))
}
pub async fn upload_bytes(
&self,
method: Method,
url: &str,
headers: &[(String, String)],
body: Vec<u8>,
) -> Result<()> {
let mut req = self.http.request(method, url);
for (name, value) in headers {
req = req.header(name.as_str(), value.as_str());
}
let response = req.body(body).send().await?;
let status = response.status();
if !status.is_success() {
let bytes = response.bytes().await?.to_vec();
return Err(api_error(status, &bytes));
}
Ok(())
}
}
fn api_error(status: StatusCode, bytes: &[u8]) -> Error {
let detail = match serde_json::from_slice::<ErrorDocument>(bytes) {
Ok(doc) if !doc.errors.is_empty() => doc
.errors
.iter()
.map(|e| format!("{} ({}): {}", e.title, e.code, e.detail))
.collect::<Vec<_>>()
.join("; "),
_ => String::from_utf8_lossy(bytes).into_owned(),
};
Error::Api {
status: status.as_u16(),
detail,
}
}