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
77fn canned_response(
79 responses: &HashMap<String, Value>,
80 url: &str,
81) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
82 responses
83 .get(url)
84 .cloned()
85 .ok_or_else(|| format!("no static response for {url}").into())
86}
87
88#[derive(Default)]
90pub struct StaticHttpClient {
91 responses: HashMap<String, Value>,
92}
93
94impl StaticHttpClient {
95 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn with_response(mut self, url: impl Into<String>, value: Value) -> Self {
102 self.responses.insert(url.into(), value);
103 self
104 }
105}
106
107impl HttpClient for StaticHttpClient {
108 fn get_json(
109 &self,
110 url: &str,
111 _headers: &[(&str, String)],
112 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
113 canned_response(&self.responses, url)
114 }
115
116 fn post_json(
117 &self,
118 url: &str,
119 _headers: &[(&str, String)],
120 _body: &Value,
121 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
122 canned_response(&self.responses, url)
123 }
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub struct RecordedRequest {
129 pub method: &'static str,
131 pub url: String,
133 pub headers: Vec<(String, String)>,
135 pub body: Option<Value>,
137}
138
139#[derive(Clone, Default)]
141pub struct RecordingHttpClient {
142 responses: HashMap<String, Value>,
143 requests: Arc<Mutex<Vec<RecordedRequest>>>,
144}
145
146impl RecordingHttpClient {
147 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn with_response(mut self, url: impl Into<String>, value: Value) -> Self {
154 self.responses.insert(url.into(), value);
155 self
156 }
157
158 pub fn requests(&self) -> Vec<RecordedRequest> {
160 self.requests
161 .lock()
162 .expect("recording client lock poisoned")
163 .clone()
164 }
165
166 fn record(
167 &self,
168 method: &'static str,
169 url: &str,
170 headers: &[(&str, String)],
171 body: Option<&Value>,
172 ) {
173 self.requests
174 .lock()
175 .expect("recording client lock poisoned")
176 .push(RecordedRequest {
177 method,
178 url: url.to_string(),
179 headers: headers
180 .iter()
181 .map(|(name, value)| ((*name).to_string(), value.clone()))
182 .collect(),
183 body: body.cloned(),
184 });
185 }
186}
187
188impl HttpClient for RecordingHttpClient {
189 fn get_json(
190 &self,
191 url: &str,
192 headers: &[(&str, String)],
193 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
194 self.record("GET", url, headers, None);
195 canned_response(&self.responses, url)
196 }
197
198 fn post_json(
199 &self,
200 url: &str,
201 headers: &[(&str, String)],
202 body: &Value,
203 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
204 self.record("POST", url, headers, Some(body));
205 canned_response(&self.responses, url)
206 }
207}