1use bytes::Bytes;
2use freedom_config::Config;
3use reqwest::{RequestBuilder, StatusCode};
4use url::Url;
5
6use crate::api::{Api, Inner, Value};
7
8#[derive(Clone, Debug)]
13pub struct Client {
14 pub(crate) config: Config,
15 pub(crate) client: reqwest::Client,
16 universal_headers: Vec<(String, String)>,
17}
18
19impl PartialEq for Client {
20 fn eq(&self, other: &Self) -> bool {
21 self.config == other.config
22 }
23}
24
25impl Client {
26 pub fn from_config(config: Config) -> Self {
43 Self {
44 config,
45 client: reqwest::Client::new(),
46 universal_headers: Vec::new(),
47 }
48 }
49
50 pub fn from_env() -> Result<Self, freedom_config::Error> {
58 let config = Config::from_env()?;
59 Ok(Self::from_config(config))
60 }
61
62 pub fn with_universal_header(
64 mut self,
65 key: impl Into<String>,
66 value: impl Into<String>,
67 ) -> Self {
68 self.universal_headers.push((key.into(), value.into()));
69 self
70 }
71
72 fn append_headers(&self, mut req: RequestBuilder) -> RequestBuilder {
73 for (header, value) in self.universal_headers.iter() {
74 req = req.header(header, value);
75 }
76 req
77 }
78}
79
80impl Api for Client {
81 type Container<T: Value> = Inner<T>;
82
83 fn config(&self) -> &Config {
84 &self.config
85 }
86
87 fn config_mut(&mut self) -> &mut Config {
88 &mut self.config
89 }
90
91 async fn get(&self, url: Url) -> Result<(Bytes, StatusCode), crate::error::Error> {
92 tracing::trace!("GET to {}", url);
93
94 let req = self.append_headers(self.client.get(url.clone()));
95
96 let resp = req
97 .basic_auth(self.config.key(), Some(&self.config.expose_secret()))
98 .send()
99 .await?;
100
101 let status = resp.status();
102 let body = resp
103 .bytes()
104 .await
105 .inspect_err(|error| tracing::warn!(%url, %error, %status, "Failed to get response body"))
106 .inspect(|body| tracing::info!(%url, body = %String::from_utf8_lossy(body), %status, "Received response body"))?;
107
108 Ok((body, status))
109 }
110
111 async fn delete(&self, url: Url) -> Result<(Bytes, StatusCode), crate::error::Error> {
112 tracing::trace!("DELETE to {}", url);
113
114 let req = self.append_headers(self.client.delete(url.clone()));
115
116 let resp = req
117 .basic_auth(self.config.key(), Some(self.config.expose_secret()))
118 .send()
119 .await?;
120
121 let status = resp.status();
122 let body = resp
123 .bytes()
124 .await
125 .inspect_err(|error| tracing::warn!(%error, %url, "Failed to DELETE"))
126 .inspect(|ok| tracing::warn!(?ok, %url, "Received response"))?;
127
128 Ok((body, status))
129 }
130
131 async fn post<S>(&self, url: Url, msg: S) -> Result<(Bytes, StatusCode), crate::error::Error>
132 where
133 S: serde::Serialize + Sync + Send,
134 {
135 tracing::trace!("POST to {}", url);
136
137 let req = self.append_headers(self.client.post(url.clone()));
138
139 let resp = req
140 .basic_auth(self.config.key(), Some(self.config.expose_secret()))
141 .json(&msg)
142 .send()
143 .await?;
144
145 let status = resp.status();
146 let body = resp
147 .bytes()
148 .await
149 .inspect_err(|error| tracing::warn!(%error, %url, "Failed to POST"))
150 .inspect(|ok| tracing::warn!(?ok, %url, "Received response"))?;
151
152 Ok((body, status))
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use freedom_config::Test;
159 use httpmock::{
160 Method::{GET, POST},
161 MockServer,
162 };
163
164 use crate::Container;
165
166 use super::*;
167
168 fn default_client() -> Client {
169 let config = Config::builder()
170 .environment(Test)
171 .key("foo")
172 .secret("bar")
173 .build()
174 .unwrap();
175
176 Client::from_config(config)
177 }
178
179 #[test]
180 fn clients_are_eq_based_on_config() {
181 let config = Config::builder()
182 .environment(Test)
183 .key("foo")
184 .secret("bar")
185 .build()
186 .unwrap();
187
188 let client_1 = Client::from_config(config.clone());
189 let client_2 = Client::from_config(config);
190 assert_eq!(client_1, client_2);
191 }
192
193 #[test]
194 fn wrap_and_unwrap_inner() {
195 let val = String::from("foobar");
196 let inner = Inner::new(val.clone());
197 assert_eq!(*inner, val);
198 let unwrapped = inner.into_inner();
199 assert_eq!(val, unwrapped);
200 }
201
202 #[test]
203 fn load_from_env() {
204 unsafe {
205 std::env::set_var("ATLAS_ENV", "TEST");
206 std::env::set_var("ATLAS_KEY", "foo");
207 std::env::set_var("ATLAS_SECRET", "bar");
208 };
209 let client = Client::from_env().unwrap();
210 assert_eq!(client.config().key(), "foo");
211 assert_eq!(client.config().expose_secret(), "bar");
212 }
213
214 #[tokio::test]
215 async fn get_ok_response() {
216 const RESPONSE: &str = "it's working";
217 let client = default_client();
218 let server = MockServer::start();
219 let addr = server.address();
220 let mock = server.mock(|when, then| {
221 when.method(GET).path("/testing");
222 then.body(RESPONSE.as_bytes());
223 });
224 let url = Url::parse(&format!("http://{}/testing", addr)).unwrap();
225 let (response, status) = client.get(url).await.unwrap();
226
227 assert_eq!(response, RESPONSE.as_bytes());
228 assert_eq!(status, StatusCode::OK);
229 mock.assert_calls(1);
230 }
231
232 #[tokio::test]
233 async fn get_err_response() {
234 const RESPONSE: &str = "NOPE";
235 let client = default_client();
236 let server = MockServer::start();
237 let addr = server.address();
238 let mock = server.mock(|when, then| {
239 when.method(GET).path("/testing");
240 then.body(RESPONSE.as_bytes()).status(404);
241 });
242 let url = Url::parse(&format!("http://{}/testing", addr)).unwrap();
243 let (response, status) = client.get(url).await.unwrap();
244
245 assert_eq!(response, RESPONSE.as_bytes());
246 assert_eq!(status, StatusCode::NOT_FOUND);
247 mock.assert_calls(1);
248 }
249
250 #[tokio::test]
251 async fn post_json() {
252 let client = default_client();
253 let server = MockServer::start();
254 let addr = server.address();
255 let json = serde_json::json!({
256 "name": "foo",
257 "data": 12
258 });
259 let json_clone = json.clone();
260 let mock = server.mock(|when, then| {
261 when.method(POST).path("/testing").json_body(json_clone);
262 then.body(b"OK").status(200);
263 });
264 let url = Url::parse(&format!("http://{}/testing", addr)).unwrap();
265 client.post(url, &json).await.unwrap();
266
267 mock.assert_calls(1);
268 }
269}