rusty_box/client/
http_client.rs

1//! The client implementation for the reqwest HTTP client, which is async by
2//! default.
3
4use std::{collections::HashMap, time::Duration};
5
6use reqwest::{Method, RequestBuilder};
7use serde_json::Value;
8
9use crate::client::client_error_model::BoxAPIErrorResponse;
10
11use super::client_error::BoxAPIError;
12
13/// HTTP headers.
14pub type Headers = HashMap<String, String>;
15/// Query parameters.
16pub type Query<'a> = HashMap<&'a str, &'a str>;
17/// Form parameters.
18pub type Form<'a> = HashMap<&'a str, &'a str>;
19
20#[derive(Debug, Clone)]
21pub struct HttpClient {
22    /// reqwest needs an instance of its client to perform requests.
23    client: reqwest::Client,
24}
25
26impl Default for HttpClient {
27    fn default() -> Self {
28        let client = reqwest::ClientBuilder::new()
29            .timeout(Duration::from_secs(10))
30            .build()
31            // building with these options cannot fail
32            .unwrap();
33        Self { client }
34    }
35}
36
37impl HttpClient {
38    async fn request<D>(
39        &self,
40        method: Method,
41        url: &str,
42        headers: Option<&Headers>,
43        add_data: D,
44    ) -> Result<String, BoxAPIError>
45    where
46        D: Fn(RequestBuilder) -> RequestBuilder,
47    {
48        let mut request = self.client.request(method.clone(), url);
49
50        // Setting the headers, if any
51        if let Some(headers) = headers {
52            // The headers need to be converted into a `reqwest::HeaderMap`,
53            // which won't fail as long as its contents are ASCII. This is an
54            // internal function, so the condition cannot be broken by the user
55            // and will always be true.
56            //
57            // The content-type header will be set automatically.
58            let headers = headers.try_into().unwrap();
59
60            request = request.headers(headers);
61        }
62
63        // Configuring the request for the specific type (get/post/put/delete)
64        request = add_data(request);
65
66        // Finally performing the request and handling the response
67        log::info!("Making request {:?}", request);
68        let response = request.send().await?;
69        let status = response.status();
70        let resp_text = response.text().await?;
71
72        // Making sure that the status code is OK
73        if status.is_success() {
74            Ok(resp_text)
75        } else {
76            let resp_error = serde_json::from_str::<BoxAPIErrorResponse>(&resp_text)?;
77            Err(BoxAPIError::ResponseError(resp_error))
78        }
79    }
80}
81
82impl HttpClient {
83    // type Error = BoxAPIError;
84
85    #[inline]
86    pub async fn get(
87        &self,
88        url: &str,
89        headers: Option<&Headers>,
90        query: &Query<'_>,
91    ) -> Result<String, BoxAPIError> {
92        self.request(Method::GET, url, headers, |req| req.query(query))
93            .await
94    }
95
96    #[inline]
97    pub async fn post(
98        &self,
99        url: &str,
100        headers: Option<&Headers>,
101        query: Option<&Query<'_>>,
102        payload: &Value,
103    ) -> Result<String, BoxAPIError> {
104        self.request(Method::POST, url, headers, |req| {
105            req.query(&query).json(payload)
106        })
107        .await
108    }
109
110    #[inline]
111    pub async fn post_form(
112        &self,
113        url: &str,
114        headers: Option<&Headers>,
115        payload: &Form<'_>,
116    ) -> Result<String, BoxAPIError> {
117        self.request(Method::POST, url, headers, |req| req.form(payload))
118            .await
119    }
120
121    #[inline]
122    pub async fn put(
123        &self,
124        url: &str,
125        headers: Option<&Headers>,
126        query: Option<&Query<'_>>,
127        payload: &Value,
128    ) -> Result<String, BoxAPIError> {
129        self.request(Method::PUT, url, headers, |req| {
130            req.query(&query).json(payload)
131        })
132        .await
133    }
134
135    #[inline]
136    pub async fn delete(
137        &self,
138        url: &str,
139        headers: Option<&Headers>,
140        payload: &Value,
141    ) -> Result<String, BoxAPIError> {
142        self.request(Method::DELETE, url, headers, |req| req.json(payload))
143            .await
144    }
145
146    // TODO: implement method OPTION for reqwest
147}