use async_trait::async_trait;
use http::header::{self, USER_AGENT};
use reqwest::{IntoUrl, RequestBuilder};
use serde::de::DeserializeOwned;
use serde::Serialize;
use url::Url;
use crate::constants::{BASE_URL_ENV, DEFAULT_BASE_URL};
use crate::{Error, Response, Result};
#[derive(Clone)]
pub struct Client {
http_client: reqwest::Client,
config: ClientConfig,
}
#[async_trait]
pub trait RequestRunner: Sync + Send {
fn prepare_request(
&self,
method: http::Method,
path: Url,
) -> Result<RequestBuilder>;
fn make_url(&self, path: &str) -> Result<Url>;
fn prepare_request_with_body<B>(
&self,
method: http::Method,
path: Url,
body: B,
) -> Result<RequestBuilder>
where
B: Serialize + std::fmt::Debug,
{
Ok(self.prepare_request(method, path)?.json(&body))
}
async fn process_response<T>(
&self,
response: reqwest::Response,
) -> Result<Response<T>>
where
T: DeserializeOwned + Send,
{
Response::from_raw_response(response).await
}
async fn run<T>(
&self,
method: http::Method,
path: Url,
) -> Result<Response<T>>
where
T: DeserializeOwned + Send,
{
let request = self.prepare_request(method, path)?;
let resp = request.send().await?;
self.process_response(resp).await
}
async fn run_with_body<T, B>(
&self,
method: http::Method,
path: Url,
body: B,
) -> Result<Response<T>>
where
T: DeserializeOwned + Send,
B: Serialize + std::fmt::Debug + Send,
{
let request = self.prepare_request_with_body(method, path, body)?;
let resp = request.send().await?;
self.process_response(resp).await
}
}
#[must_use]
#[derive(Default, Clone)]
pub struct ClientBuilder {
config: Config,
}
impl ClientBuilder {
pub fn new() -> Self {
Self {
config: Config::default(),
}
}
pub fn base_url<T: IntoUrl>(mut self, base_url: T) -> Result<Self> {
let mut base_url = base_url.into_url()?;
base_url.set_query(None);
self.config.base_url = Some(base_url);
Ok(self)
}
pub fn secret_token(mut self, secret_token: String) -> Self {
self.config.secret_token = Some(secret_token);
self
}
#[cfg(feature = "admin")]
pub fn on_behalf_of(mut self, project_id: String) -> Self {
self.config.on_behalf_of = Some(project_id);
self
}
pub fn build(self) -> Result<Client> {
let user_agent = format!(
"rust-{}-{}-{}",
env!("CARGO_PKG_VERSION"),
std::env::consts::OS,
std::env::consts::ARCH,
);
let mut headers = header::HeaderMap::new();
headers.insert(
USER_AGENT,
header::HeaderValue::from_str(&user_agent).expect("User-Agent"),
);
if let Some(prj) = &self.config.on_behalf_of {
headers.insert(
"X-On-Behalf-Of",
header::HeaderValue::from_str(prj).expect("X-On-Behalf-Of"),
);
}
let http_client = match self.config.reqwest_client {
| Some(c) => c,
| None => {
reqwest::ClientBuilder::new()
.redirect(reqwest::redirect::Policy::none())
.default_headers(headers)
.build()?
}
};
let base_url = match self.config.base_url {
| Some(c) => c,
| None => {
std::env::var(BASE_URL_ENV)
.ok()
.map(|base_url| Url::parse(&base_url))
.unwrap_or(Ok(DEFAULT_BASE_URL.clone()))
.expect("Config::default()")
}
};
Ok(Client {
http_client,
config: ClientConfig {
base_url,
secret_token: self
.config
.secret_token
.ok_or(Error::SecretTokenRequired)?,
},
})
}
pub fn reqwest_client(mut self, c: reqwest::Client) -> Self {
self.config.reqwest_client = Some(c);
self
}
}
impl Client {
pub fn new() -> Self {
Self::builder().build().expect("Client::new()")
}
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
#[derive(Default, Clone)]
struct Config {
base_url: Option<Url>,
secret_token: Option<String>,
on_behalf_of: Option<String>,
reqwest_client: Option<reqwest::Client>,
}
#[derive(Clone)]
pub(crate) struct ClientConfig {
pub base_url: Url,
secret_token: String,
}
const _: () = {
fn assert_send<T: Send + Sync>() {}
let _ = assert_send::<Client>;
};
#[async_trait]
impl RequestRunner for Client {
fn make_url(&self, path: &str) -> Result<Url> {
Ok(self.config.base_url.join(path)?)
}
fn prepare_request(
&self,
method: http::Method,
url: Url,
) -> Result<RequestBuilder> {
let request = self
.http_client
.request(method, url)
.bearer_auth(&self.config.secret_token);
Ok(request)
}
}
#[cfg(test)]
mod tests {}