1use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5
6use serde_json::Value;
7
8pub trait HttpClient: Send + Sync {
10 fn get_json(
12 &self,
13 url: &str,
14 headers: &[(&str, String)],
15 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>>;
16
17 fn post_json(
19 &self,
20 url: &str,
21 headers: &[(&str, String)],
22 body: &Value,
23 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>>;
24}
25
26pub struct ReqwestHttpClient {
28 client: reqwest::blocking::Client,
29}
30
31impl ReqwestHttpClient {
32 pub fn new() -> Self {
36 let client = reqwest::blocking::Client::builder()
37 .timeout(std::time::Duration::from_secs(30))
38 .build()
39 .unwrap_or_else(|_| reqwest::blocking::Client::new());
40 Self { client }
41 }
42}
43
44impl Default for ReqwestHttpClient {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl HttpClient for ReqwestHttpClient {
51 fn get_json(
52 &self,
53 url: &str,
54 headers: &[(&str, String)],
55 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
56 let mut req = self.client.get(url);
57 for (name, value) in headers {
58 req = req.header(*name, value);
59 }
60 Ok(req.send()?.error_for_status()?.json()?)
61 }
62
63 fn post_json(
64 &self,
65 url: &str,
66 headers: &[(&str, String)],
67 body: &Value,
68 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
69 let mut req = self.client.post(url).json(body);
70 for (name, value) in headers {
71 req = req.header(*name, value);
72 }
73 Ok(req.send()?.error_for_status()?.json()?)
74 }
75}
76
77#[derive(Default)]
79pub struct StaticHttpClient {
80 responses: HashMap<String, Value>,
81}
82
83impl StaticHttpClient {
84 pub fn new() -> Self {
86 Self::default()
87 }
88
89 pub fn with_response(mut self, url: impl Into<String>, value: Value) -> Self {
91 self.responses.insert(url.into(), value);
92 self
93 }
94}
95
96impl HttpClient for StaticHttpClient {
97 fn get_json(
98 &self,
99 url: &str,
100 _headers: &[(&str, String)],
101 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
102 self.responses
103 .get(url)
104 .cloned()
105 .ok_or_else(|| format!("no static response for {url}").into())
106 }
107
108 fn post_json(
109 &self,
110 url: &str,
111 _headers: &[(&str, String)],
112 _body: &Value,
113 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
114 self.responses
115 .get(url)
116 .cloned()
117 .ok_or_else(|| format!("no static response for {url}").into())
118 }
119}
120
121#[derive(Debug, Clone, PartialEq)]
123pub struct RecordedRequest {
124 pub method: &'static str,
126 pub url: String,
128 pub headers: Vec<(String, String)>,
130 pub body: Option<Value>,
132}
133
134#[derive(Clone, Default)]
136pub struct RecordingHttpClient {
137 responses: HashMap<String, Value>,
138 requests: Arc<Mutex<Vec<RecordedRequest>>>,
139}
140
141impl RecordingHttpClient {
142 pub fn new() -> Self {
144 Self::default()
145 }
146
147 pub fn with_response(mut self, url: impl Into<String>, value: Value) -> Self {
149 self.responses.insert(url.into(), value);
150 self
151 }
152
153 pub fn requests(&self) -> Vec<RecordedRequest> {
155 self.requests
156 .lock()
157 .expect("recording client lock poisoned")
158 .clone()
159 }
160
161 fn record(
162 &self,
163 method: &'static str,
164 url: &str,
165 headers: &[(&str, String)],
166 body: Option<&Value>,
167 ) {
168 self.requests
169 .lock()
170 .expect("recording client lock poisoned")
171 .push(RecordedRequest {
172 method,
173 url: url.to_string(),
174 headers: headers
175 .iter()
176 .map(|(name, value)| ((*name).to_string(), value.clone()))
177 .collect(),
178 body: body.cloned(),
179 });
180 }
181}
182
183impl HttpClient for RecordingHttpClient {
184 fn get_json(
185 &self,
186 url: &str,
187 headers: &[(&str, String)],
188 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
189 self.record("GET", url, headers, None);
190 self.responses
191 .get(url)
192 .cloned()
193 .ok_or_else(|| format!("no static response for {url}").into())
194 }
195
196 fn post_json(
197 &self,
198 url: &str,
199 headers: &[(&str, String)],
200 body: &Value,
201 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
202 self.record("POST", url, headers, Some(body));
203 self.responses
204 .get(url)
205 .cloned()
206 .ok_or_else(|| format!("no static response for {url}").into())
207 }
208}