tastyworks/
request.rs

1use crate::{
2    errors::{ApiError, RequestError},
3    session::Session,
4};
5
6use lazy_static::lazy_static;
7use reqwest::{header, Client, Method};
8
9pub use reqwest::StatusCode;
10
11pub(crate) const BASE_URL: &str = "https://api.tastyworks.com";
12const VERSION: &str = env!("CARGO_PKG_VERSION");
13
14lazy_static! {
15    static ref CLIENT: Client = Client::builder()
16        .user_agent(format!("tasyworks-rs/{}", VERSION))
17        .build()
18        .unwrap();
19}
20
21pub async fn request(
22    url_path: &str,
23    params_string: &str,
24    session: &Session,
25) -> Result<reqwest::Response, RequestError> {
26    let mut api_token_header_value = header::HeaderValue::from_str(&session.token).unwrap();
27    api_token_header_value.set_sensitive(true);
28
29    let params_string = if params_string.is_empty() {
30        params_string.to_string()
31    } else {
32        format!("?{}", params_string)
33    };
34
35    let url = &format!("{}/{}{}", BASE_URL, url_path, params_string);
36    let response = build_request(&url, Method::GET)
37        .header(header::AUTHORIZATION, api_token_header_value)
38        .send()
39        .await;
40
41    map_result(&url, response).await
42}
43
44pub(crate) fn build_request(url: &str, method: Method) -> reqwest::RequestBuilder {
45    CLIENT
46        .request(method, url)
47        .header(header::CONTENT_TYPE, "application/json")
48        .header(header::ACCEPT, "application/json")
49}
50
51pub(crate) async fn map_result(
52    url: &str,
53    result: Result<reqwest::Response, reqwest::Error>,
54) -> Result<reqwest::Response, RequestError> {
55    match result {
56        Err(e) => {
57            return Err(RequestError::FailedRequest {
58                e,
59                url: obfuscate_account_url(url),
60            });
61        }
62        Ok(response) => {
63            if response.status() == 200 || response.status() == 201 {
64                Ok(response)
65            } else {
66                return Err(RequestError::FailedResponse {
67                    status: response.status(),
68                    body: response.text().await.unwrap_or_else(|e| e.to_string()),
69                    url: obfuscate_account_url(url),
70                });
71            }
72        }
73    }
74}
75
76pub(crate) async fn deserialize_response<T>(response: reqwest::Response) -> Result<T, ApiError>
77where
78    T: serde::de::DeserializeOwned,
79{
80    let url = response.url().clone();
81    let bytes = response
82        .bytes()
83        .await
84        .map_err(|e| RequestError::FailedRequest {
85            e,
86            url: obfuscate_account_url(&url),
87        })?;
88
89    let de = &mut serde_json::Deserializer::from_slice(&bytes);
90    let result: Result<T, _> = serde_path_to_error::deserialize(de);
91    result.map_err(|e| ApiError::Decode {
92        e: Box::new(e),
93        url: obfuscate_account_url(&url),
94    })
95}
96
97pub(crate) fn obfuscate_account_url(url: impl AsRef<str>) -> String {
98    const ACCOUNTS_STR: &str = "accounts/";
99
100    let url = url.as_ref();
101    if let Some(accounts_byte_idx) = url.find(ACCOUNTS_STR) {
102        let mut ending_separator_found = false;
103        url.char_indices()
104            .map(|(char_byte_idx, ch)| {
105                if char_byte_idx < accounts_byte_idx + ACCOUNTS_STR.len() || ending_separator_found
106                {
107                    ch
108                } else if ch == '/' {
109                    ending_separator_found = true;
110                    ch
111                } else {
112                    '*'
113                }
114            })
115            .collect()
116    } else {
117        url.to_string()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_obfuscate_account_url() {
127        assert_eq!(obfuscate_account_url("accounts/123ABC"), "accounts/******");
128        assert_eq!(
129            obfuscate_account_url("foo/accounts/123AB/bar"),
130            "foo/accounts/*****/bar"
131        );
132    }
133}