use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use serde_json::Value;
pub trait HttpClient: Send + Sync {
fn get_json(
&self,
url: &str,
headers: &[(&str, String)],
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>>;
fn post_json(
&self,
url: &str,
headers: &[(&str, String)],
body: &Value,
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>>;
}
pub struct ReqwestHttpClient {
client: reqwest::blocking::Client,
}
impl ReqwestHttpClient {
pub fn new() -> Self {
Self {
client: reqwest::blocking::Client::new(),
}
}
}
impl Default for ReqwestHttpClient {
fn default() -> Self {
Self::new()
}
}
impl HttpClient for ReqwestHttpClient {
fn get_json(
&self,
url: &str,
headers: &[(&str, String)],
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
let mut req = self.client.get(url);
for (name, value) in headers {
req = req.header(*name, value);
}
Ok(req.send()?.error_for_status()?.json()?)
}
fn post_json(
&self,
url: &str,
headers: &[(&str, String)],
body: &Value,
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
let mut req = self.client.post(url).json(body);
for (name, value) in headers {
req = req.header(*name, value);
}
Ok(req.send()?.error_for_status()?.json()?)
}
}
#[derive(Default)]
pub struct StaticHttpClient {
responses: HashMap<String, Value>,
}
impl StaticHttpClient {
pub fn new() -> Self {
Self::default()
}
pub fn with_response(mut self, url: impl Into<String>, value: Value) -> Self {
self.responses.insert(url.into(), value);
self
}
}
impl HttpClient for StaticHttpClient {
fn get_json(
&self,
url: &str,
_headers: &[(&str, String)],
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
self.responses
.get(url)
.cloned()
.ok_or_else(|| format!("no static response for {url}").into())
}
fn post_json(
&self,
url: &str,
_headers: &[(&str, String)],
_body: &Value,
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
self.responses
.get(url)
.cloned()
.ok_or_else(|| format!("no static response for {url}").into())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RecordedRequest {
pub method: &'static str,
pub url: String,
pub headers: Vec<(String, String)>,
pub body: Option<Value>,
}
#[derive(Clone, Default)]
pub struct RecordingHttpClient {
responses: HashMap<String, Value>,
requests: Arc<Mutex<Vec<RecordedRequest>>>,
}
impl RecordingHttpClient {
pub fn new() -> Self {
Self::default()
}
pub fn with_response(mut self, url: impl Into<String>, value: Value) -> Self {
self.responses.insert(url.into(), value);
self
}
pub fn requests(&self) -> Vec<RecordedRequest> {
self.requests
.lock()
.expect("recording client lock poisoned")
.clone()
}
fn record(
&self,
method: &'static str,
url: &str,
headers: &[(&str, String)],
body: Option<&Value>,
) {
self.requests
.lock()
.expect("recording client lock poisoned")
.push(RecordedRequest {
method,
url: url.to_string(),
headers: headers
.iter()
.map(|(name, value)| ((*name).to_string(), value.clone()))
.collect(),
body: body.cloned(),
});
}
}
impl HttpClient for RecordingHttpClient {
fn get_json(
&self,
url: &str,
headers: &[(&str, String)],
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
self.record("GET", url, headers, None);
self.responses
.get(url)
.cloned()
.ok_or_else(|| format!("no static response for {url}").into())
}
fn post_json(
&self,
url: &str,
headers: &[(&str, String)],
body: &Value,
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
self.record("POST", url, headers, Some(body));
self.responses
.get(url)
.cloned()
.ok_or_else(|| format!("no static response for {url}").into())
}
}