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) fn unreachable_server_msg(&self) -> String {
let server_url = self.base_url.trim_end_matches("/api/v1");
format!(
"cannot reach manta server at {server_url}. Is the server \
running, and is `manta_server_url` in your config correct?"
)
}
fn map_send_error(&self, e: reqwest::Error, method: &str) -> anyhow::Error {
if e.is_connect() || e.is_timeout() {
anyhow::Error::new(e).context(self.unreachable_server_msg())
} else {
anyhow::Error::new(e).context(format!("HTTP {method} failed"))
}
}
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
.map_err(|e| self.map_send_error(e, "GET"))?;
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
.map_err(|e| self.map_send_error(e, "POST"))?;
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
.map_err(|e| self.map_send_error(e, "PUT"))?;
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
.map_err(|e| self.map_send_error(e, "DELETE"))?;
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
.map_err(|e| self.map_send_error(e, "DELETE"))?;
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
.map_err(|e| self.map_send_error(e, "DELETE"))?;
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
.map_err(|e| self.map_send_error(e, "DELETE"))?;
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
.map_err(|e| self.map_send_error(e, "DELETE"))?;
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
}
}