use ureq::http::{HeaderValue, Uri};
use crate::{Config, Error};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reply {
pub status: u16,
pub retry_after: Option<String>,
pub body: String,
}
pub trait Transport: Send + Sync {
fn post(&self, body: &[u8]) -> Result<Reply, String>;
}
impl<F: Fn(&[u8]) -> Result<Reply, String> + Send + Sync> Transport for F {
fn post(&self, body: &[u8]) -> Result<Reply, String> {
self(body)
}
}
impl Transport for Box<dyn Transport> {
fn post(&self, body: &[u8]) -> Result<Reply, String> {
(**self).post(body)
}
}
pub(crate) struct Http {
agent: ureq::Agent,
url: String,
auth: String,
}
impl Http {
pub(crate) fn new(api_key: &str, cfg: &Config) -> Result<Self, Error> {
let api_key = api_key.trim();
if api_key.is_empty() {
return Err(Error::Auth("the API key is empty".into()));
}
let auth = format!("Bearer {api_key}");
if HeaderValue::from_str(&auth).is_err() {
return Err(Error::Auth("the API key has characters that cannot be sent in an HTTP header".into()));
}
let uri: Uri = cfg.base_url.parse().map_err(|e| Error::InvalidConfig(format!("`Config::base_url` is not a URL: {e}")))?;
if !matches!(uri.scheme_str(), Some("http" | "https")) || uri.host().is_none_or(str::is_empty) {
return Err(Error::InvalidConfig("`Config::base_url` must be an absolute `https://` (or `http://`) URL".into()));
}
if HeaderValue::from_str(&cfg.user_agent).is_err() {
return Err(Error::InvalidConfig("`Config::user_agent` has characters that cannot be sent in an HTTP header".into()));
}
let agent: ureq::Agent = ureq::Agent::config_builder()
.http_status_as_error(false)
.timeout_connect(Some(cfg.connect_timeout))
.timeout_send_body(Some(cfg.timeout))
.timeout_recv_response(Some(cfg.timeout))
.timeout_recv_body(Some(cfg.timeout))
.max_idle_connections(cfg.pool_size)
.max_idle_connections_per_host(cfg.pool_size)
.user_agent(&cfg.user_agent)
.build()
.into();
Ok(Http { agent, url: cfg.base_url.clone(), auth })
}
}
const BODY_LIMIT: u64 = 256 << 20;
impl Transport for Http {
fn post(&self, body: &[u8]) -> Result<Reply, String> {
let mut resp = self
.agent
.post(&self.url)
.header("Authorization", &self.auth)
.header("Content-Type", "application/json")
.send(body)
.map_err(|e| e.to_string())?;
let retry_after = resp.headers().get("retry-after").and_then(|v| v.to_str().ok()).map(str::to_owned);
let status = resp.status().as_u16();
let body = resp.body_mut().with_config().limit(BODY_LIMIT).read_to_string().map_err(|e| e.to_string())?;
Ok(Reply { status, retry_after, body })
}
}