use anyhow::{anyhow, Context, Result};
use reqwest::{header, Client as HttpClient, Response};
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::config::{Config, USER_AGENT};
pub struct Client {
http: HttpClient,
base: String,
key: Option<String>,
}
impl Client {
pub fn new(cfg: &Config) -> Result<Self> {
let http = HttpClient::builder()
.user_agent(USER_AGENT)
.build()
.context("building http client")?;
Ok(Self {
http,
base: cfg.base.clone(),
key: cfg.key.clone(),
})
}
fn url(&self, path: &str) -> String {
if path.starts_with("http://") || path.starts_with("https://") {
path.to_string()
} else if path.starts_with('/') {
format!("{}{}", self.base, path)
} else {
format!("{}/{}", self.base, path)
}
}
fn auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
match &self.key {
Some(k) => req.header(header::AUTHORIZATION, format!("Bearer {k}")),
None => req,
}
}
pub async fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
let resp = self.auth(self.http.get(self.url(path))).send().await?;
decode_json(resp).await
}
pub async fn post_json<T: DeserializeOwned>(&self, path: &str, body: &Value) -> Result<T> {
let resp = self
.auth(self.http.post(self.url(path)).json(body))
.send()
.await?;
decode_json(resp).await
}
pub async fn post_stream(&self, path: &str, body: &Value) -> Result<Response> {
let resp = self
.auth(self.http.post(self.url(path)).json(body))
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(anyhow!("HTTP {status}: {}", truncate(&text, 400)));
}
Ok(resp)
}
}
async fn decode_json<T: DeserializeOwned>(resp: Response) -> Result<T> {
let status = resp.status();
let bytes = resp.bytes().await.context("reading body")?;
if !status.is_success() {
let text = String::from_utf8_lossy(&bytes);
return Err(anyhow!("HTTP {status}: {}", truncate(&text, 400)));
}
serde_json::from_slice::<T>(&bytes).with_context(|| {
let text = String::from_utf8_lossy(&bytes);
format!("decoding JSON: body was {}", truncate(&text, 200))
})
}
fn truncate(s: &str, max: usize) -> String {
if s.len() <= max {
s.to_string()
} else {
format!("{}…", &s[..max])
}
}