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}