use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use serde::de::DeserializeOwned;
use serde::Serialize;
use url::Url;
use crate::error::{Envelope, Error, Redacted, Result};
#[derive(Clone)]
pub struct Client {
base: Url,
http: reqwest::Client,
}
#[allow(clippy::missing_fields_in_debug)]
impl std::fmt::Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("base", &self.base.as_str())
.field("api_key", &Redacted)
.finish()
}
}
#[derive(Debug, Default)]
pub struct ClientBuilder {
base: Option<String>,
api_key: Option<String>,
timeout: Option<std::time::Duration>,
}
impl ClientBuilder {
#[must_use]
pub fn base(mut self, base: impl Into<String>) -> Self {
self.base = Some(base.into());
self
}
#[must_use]
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
#[must_use]
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn build(self) -> Result<Client> {
let base = Url::parse(self.base.as_deref().unwrap_or("http://127.0.0.1:3000"))?;
let mut headers = HeaderMap::new();
if let Some(key) = self.api_key {
let mut value = HeaderValue::from_str(&format!("Bearer {key}"))?;
value.set_sensitive(true);
headers.insert(AUTHORIZATION, value);
}
let mut http = reqwest::Client::builder().default_headers(headers);
if let Some(timeout) = self.timeout {
http = http.timeout(timeout);
}
let http = http.build().map_err(|source| Error::Transport {
base: base.to_string(),
source,
})?;
Ok(Client { base, http })
}
}
impl Client {
pub fn new(base: impl Into<String>) -> Result<Self> {
ClientBuilder::default().base(base).build()
}
pub fn local() -> Result<Self> {
ClientBuilder::default().build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub(crate) async fn send_json<T, B, Q>(
&self,
name: &'static str,
method: reqwest::Method,
path: &str,
body: Option<&B>,
query: Option<&Q>,
) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
Q: Serialize + ?Sized,
{
let mut request = self.http.request(method, self.route(path)?);
if let Some(body) = body {
request = request.json(body);
}
if let Some(query) = query {
request = request.query(query);
}
let response = self.send(request).await?;
Self::read(name, response).await
}
pub(crate) async fn send_raw<T, Q>(
&self,
name: &'static str,
method: reqwest::Method,
path: &str,
query: Option<&Q>,
) -> Result<T>
where
T: DeserializeOwned,
Q: Serialize + ?Sized,
{
let mut request = self.http.request(method, self.route(path)?);
if let Some(query) = query {
request = request.query(query);
}
let response = self.send(request).await?;
response.json().await.map_err(|source| Error::Shape {
route: name,
source,
})
}
pub(crate) async fn send_empty<T, Q>(
&self,
name: &'static str,
method: reqwest::Method,
path: &str,
_body: Option<&()>,
query: Option<&Q>,
) -> Result<T>
where
T: DeserializeOwned,
Q: Serialize + ?Sized,
{
self.send_json::<T, (), Q>(name, method, path, None, query)
.await
}
fn route(&self, path: &str) -> Result<Url> {
Ok(self.base.join(path)?)
}
async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
request.send().await.map_err(|source| Error::Transport {
base: self.base.to_string(),
source,
})
}
async fn read<T: DeserializeOwned>(
name: &'static str,
response: reqwest::Response,
) -> Result<T> {
response
.json::<Envelope<T>>()
.await
.map_err(|source| Error::Shape {
route: name,
source,
})?
.into_data()
}
}