typesafe_jev/
transport.rs1use ureq::http::{HeaderValue, Uri};
2
3use crate::{Config, Error};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Reply {
8 pub status: u16,
10 pub retry_after: Option<String>,
12 pub body: String,
14}
15
16pub trait Transport: Send + Sync {
23 fn post(&self, body: &[u8]) -> Result<Reply, String>;
25}
26
27impl<F: Fn(&[u8]) -> Result<Reply, String> + Send + Sync> Transport for F {
28 fn post(&self, body: &[u8]) -> Result<Reply, String> {
29 self(body)
30 }
31}
32
33impl Transport for Box<dyn Transport> {
34 fn post(&self, body: &[u8]) -> Result<Reply, String> {
35 (**self).post(body)
36 }
37}
38
39pub(crate) struct Http {
41 agent: ureq::Agent,
42 url: String,
43 auth: String,
44}
45
46impl Http {
47 pub(crate) fn new(api_key: &str, cfg: &Config) -> Result<Self, Error> {
52 let api_key = api_key.trim();
53 if api_key.is_empty() {
54 return Err(Error::Auth("the API key is empty".into()));
55 }
56 let auth = format!("Bearer {api_key}");
57 if HeaderValue::from_str(&auth).is_err() {
58 return Err(Error::Auth("the API key has characters that cannot be sent in an HTTP header".into()));
59 }
60 let uri: Uri = cfg.base_url.parse().map_err(|e| Error::InvalidConfig(format!("`Config::base_url` is not a URL: {e}")))?;
61 if !matches!(uri.scheme_str(), Some("http" | "https")) || uri.host().is_none_or(str::is_empty) {
62 return Err(Error::InvalidConfig("`Config::base_url` must be an absolute `https://` (or `http://`) URL".into()));
63 }
64 if HeaderValue::from_str(&cfg.user_agent).is_err() {
65 return Err(Error::InvalidConfig("`Config::user_agent` has characters that cannot be sent in an HTTP header".into()));
66 }
67 let agent: ureq::Agent = ureq::Agent::config_builder()
68 .http_status_as_error(false)
69 .timeout_connect(Some(cfg.connect_timeout))
70 .timeout_send_body(Some(cfg.timeout))
71 .timeout_recv_response(Some(cfg.timeout))
72 .timeout_recv_body(Some(cfg.timeout))
73 .max_idle_connections(cfg.pool_size)
74 .max_idle_connections_per_host(cfg.pool_size)
75 .user_agent(&cfg.user_agent)
76 .build()
77 .into();
78 Ok(Http { agent, url: cfg.base_url.clone(), auth })
79 }
80}
81
82const BODY_LIMIT: u64 = 256 << 20;
84
85impl Transport for Http {
86 fn post(&self, body: &[u8]) -> Result<Reply, String> {
87 let mut resp = self
88 .agent
89 .post(&self.url)
90 .header("Authorization", &self.auth)
91 .header("Content-Type", "application/json")
92 .send(body)
93 .map_err(|e| e.to_string())?;
94 let retry_after = resp.headers().get("retry-after").and_then(|v| v.to_str().ok()).map(str::to_owned);
95 let status = resp.status().as_u16();
96 let body = resp.body_mut().with_config().limit(BODY_LIMIT).read_to_string().map_err(|e| e.to_string())?;
97 Ok(Reply { status, retry_after, body })
98 }
99}