ubi_rs/
client.rs

1use crate::errors::UbiClientError;
2use percent_encoding::{NON_ALPHANUMERIC, percent_encode};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone)]
6pub struct HTTPClient {
7    client: reqwest::Client,
8    base_url: reqwest::Url,
9    version: String,
10}
11
12#[derive(Deserialize, Serialize, Debug, Default)]
13pub struct APIError {
14    pub code: Option<u16>,
15    pub message: Option<String>,
16
17    #[serde(rename = "type")]
18    pub _type: Option<String>,
19    pub details: Option<String>,
20}
21
22#[derive(Deserialize, Serialize, Debug, Default)]
23pub struct ResponseAPIError {
24    pub error: APIError,
25}
26
27/// Percent encode an incoming parameter
28pub(crate) fn encode_param(param: &str) -> String {
29    percent_encode(param.as_bytes(), NON_ALPHANUMERIC).to_string()
30}
31
32/// Make a http request by providing a json-body
33#[macro_export]
34/// Macro for making a JSON request using the specified HTTP method, URL, and request body.
35///
36/// # Arguments
37///
38/// * `$sel`: The selector for the HTTP client.
39/// * `$method`: The HTTP method to use for the request.
40/// * `$url`: The URL to send the request to.
41/// * `$body`: The request body.
42/// * `$query`: Optional query parameters to include in the request.
43/// * `$response_ty`: The expected response type.
44///
45/// # Returns
46///
47/// Returns a `Result` containing the JSON response if the request is successful, or an `APIError` if the request fails.
48///
49macro_rules! make_json_request {
50    ($sel:ident, $method:path, $url:expr, $body:expr, $query:expr, $response_ty:ty) => {{
51        use $crate::{client::ResponseAPIError, errors::UbiClientError};
52        use tracing::{debug, error};
53
54        debug!(
55            method = stringify!($method),
56            url = $url,
57            body = ?$body,
58            "Sending JSON request"
59        );
60
61        let response = $sel
62            .http_client
63            .inner($method, $url)?
64            .json(&$body)
65            .query(&$query)
66            .send()
67            .await?;
68
69        let status = response.status();
70        if status.as_u16() == 204 {
71            return Ok(Default::default());
72        }
73
74        if !status.is_success() {
75            error!(status = status.as_u16(), url = $url, "Non-success response");
76
77            let value: serde_json::Value = response.json().await?;
78            debug!(?value, "Received error response");
79
80            let parsed_error: ResponseAPIError = serde_json::from_value(value)?;
81
82            return Err(UbiClientError::APIResponseError {
83                etype: parsed_error.error._type.unwrap_or_default(),
84                message: parsed_error.error.message.unwrap_or_default(),
85                details: parsed_error.error.details,
86            });
87        }
88
89        #[cfg(feature = "debug")]
90        {
91            let json: serde_json::Value = response.json().await?;
92            debug!(?json, "Parsed JSON response");
93            Ok(serde_json::from_value::<$response_ty>(json)?)
94        }
95
96        #[cfg(not(feature = "debug"))]
97        {
98            Ok(response.json::<$response_ty>().await?)
99        }
100    }};
101}
102
103/// Make a http request without json body.
104#[macro_export]
105macro_rules! make_request {
106    ($sel:ident, $method:path, $url:expr) => {{
107        use reqwest::Response;
108        use tracing::{debug, error};
109        use $crate::client::ResponseAPIError;
110
111        debug!(method = stringify!($method), url = $url);
112
113        let response: Response = $sel.http_client.inner($method, $url)?.send().await?;
114        let status_code = response.status().as_u16();
115
116        debug!("Received HTTP status code: {}", status_code);
117        debug!("Sending request to URL: {}", $url);
118        if status_code == 204 {
119            return Ok(Default::default());
120        }
121
122        if !response.status().is_success() {
123            error!(status = status_code, url = $url, "Non-success response");
124
125            let value: serde_json::Value = response.json().await?;
126            debug!(?value, "Received error response");
127
128            let parsed_error: ResponseAPIError = serde_json::from_value(value)?;
129
130            return Err(UbiClientError::APIResponseError {
131                etype: parsed_error.error._type.unwrap_or_default(),
132                message: parsed_error.error.message.unwrap_or_default(),
133                details: parsed_error.error.details,
134            });
135        }
136        let ret: Result<reqwest::Response, UbiClientError> = Ok(response);
137        ret
138        // match status_code {
139        //     200..=299 => {
140        //         #[cfg(feature = "debug")]
141        //         {
142        //             if status_code == 204 {
143        //                 return Ok(Default::default());
144        //             }
145        //             let res: serde_json::Value = response.json().await?;
146        //             debug!("Response: {:#?}", res);
147        //             Ok(serde_json::from_value(res)?)
148        //         }
149        //         #[cfg(not(feature = "debug"))]
150        //         {
151        //             if status_code == 204 {
152        //                 return Ok(Default::default());
153        //             }
154        //             Ok(response.json().await?)
155        //         }
156        //     }
157
158        //     _ => {
159        //         let json_body: serde_json::Value = response.json().await?;
160        //         debug!("Received API error response: {:#?}", json_body);
161
162        //         let api_error: ResponseAPIError =
163        //             serde_json::from_value(json_body).unwrap_or_else(|_| ResponseAPIError {
164        //                 error: Default::default(),
165        //             });
166
167        //         Err(UbiClientError::APIResponseError {
168        //             etype: api_error.error._type.unwrap_or_default(),
169        //             message: api_error.error.message.unwrap_or_default(),
170        //             details: api_error.error.details,
171        //         })
172        //     }
173        // }
174    }};
175}
176
177// #[macro_export]
178// macro_rules! make_request {
179//     ($sel:ident, $method:path, $url:expr) => {{
180//         use reqwest;
181//         tracing::debug!(method = stringify!($method), url = $url,);
182//         let response: reqwest::Response = $sel.http_client.inner($method, $url)?.send().await?;
183//         use $crate::client::ResponseAPIError;
184
185//         let status_code = &response.status().as_u16();
186//         tracing::debug!("Received http status code: {}", status_code);
187//         tracing::debug!("Sending requests to url: {}", $url);
188
189//         if !(*status_code >= 200 && *status_code < 300) {
190//             if *status_code == 204 {
191//                 return Ok(());
192//             }
193//             let api_response: serde_json::Value = response.json().await?;
194//             tracing::debug!("Received api response: {:#?}", api_response);
195//             let api_response: ResponseAPIError = serde_json::from_value(api_response).unwrap();
196//             return Err(UbiClientError::APIResponseError {
197//                 etype: api_response.error._type.unwrap_or_default(),
198//                 message: api_response.error.message.unwrap_or_default(),
199//                 details: api_response.error.details,
200//             });
201//         }
202
203//         #[cfg(feature = "debug")]
204//         {
205//             let res: serde_json::Value = response.json().await?;
206//             tracing::debug!("Response {:?}", res);
207//             Ok(serde_json::from_value(res)?)
208//         }
209
210//         #[cfg(not(feature = "debug"))]
211//         {
212//             Ok(response.json().await?)
213//         }
214//     }};
215// }
216
217/// Represents an HTTP client for making requests to a specific base URL and API version.
218///
219/// The `HTTPClient` struct provides a convenient way to make HTTP requests to a specific base URL and API version.
220/// It wraps the `reqwest` library to provide a higher-level interface for making requests.
221///
222/// The `new` function creates a new `HTTPClient` instance by parsing the provided base URL and API version.
223/// The `inner` function is used to create a `reqwest::RequestBuilder` for a specific HTTP method and URL path.
224///
225/// This implementation is an internal detail of the crate and is not intended to be used directly by end-users.
226impl HTTPClient {
227    pub fn new<S, T>(base_url: S, client: reqwest::Client, version: T) -> HTTPClient
228    where
229        S: Into<String>,
230        T: Into<String>,
231    {
232        let parsed_url =
233            reqwest::Url::parse(&base_url.into()).expect("Failed to parse the base_url");
234
235        let ver = format!("{}/", version.into().replace('/', ""));
236        tracing::debug!("API Version is {}", &ver);
237        HTTPClient {
238            base_url: parsed_url,
239            client,
240            version: ver,
241        }
242    }
243
244    pub(crate) fn inner(
245        &self,
246        method: reqwest::Method,
247        query_url: &str,
248    ) -> Result<reqwest::RequestBuilder, UbiClientError> {
249        let qurl = query_url.trim_start_matches('/');
250        let url = self.base_url.join(&self.version)?.join(qurl)?;
251        tracing::debug!("URL is {:?}", &url);
252
253        // dbg!(&url);
254        let request_with_url_and_header: Result<reqwest::RequestBuilder, UbiClientError> =
255            match method {
256                reqwest::Method::GET => Ok(self.client.get(url)),
257                reqwest::Method::PUT => Ok(self.client.put(url)),
258                reqwest::Method::POST => Ok(self.client.post(url)),
259                reqwest::Method::DELETE => Ok(self.client.delete(url)),
260                _ => return Err(UbiClientError::UnsupportedMethod),
261            };
262        request_with_url_and_header
263    }
264}