freedom_api/
client.rs

1use bytes::Bytes;
2use freedom_config::Config;
3use reqwest::{RequestBuilder, Response, 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<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}