1use bytes::Bytes;
2use freedom_config::Config;
3use reqwest::{RequestBuilder, Response, 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<Response, crate::error::Error> {
112 tracing::trace!("DELETE to {}", url);
113
114 let req = self.append_headers(self.client.delete(url.clone()));
115
116 req.basic_auth(self.config.key(), Some(self.config.expose_secret()))
117 .send()
118 .await
119 .inspect_err(|error| tracing::warn!(%error, %url, "Failed to DELETE"))
120 .inspect(|ok| tracing::warn!(?ok, %url, "Received response"))
121 .map_err(From::from)
122 }
123
124 async fn post<S>(&self, url: Url, msg: S) -> Result<Response, crate::error::Error>
125 where
126 S: serde::Serialize + Sync + Send,
127 {
128 tracing::trace!("POST to {}", url);
129
130 let req = self.append_headers(self.client.post(url.clone()));
131
132 req.basic_auth(self.config.key(), Some(self.config.expose_secret()))
133 .json(&msg)
134 .send()
135 .await
136 .inspect_err(|error| tracing::warn!(%error, %url, "Failed to POST"))
137 .inspect(|ok| tracing::warn!(?ok, %url, "Received response"))
138 .map_err(From::from)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use freedom_config::Test;
145 use httpmock::{
146 Method::{GET, POST},
147 MockServer,
148 };
149
150 use crate::Container;
151
152 use super::*;
153
154 fn default_client() -> Client {
155 let config = Config::builder()
156 .environment(Test)
157 .key("foo")
158 .secret("bar")
159 .build()
160 .unwrap();
161
162 Client::from_config(config)
163 }
164
165 #[test]
166 fn clients_are_eq_based_on_config() {
167 let config = Config::builder()
168 .environment(Test)
169 .key("foo")
170 .secret("bar")
171 .build()
172 .unwrap();
173
174 let client_1 = Client::from_config(config.clone());
175 let client_2 = Client::from_config(config);
176 assert_eq!(client_1, client_2);
177 }
178
179 #[test]
180 fn wrap_and_unwrap_inner() {
181 let val = String::from("foobar");
182 let inner = Inner::new(val.clone());
183 assert_eq!(*inner, val);
184 let unwrapped = inner.into_inner();
185 assert_eq!(val, unwrapped);
186 }
187
188 #[test]
189 fn load_from_env() {
190 unsafe {
191 std::env::set_var("ATLAS_ENV", "TEST");
192 std::env::set_var("ATLAS_KEY", "foo");
193 std::env::set_var("ATLAS_SECRET", "bar");
194 };
195 let client = Client::from_env().unwrap();
196 assert_eq!(client.config().key(), "foo");
197 assert_eq!(client.config().expose_secret(), "bar");
198 }
199
200 #[tokio::test]
201 async fn get_ok_response() {
202 const RESPONSE: &str = "it's working";
203 let client = default_client();
204 let server = MockServer::start();
205 let addr = server.address();
206 let mock = server.mock(|when, then| {
207 when.method(GET).path("/testing");
208 then.body(RESPONSE.as_bytes());
209 });
210 let url = Url::parse(&format!("http://{}/testing", addr)).unwrap();
211 let (response, status) = client.get(url).await.unwrap();
212
213 assert_eq!(response, RESPONSE.as_bytes());
214 assert_eq!(status, StatusCode::OK);
215 mock.assert_calls(1);
216 }
217
218 #[tokio::test]
219 async fn get_err_response() {
220 const RESPONSE: &str = "NOPE";
221 let client = default_client();
222 let server = MockServer::start();
223 let addr = server.address();
224 let mock = server.mock(|when, then| {
225 when.method(GET).path("/testing");
226 then.body(RESPONSE.as_bytes()).status(404);
227 });
228 let url = Url::parse(&format!("http://{}/testing", addr)).unwrap();
229 let (response, status) = client.get(url).await.unwrap();
230
231 assert_eq!(response, RESPONSE.as_bytes());
232 assert_eq!(status, StatusCode::NOT_FOUND);
233 mock.assert_calls(1);
234 }
235
236 #[tokio::test]
237 async fn post_json() {
238 let client = default_client();
239 let server = MockServer::start();
240 let addr = server.address();
241 let json = serde_json::json!({
242 "name": "foo",
243 "data": 12
244 });
245 let json_clone = json.clone();
246 let mock = server.mock(|when, then| {
247 when.method(POST).path("/testing").json_body(json_clone);
248 then.body(b"OK").status(200);
249 });
250 let url = Url::parse(&format!("http://{}/testing", addr)).unwrap();
251 client.post(url, &json).await.unwrap();
252
253 mock.assert_calls(1);
254 }
255}