use std::path::Path;
use anyhow::{Context, Result, anyhow};
use serde::{Serialize, de::DeserializeOwned};
pub fn get(url: &str) -> Result<String> {
let response = ureq::get(url)
.call()
.map_err(|e| handle_ureq_error(e, url))?;
response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))
}
pub fn get_json<T: DeserializeOwned>(url: &str) -> Result<T> {
let body = get(url)?;
serde_json::from_str(&body)
.with_context(|| format!("Failed to parse JSON response from: {url}"))
}
pub fn post(url: &str, body: &str) -> Result<String> {
let response = ureq::post(url)
.header("Content-Type", "text/plain")
.send(body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))
}
pub fn post_json<T: Serialize, R: DeserializeOwned>(url: &str, body: &T) -> Result<R> {
let json_body = serde_json::to_string(body)
.with_context(|| format!("Failed to serialize request body for: {url}"))?;
let response = ureq::post(url)
.header("Content-Type", "application/json")
.send(json_body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
let response_body = response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))?;
serde_json::from_str(&response_body)
.with_context(|| format!("Failed to parse JSON response from: {url}"))
}
pub fn download<P: AsRef<Path>>(url: &str, path: P) -> Result<()> {
let path = path.as_ref();
let response = ureq::get(url)
.call()
.map_err(|e| handle_ureq_error(e, url))?;
let mut file = std::fs::File::create(path)
.with_context(|| format!("Failed to create file: {}", path.display()))?;
std::io::copy(&mut response.into_body().as_reader(), &mut file)
.with_context(|| format!("Failed to download {url} to {}", path.display()))?;
Ok(())
}
pub fn put(url: &str, body: &str) -> Result<String> {
let response = ureq::put(url)
.header("Content-Type", "text/plain")
.send(body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))
}
pub fn delete(url: &str) -> Result<String> {
let response = ureq::delete(url)
.call()
.map_err(|e| handle_ureq_error(e, url))?;
response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))
}
pub fn patch(url: &str, body: &str) -> Result<String> {
let response = ureq::patch(url)
.header("Content-Type", "text/plain")
.send(body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))
}
pub fn put_json<T: Serialize, R: DeserializeOwned>(url: &str, body: &T) -> Result<R> {
let json_body = serde_json::to_string(body)
.with_context(|| format!("Failed to serialize request body for: {url}"))?;
let response = ureq::put(url)
.header("Content-Type", "application/json")
.send(json_body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
let response_body = response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))?;
serde_json::from_str(&response_body)
.with_context(|| format!("Failed to parse JSON response from: {url}"))
}
pub fn delete_json<R: DeserializeOwned>(url: &str) -> Result<R> {
let body = delete(url)?;
serde_json::from_str(&body)
.with_context(|| format!("Failed to parse JSON response from: {url}"))
}
pub fn patch_json<T: Serialize, R: DeserializeOwned>(url: &str, body: &T) -> Result<R> {
let json_body = serde_json::to_string(body)
.with_context(|| format!("Failed to serialize request body for: {url}"))?;
let response = ureq::patch(url)
.header("Content-Type", "application/json")
.send(json_body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
let response_body = response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))?;
serde_json::from_str(&response_body)
.with_context(|| format!("Failed to parse JSON response from: {url}"))
}
#[must_use]
pub struct Client {
agent: ureq::Agent,
headers: Vec<(String, String)>,
}
impl Client {
pub fn new() -> Self {
Self {
agent: ureq::Agent::new_with_defaults(),
headers: Vec::new(),
}
}
pub fn timeout(mut self, seconds: u64) -> Self {
let config = ureq::config::Config::builder()
.timeout_global(Some(std::time::Duration::from_secs(seconds)))
.build();
self.agent = ureq::Agent::new_with_config(config);
self
}
pub fn auth(mut self, username: &str, password: &str) -> Self {
let credentials = format!("{username}:{password}");
self.headers
.push(("Authorization".to_string(), format!("Basic {credentials}")));
self
}
pub fn header(mut self, key: &str, value: &str) -> Self {
self.headers.push((key.to_string(), value.to_string()));
self
}
pub fn get(&self, url: &str) -> Result<String> {
let mut request = self.agent.get(url);
for (key, value) in &self.headers {
request = request.header(key, value);
}
let response = request.call().map_err(|e| handle_ureq_error(e, url))?;
response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))
}
pub fn post(&self, url: &str, body: &str) -> Result<String> {
let mut request = self.agent.post(url).header("Content-Type", "text/plain");
for (key, value) in &self.headers {
request = request.header(key, value);
}
let response = request
.send(body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))
}
pub fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T> {
let body = self.get(url)?;
serde_json::from_str(&body)
.with_context(|| format!("Failed to parse JSON response from: {url}"))
}
pub fn post_json<T: Serialize, R: DeserializeOwned>(&self, url: &str, body: &T) -> Result<R> {
let json_body = serde_json::to_string(body)
.with_context(|| format!("Failed to serialize request body for: {url}"))?;
let mut request = self
.agent
.post(url)
.header("Content-Type", "application/json");
for (key, value) in &self.headers {
request = request.header(key, value);
}
let response = request
.send(json_body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
let response_body = response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))?;
serde_json::from_str(&response_body)
.with_context(|| format!("Failed to parse JSON response from: {url}"))
}
pub fn put(&self, url: &str, body: &str) -> Result<String> {
let mut request = self.agent.put(url).header("Content-Type", "text/plain");
for (key, value) in &self.headers {
request = request.header(key, value);
}
let response = request
.send(body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))
}
pub fn delete(&self, url: &str) -> Result<String> {
let mut request = self.agent.delete(url);
for (key, value) in &self.headers {
request = request.header(key, value);
}
let response = request.call().map_err(|e| handle_ureq_error(e, url))?;
response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))
}
pub fn patch(&self, url: &str, body: &str) -> Result<String> {
let mut request = self.agent.patch(url).header("Content-Type", "text/plain");
for (key, value) in &self.headers {
request = request.header(key, value);
}
let response = request
.send(body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))
}
pub fn put_json<T: Serialize, R: DeserializeOwned>(&self, url: &str, body: &T) -> Result<R> {
let json_body = serde_json::to_string(body)
.with_context(|| format!("Failed to serialize request body for: {url}"))?;
let mut request = self
.agent
.put(url)
.header("Content-Type", "application/json");
for (key, value) in &self.headers {
request = request.header(key, value);
}
let response = request
.send(json_body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
let response_body = response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))?;
serde_json::from_str(&response_body)
.with_context(|| format!("Failed to parse JSON response from: {url}"))
}
pub fn delete_json<R: DeserializeOwned>(&self, url: &str) -> Result<R> {
let body = self.delete(url)?;
serde_json::from_str(&body)
.with_context(|| format!("Failed to parse JSON response from: {url}"))
}
pub fn patch_json<T: Serialize, R: DeserializeOwned>(&self, url: &str, body: &T) -> Result<R> {
let json_body = serde_json::to_string(body)
.with_context(|| format!("Failed to serialize request body for: {url}"))?;
let mut request = self
.agent
.patch(url)
.header("Content-Type", "application/json");
for (key, value) in &self.headers {
request = request.header(key, value);
}
let response = request
.send(json_body.as_bytes())
.map_err(|e| handle_ureq_error(e, url))?;
let response_body = response
.into_body()
.read_to_string()
.with_context(|| format!("Failed to read response body from: {url}"))?;
serde_json::from_str(&response_body)
.with_context(|| format!("Failed to parse JSON response from: {url}"))
}
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
fn handle_ureq_error(error: ureq::Error, url: &str) -> anyhow::Error {
match error {
ureq::Error::StatusCode(code) => {
anyhow!("HTTP request to {url} failed with status {code}")
}
ureq::Error::Timeout(kind) => {
anyhow!("HTTP request to {url} timed out: {kind:?}")
}
ureq::Error::HostNotFound => {
anyhow!("Host not found for URL: {url}")
}
ureq::Error::Io(e) => {
anyhow!("IO error during HTTP request to {url}: {e}")
}
_ => {
anyhow!("HTTP request to {url} failed: {error}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_returns_error_for_invalid_url() {
let result = get("http://invalid.local.host.that.does.not.exist.12345/");
assert!(result.is_err());
}
#[test]
fn get_json_returns_error_for_invalid_url() {
let result: Result<serde_json::Value> =
get_json("http://invalid.local.host.that.does.not.exist.12345/");
assert!(result.is_err());
}
#[test]
fn post_returns_error_for_invalid_url() {
let result = post(
"http://invalid.local.host.that.does.not.exist.12345/",
"body",
);
assert!(result.is_err());
}
#[test]
fn post_json_returns_error_for_invalid_url() {
#[derive(serde::Serialize)]
struct TestBody {
key: String,
}
let body = TestBody {
key: "value".to_string(),
};
let result: Result<serde_json::Value> = post_json(
"http://invalid.local.host.that.does.not.exist.12345/",
&body,
);
assert!(result.is_err());
}
}