use std::sync::OnceLock;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpResponse {
pub status: u16,
pub body: String,
}
impl HttpResponse {
pub fn ok(&self) -> bool {
(200..300).contains(&self.status)
}
}
#[derive(Debug, thiserror::Error)]
#[error("request to {url} failed: {reason}")]
pub struct HttpError {
pub url: String,
pub reason: String,
}
pub const OPYS_USER_AGENT: &str = "opys/1.0";
const BODY_LIMIT: u64 = 32 * 1024 * 1024;
fn agent() -> &'static ureq::Agent {
static AGENT: OnceLock<ureq::Agent> = OnceLock::new();
AGENT.get_or_init(|| {
ureq::Agent::config_builder()
.user_agent(OPYS_USER_AGENT)
.http_status_as_error(false)
.build()
.into()
})
}
fn call(
url: &str,
headers: &[(&str, &str)],
) -> Result<ureq::http::Response<ureq::Body>, HttpError> {
let mut req = agent().get(url);
for (name, value) in headers {
req = req.header(*name, *value);
}
req.call().map_err(|e| HttpError {
url: url.to_owned(),
reason: e.to_string(),
})
}
pub fn get(url: &str, headers: &[(&str, &str)]) -> Result<HttpResponse, HttpError> {
let mut res = call(url, headers)?;
let status = res.status().as_u16();
let body = res
.body_mut()
.with_config()
.limit(BODY_LIMIT)
.read_to_string()
.map_err(|e| HttpError {
url: url.to_owned(),
reason: e.to_string(),
})?;
Ok(HttpResponse { status, body })
}
pub fn post_json(
url: &str,
headers: &[(&str, &str)],
body: &serde_json::Value,
) -> Result<HttpResponse, HttpError> {
let fail = |e: &dyn std::fmt::Display| HttpError {
url: url.to_owned(),
reason: e.to_string(),
};
let mut req = agent().post(url).header("content-type", "application/json");
for (name, value) in headers {
req = req.header(*name, *value);
}
let mut res = req.send(body.to_string()).map_err(|e| fail(&e))?;
let status = res.status().as_u16();
let body = res
.body_mut()
.with_config()
.limit(BODY_LIMIT)
.read_to_string()
.map_err(|e| fail(&e))?;
Ok(HttpResponse { status, body })
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpBytes {
pub status: u16,
pub body: Vec<u8>,
}
impl HttpBytes {
pub fn ok(&self) -> bool {
(200..300).contains(&self.status)
}
}
const BYTES_LIMIT: u64 = 2 * 1024 * 1024 * 1024;
pub fn get_bytes(url: &str, headers: &[(&str, &str)]) -> Result<HttpBytes, HttpError> {
let mut res = call(url, headers)?;
let status = res.status().as_u16();
let body = res
.body_mut()
.with_config()
.limit(BYTES_LIMIT)
.read_to_vec()
.map_err(|e| HttpError {
url: url.to_owned(),
reason: e.to_string(),
})?;
Ok(HttpBytes { status, body })
}
#[derive(Debug, thiserror::Error)]
pub enum JsonGetError {
#[error(transparent)]
Transport(#[from] HttpError),
#[error("{url} returned HTTP {status}")]
Status { url: String, status: u16 },
#[error(transparent)]
Decode(#[from] serde_json::Error),
}
pub fn get_json<T: serde::de::DeserializeOwned>(
url: &str,
headers: &[(&str, &str)],
) -> Result<T, JsonGetError> {
let response = get(url, headers)?;
if !response.ok() {
return Err(JsonGetError::Status {
url: url.to_owned(),
status: response.status,
});
}
Ok(serde_json::from_str(&response.body)?)
}