use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use reqwest::Method;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::time::Duration;
use crate::error::{extract_error_message, Error, Result};
const API_KEY_HEADER: &str = "X-API-KEY";
const USER_AGENT: &str = concat!("dimescheduler-rust/", env!("CARGO_PKG_VERSION"));
const TIMEOUT: Duration = Duration::from_secs(60);
const SEGMENT: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'_')
.remove(b'.')
.remove(b'~');
pub(crate) fn encode_segment(s: &str) -> String {
utf8_percent_encode(s, SEGMENT).to_string()
}
#[derive(Clone)]
pub(crate) struct Transport {
http: reqwest::Client,
base_url: String,
api_key: String,
}
impl Transport {
pub(crate) fn new(base_url: String, api_key: String) -> Self {
let http = reqwest::Client::builder()
.timeout(TIMEOUT)
.user_agent(USER_AGENT)
.build()
.expect("reqwest client with constant settings");
Self {
http,
base_url: base_url.trim_end_matches('/').to_string(),
api_key,
}
}
pub(crate) fn base_url(&self) -> &str {
&self.base_url
}
pub(crate) async fn get_json<R: DeserializeOwned>(
&self,
path: &str,
query: &[(&str, String)],
) -> Result<R> {
let req = self.http.get(self.url(path)).query(query);
Self::decode(self.sign(req).send().await?).await
}
pub(crate) async fn send_json<B, R>(&self, method: Method, path: &str, body: &B) -> Result<R>
where
B: Serialize + ?Sized,
R: DeserializeOwned,
{
let req = self.http.request(method, self.url(path)).json(body);
Self::decode(self.sign(req).send().await?).await
}
pub(crate) async fn send_empty<B>(&self, method: Method, path: &str, body: &B) -> Result<()>
where
B: Serialize + ?Sized,
{
let req = self.http.request(method, self.url(path)).json(body);
let resp = self.sign(req).send().await?;
Self::check_status(resp).await.map(|_| ())
}
fn url(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
fn sign(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
req.header(API_KEY_HEADER, &self.api_key)
.header(reqwest::header::ACCEPT, "application/json")
}
async fn check_status(resp: reqwest::Response) -> Result<reqwest::Response> {
let status = resp.status();
if status.is_success() {
return Ok(resp);
}
let body = resp.text().await.unwrap_or_default();
Err(Error::Api {
status: status.as_u16(),
body: extract_error_message(&body),
})
}
async fn decode<R: DeserializeOwned>(resp: reqwest::Response) -> Result<R> {
let resp = Self::check_status(resp).await?;
Ok(resp.json::<R>().await?)
}
}