freedom_api/
client.rs

1use bytes::Bytes;
2use freedom_config::Config;
3use reqwest::{RequestBuilder, StatusCode};
4use url::Url;
5
6use crate::api::{Api, Inner, Value};
7
8/// An asynchronous `Client` for interfacing with the ATLAS freedom API.
9///
10/// The client is primarily defined based on it's [`Env`](crate::config::Env)
11/// and it's credentials (username and password).
12#[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    /// Construct an API client from the provided Freedom config
27    ///
28    /// # Example
29    ///
30    /// ```
31    /// # use freedom_api::prelude::*;
32    /// let config = Config::builder()
33    ///     .environment(Test)
34    ///     .key("foo")
35    ///     .secret("bar")
36    ///     .build()
37    ///     .unwrap();
38    /// let client = Client::from_config(config);
39    ///
40    /// assert_eq!(client.config().key(), "foo");
41    /// ```
42    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    /// A convenience method for constructing an FPS client from environment variables.
51    ///
52    /// This function expects the following environment variables:
53    ///
54    /// + ATLAS_ENV: [possible values: test, prod]
55    /// + ATLAS_KEY: The ATLAS freedom key registered with an account
56    /// + ATLAS_SECRET: The ATLAS freedom secret registered with an account
57    pub fn from_env() -> Result<Self, freedom_config::Error> {
58        let config = Config::from_env()?;
59        Ok(Self::from_config(config))
60    }
61
62    /// Adds a universal header key and value to all GET POST, and DELETEs made with the client
63    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}