use crate::config::ConfigFile;
use anyhow::{Context, Result};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
use serde::de::DeserializeOwned;
use serde::Serialize;
pub struct ApiClient {
http: reqwest::Client,
base_url: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct ApiError {
pub error: String,
}
impl ApiClient {
pub fn new(base_url: &str) -> Result<Self> {
let config = ConfigFile::load()?;
Self::with_config(base_url, &config)
}
pub fn with_config(base_url: &str, config: &ConfigFile) -> Result<Self> {
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
HeaderValue::from_static("cinchdb/0.2.0"),
);
if let Some(auth_value) = config.auth_header_value() {
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&auth_value)
.context("invalid auth header value")?,
);
}
let http = reqwest::Client::builder()
.default_headers(headers)
.timeout(std::time::Duration::from_secs(30))
.build()
.context("failed to create HTTP client")?;
Ok(Self {
http,
base_url: base_url.trim_end_matches('/').to_string(),
})
}
#[allow(dead_code)]
pub fn unauthenticated(base_url: &str) -> Result<Self> {
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
HeaderValue::from_static("cinchdb/0.2.0"),
);
let http = reqwest::Client::builder()
.default_headers(headers)
.timeout(std::time::Duration::from_secs(30))
.build()
.context("failed to create HTTP client")?;
Ok(Self {
http,
base_url: base_url.trim_end_matches('/').to_string(),
})
}
pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
let url = format!("{}{path}", self.base_url);
let resp = self
.http
.get(&url)
.send()
.await
.with_context(|| format!("request to {url} failed"))?;
handle_response(resp).await
}
pub async fn post<B: Serialize, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let url = format!("{}{path}", self.base_url);
let resp = self
.http
.post(&url)
.json(body)
.send()
.await
.with_context(|| format!("request to {url} failed"))?;
handle_response(resp).await
}
pub async fn post_empty(&self, path: &str) -> Result<()> {
let url = format!("{}{path}", self.base_url);
let resp = self
.http
.post(&url)
.send()
.await
.with_context(|| format!("request to {url} failed"))?;
let status = resp.status();
if status.is_success() {
Ok(())
} else {
let body = resp.text().await.unwrap_or_else(|e| format!("<failed to read response body: {e}>"));
if let Ok(api_err) = serde_json::from_str::<ApiError>(&body) {
anyhow::bail!("API error ({}): {}", status, api_err.error);
}
anyhow::bail!("API error ({}): {}", status, body);
}
}
pub async fn delete(&self, path: &str) -> Result<()> {
let url = format!("{}{path}", self.base_url);
let resp = self
.http
.delete(&url)
.send()
.await
.with_context(|| format!("request to {url} failed"))?;
let status = resp.status();
if status.is_success() {
Ok(())
} else {
let body = resp.text().await.unwrap_or_else(|e| format!("<failed to read response body: {e}>"));
if let Ok(api_err) = serde_json::from_str::<ApiError>(&body) {
anyhow::bail!("API error ({}): {}", status, api_err.error);
}
anyhow::bail!("API error ({}): {}", status, body);
}
}
#[allow(dead_code)]
pub fn http(&self) -> &reqwest::Client {
&self.http
}
#[allow(dead_code)]
pub fn base_url(&self) -> &str {
&self.base_url
}
}
async fn handle_response<T: DeserializeOwned>(resp: reqwest::Response) -> Result<T> {
let status = resp.status();
let url = resp.url().to_string();
if status.is_success() {
let body = resp
.json::<T>()
.await
.with_context(|| format!("failed to parse response from {url}"))?;
Ok(body)
} else if status == reqwest::StatusCode::UNAUTHORIZED {
anyhow::bail!("not authenticated. Run `cinch auth login` first.");
} else {
let body = resp.text().await.unwrap_or_else(|e| format!("<failed to read response body: {e}>"));
if let Ok(api_err) = serde_json::from_str::<ApiError>(&body) {
anyhow::bail!("API error ({}): {}", status, api_err.error);
}
anyhow::bail!("API error ({}): {}", status, body);
}
}