rusty_box/auth/
auth_client.rs

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