use anyhow::{Context, bail};
use serde::de::DeserializeOwned;
use super::wire::format_request_as_curl;
#[derive(Debug)]
pub struct MantaClient {
client: reqwest::Client,
base_url: String,
site_name: String,
}
impl MantaClient {
pub fn new(server_url: &str, site_name: &str) -> anyhow::Result<Self> {
Self::new_with_timeout(server_url, site_name, None)
}
pub fn new_with_timeout(
server_url: &str,
site_name: &str,
timeout_secs: Option<u64>,
) -> anyhow::Result<Self> {
let normalized = if server_url.starts_with("http://")
|| server_url.starts_with("https://")
{
server_url.to_owned()
} else {
format!("http://{server_url}")
};
let mut builder = reqwest::Client::builder();
if let Some(secs) = timeout_secs {
builder = builder.timeout(std::time::Duration::from_secs(secs));
}
let client = builder.build().context("Failed to build HTTP client")?;
Ok(Self {
client,
base_url: format!("{}/api/v1", normalized.trim_end_matches('/')),
site_name: site_name.to_owned(),
})
}
pub(super) fn log_request_as_curl(builder: &reqwest::RequestBuilder) {
if !tracing::enabled!(tracing::Level::DEBUG) {
return;
}
let Some(cloned) = builder.try_clone() else {
return;
};
let Ok(req) = cloned.build() else {
return;
};
tracing::debug!(
"curl equivalent (secrets replaced with <REDACTED>):\n{}",
format_request_as_curl(&req)
);
}
pub(super) async fn parse_json<T: DeserializeOwned>(
resp: reqwest::Response,
) -> anyhow::Result<T> {
if resp.status().is_success() {
resp
.json::<T>()
.await
.context("Failed to parse response JSON")
} else {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
bail!("Server returned {status}: {body}")
}
}
pub(super) async fn parse_no_content(
resp: reqwest::Response,
) -> anyhow::Result<()> {
if resp.status().is_success() {
Ok(())
} else {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
bail!("Server returned {status}: {body}")
}
}
pub(super) async fn get_json<T: DeserializeOwned>(
&self,
token: &str,
path: &str,
query: &[(&str, String)],
) -> anyhow::Result<T> {
let url = format!("{}{}", self.base_url, path);
let builder = self
.client
.get(&url)
.bearer_auth(token)
.header("X-Manta-Site", &self.site_name)
.query(query);
Self::log_request_as_curl(&builder);
let resp = builder.send().await.context("HTTP GET failed")?;
Self::parse_json(resp).await
}
pub(super) async fn post_json<T: DeserializeOwned>(
&self,
token: &str,
path: &str,
body: &impl serde::Serialize,
) -> anyhow::Result<T> {
let url = format!("{}{}", self.base_url, path);
let builder = self
.client
.post(&url)
.bearer_auth(token)
.header("X-Manta-Site", &self.site_name)
.json(body);
Self::log_request_as_curl(&builder);
let resp = builder.send().await.context("HTTP POST failed")?;
Self::parse_json(resp).await
}
pub(super) async fn put_no_content(
&self,
token: &str,
path: &str,
body: &impl serde::Serialize,
) -> anyhow::Result<()> {
let url = format!("{}{}", self.base_url, path);
let builder = self
.client
.put(&url)
.bearer_auth(token)
.header("X-Manta-Site", &self.site_name)
.json(body);
Self::log_request_as_curl(&builder);
let resp = builder.send().await.context("HTTP PUT failed")?;
Self::parse_no_content(resp).await
}
pub(super) async fn delete_no_content(
&self,
token: &str,
path: &str,
) -> anyhow::Result<()> {
let url = format!("{}{}", self.base_url, path);
let builder = self
.client
.delete(&url)
.bearer_auth(token)
.header("X-Manta-Site", &self.site_name);
Self::log_request_as_curl(&builder);
let resp = builder.send().await.context("HTTP DELETE failed")?;
Self::parse_no_content(resp).await
}
pub(super) async fn delete_no_content_with_query(
&self,
token: &str,
path: &str,
query: &[(&str, String)],
) -> anyhow::Result<()> {
let url = format!("{}{}", self.base_url, path);
let builder = self
.client
.delete(&url)
.bearer_auth(token)
.header("X-Manta-Site", &self.site_name)
.query(query);
Self::log_request_as_curl(&builder);
let resp = builder.send().await.context("HTTP DELETE failed")?;
Self::parse_no_content(resp).await
}
pub(super) async fn delete_no_content_with_body(
&self,
token: &str,
path: &str,
body: &impl serde::Serialize,
) -> anyhow::Result<()> {
let url = format!("{}{}", self.base_url, path);
let builder = self
.client
.delete(&url)
.bearer_auth(token)
.header("X-Manta-Site", &self.site_name)
.json(body);
Self::log_request_as_curl(&builder);
let resp = builder.send().await.context("HTTP DELETE failed")?;
Self::parse_no_content(resp).await
}
pub(super) async fn delete_json_with_body<T: DeserializeOwned>(
&self,
token: &str,
path: &str,
body: &impl serde::Serialize,
) -> anyhow::Result<T> {
let url = format!("{}{}", self.base_url, path);
let builder = self
.client
.delete(&url)
.bearer_auth(token)
.header("X-Manta-Site", &self.site_name)
.json(body);
Self::log_request_as_curl(&builder);
let resp = builder.send().await.context("HTTP DELETE failed")?;
Self::parse_json(resp).await
}
pub(super) async fn delete_json_with_query<T: DeserializeOwned>(
&self,
token: &str,
path: &str,
query: &[(&str, String)],
) -> anyhow::Result<T> {
let url = format!("{}{}", self.base_url, path);
let builder = self
.client
.delete(&url)
.bearer_auth(token)
.header("X-Manta-Site", &self.site_name)
.query(query);
Self::log_request_as_curl(&builder);
let resp = builder.send().await.context("HTTP DELETE failed")?;
Self::parse_json(resp).await
}
pub(super) fn http_client(&self) -> &reqwest::Client {
&self.client
}
pub(super) fn base_url(&self) -> &str {
&self.base_url
}
pub(super) fn site_name(&self) -> &str {
&self.site_name
}
}