use http::Method;
use http::header::AUTHORIZATION;
use crate::config::ClientConfig;
use crate::core::operation::Operation;
use crate::core::ratelimit::ResponseMeta;
use crate::core::request::RequestSpec;
use crate::core::response::parse_with;
use crate::error::{Error, Result};
use crate::secret::Secret;
#[derive(Clone, Debug)]
pub struct BlockingClient {
config: ClientConfig,
agent: ureq::Agent,
auth_header: Secret<String>,
}
impl BlockingClient {
pub fn new(api_key: impl Into<Secret<String>>) -> Result<Self> {
Self::from_config(ClientConfig::new(api_key))
}
pub fn from_config(config: ClientConfig) -> Result<Self> {
let builder = ureq::Agent::config_builder()
.http_status_as_error(false)
.user_agent(&config.user_agent)
.timeout_global(Some(config.timeout));
#[cfg(all(feature = "native-tls", not(feature = "rustls")))]
let builder = builder.tls_config(
ureq::tls::TlsConfig::builder()
.provider(ureq::tls::TlsProvider::NativeTls)
.build(),
);
let agent: ureq::Agent = builder.build().into();
Ok(Self::from_config_and_agent(config, agent))
}
pub fn from_config_and_agent(config: ClientConfig, agent: ureq::Agent) -> Self {
let auth_header = Secret::new(format!("Bearer {}", config.api_key.expose()));
BlockingClient {
config,
agent,
auth_header,
}
}
pub fn config(&self) -> &ClientConfig {
&self.config
}
fn apply<B>(
&self,
mut rb: ureq::RequestBuilder<B>,
spec: &RequestSpec,
) -> ureq::RequestBuilder<B> {
for (k, v) in &spec.query {
rb = rb.query(*k, v);
}
rb = rb.header(AUTHORIZATION.as_str(), self.auth_header.expose().as_str());
for (k, v) in &spec.headers {
rb = rb.header(*k, v);
}
rb
}
#[allow(clippy::needless_pass_by_value)]
pub fn send<O: Operation>(&self, op: O) -> Result<O::Output> {
self.send_with_meta(op).map(|(out, _meta)| out)
}
#[allow(clippy::needless_pass_by_value)]
pub fn send_with_meta<O: Operation>(&self, op: O) -> Result<(O::Output, ResponseMeta)> {
let mut spec = RequestSpec::build(&op)?;
let retry = self.config.retry;
if retry.max_retries > 0 {
spec.ensure_idempotency_key();
}
let url = self.config.url_for(&spec.path);
let mut retries_done = 0u32;
loop {
match self.send_once::<O>(&spec, &url) {
Ok(v) => return Ok(v),
Err(e) if retry.should_retry(retries_done, &e) => {
let delay = retry.delay_for(retries_done, &e);
#[cfg(feature = "tracing")]
tracing::warn!(
attempt = retries_done + 1,
delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX),
code = ?e.code(),
"retrying request after transient failure"
);
std::thread::sleep(delay);
retries_done += 1;
}
Err(e) => return Err(e),
}
}
}
fn send_once<O: Operation>(
&self,
spec: &RequestSpec,
url: &str,
) -> Result<(O::Output, ResponseMeta)> {
#[cfg(feature = "tracing")]
let span = tracing::info_span!(
"blooio.request",
method = %spec.method,
path = %spec.path,
status = tracing::field::Empty,
elapsed_ms = tracing::field::Empty,
);
#[cfg(feature = "tracing")]
let _enter = span.enter();
#[cfg(feature = "tracing")]
let start = std::time::Instant::now();
let result = match spec.method {
Method::GET => self.apply(self.agent.get(url), spec).call(),
Method::DELETE => self.apply(self.agent.delete(url), spec).call(),
Method::POST => self.send_with_body(self.agent.post(url), spec),
Method::PUT => self.send_with_body(self.agent.put(url), spec),
Method::PATCH => self.send_with_body(self.agent.patch(url), spec),
ref other => {
return Err(Error::transport(format!("unsupported HTTP method {other}")));
}
};
let mut resp = result.map_err(Error::transport)?;
let status = resp.status().as_u16();
let meta = ResponseMeta::from_headers(status, resp.headers());
let bytes = resp.body_mut().read_to_vec().map_err(Error::transport)?;
let parsed = parse_with(status, &bytes, meta.retry_after).map(|out| (out, meta));
#[cfg(feature = "tracing")]
{
span.record("status", status);
span.record(
"elapsed_ms",
u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
);
match &parsed {
Ok(_) => tracing::debug!("request completed"),
Err(e) => tracing::warn!(code = ?e.code(), "request failed"),
}
}
parsed
}
fn send_with_body(
&self,
rb: ureq::RequestBuilder<ureq::typestate::WithBody>,
spec: &RequestSpec,
) -> std::result::Result<http::Response<ureq::Body>, ureq::Error> {
let rb = self.apply(rb, spec);
match &spec.body {
Some(body) => rb.content_type("application/json").send(body.as_slice()),
None => rb.send_empty(),
}
}
}