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 {
34 Self {
35 client: reqwest::blocking::Client::new(),
36 }
37 }
38}
39
40impl Default for ReqwestHttpClient {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl HttpClient for ReqwestHttpClient {
47 fn get_json(
48 &self,
49 url: &str,
50 headers: &[(&str, String)],
51 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
52 let mut req = self.client.get(url);
53 for (name, value) in headers {
54 req = req.header(*name, value);
55 }
56 Ok(req.send()?.error_for_status()?.json()?)
57 }
58
59 fn post_json(
60 &self,
61 url: &str,
62 headers: &[(&str, String)],
63 body: &Value,
64 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
65 let mut req = self.client.post(url).json(body);
66 for (name, value) in headers {
67 req = req.header(*name, value);
68 }
69 Ok(req.send()?.error_for_status()?.json()?)
70 }
71}
72
73#[derive(Default)]
75pub struct StaticHttpClient {
76 responses: HashMap<String, Value>,
77}
78
79impl StaticHttpClient {
80 pub fn new() -> Self {
82 Self::default()
83 }
84
85 pub fn with_response(mut self, url: impl Into<String>, value: Value) -> Self {
87 self.responses.insert(url.into(), value);
88 self
89 }
90}
91
92impl HttpClient for StaticHttpClient {
93 fn get_json(
94 &self,
95 url: &str,
96 _headers: &[(&str, String)],
97 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
98 self.responses
99 .get(url)
100 .cloned()
101 .ok_or_else(|| format!("no static response for {url}").into())
102 }
103
104 fn post_json(
105 &self,
106 url: &str,
107 _headers: &[(&str, String)],
108 _body: &Value,
109 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
110 self.responses
111 .get(url)
112 .cloned()
113 .ok_or_else(|| format!("no static response for {url}").into())
114 }
115}
116
117#[derive(Debug, Clone, PartialEq)]
119pub struct RecordedRequest {
120 pub method: &'static str,
122 pub url: String,
124 pub headers: Vec<(String, String)>,
126 pub body: Option<Value>,
128}
129
130#[derive(Clone, Default)]
132pub struct RecordingHttpClient {
133 responses: HashMap<String, Value>,
134 requests: Arc<Mutex<Vec<RecordedRequest>>>,
135}
136
137impl RecordingHttpClient {
138 pub fn new() -> Self {
140 Self::default()
141 }
142
143 pub fn with_response(mut self, url: impl Into<String>, value: Value) -> Self {
145 self.responses.insert(url.into(), value);
146 self
147 }
148
149 pub fn requests(&self) -> Vec<RecordedRequest> {
151 self.requests
152 .lock()
153 .expect("recording client lock poisoned")
154 .clone()
155 }
156
157 fn record(
158 &self,
159 method: &'static str,
160 url: &str,
161 headers: &[(&str, String)],
162 body: Option<&Value>,
163 ) {
164 self.requests
165 .lock()
166 .expect("recording client lock poisoned")
167 .push(RecordedRequest {
168 method,
169 url: url.to_string(),
170 headers: headers
171 .iter()
172 .map(|(name, value)| ((*name).to_string(), value.clone()))
173 .collect(),
174 body: body.cloned(),
175 });
176 }
177}
178
179impl HttpClient for RecordingHttpClient {
180 fn get_json(
181 &self,
182 url: &str,
183 headers: &[(&str, String)],
184 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
185 self.record("GET", url, headers, None);
186 self.responses
187 .get(url)
188 .cloned()
189 .ok_or_else(|| format!("no static response for {url}").into())
190 }
191
192 fn post_json(
193 &self,
194 url: &str,
195 headers: &[(&str, String)],
196 body: &Value,
197 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
198 self.record("POST", url, headers, Some(body));
199 self.responses
200 .get(url)
201 .cloned()
202 .ok_or_else(|| format!("no static response for {url}").into())
203 }
204}