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
71        if !status.is_success() {
72            error!(status = status.as_u16(), url = $url, "Non-success response");
73
74            let value: serde_json::Value = response.json().await?;
75            debug!(?value, "Received error response");
76
77            let parsed_error: ResponseAPIError = serde_json::from_value(value)?;
78
79            return Err(UbiClientError::APIResponseError {
80                etype: parsed_error.error._type.unwrap_or_default(),
81                message: parsed_error.error.message.unwrap_or_default(),
82                details: parsed_error.error.details,
83            });
84        }
85
86        #[cfg(feature = "debug")]
87        {
88            let json: serde_json::Value = response.json().await?;
89            debug!(?json, "Parsed JSON response");
90            Ok(serde_json::from_value::<$response_ty>(json)?)
91        }
92
93        #[cfg(not(feature = "debug"))]
94        {
95            if status == reqwest::StatusCode::NO_CONTENT {
96                return Ok(());
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;
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
119        match status_code {
120            200..=299 => {
121                #[cfg(feature = "debug")]
122                {
123                    let res: serde_json::Value = response.json().await?;
124                    debug!("Response: {:#?}", res);
125                    Ok(serde_json::from_value(res)?)
126                }
127                #[cfg(not(feature = "debug"))]
128                {
129                    if status_code == 204 {
130                        return Ok(());
131                    }
132                    Ok(response.json().await?)
133                }
134            }
135
136            _ => {
137                let json_body: serde_json::Value = response.json().await?;
138                debug!("Received API error response: {:#?}", json_body);
139
140                let api_error: ResponseAPIError =
141                    serde_json::from_value(json_body).unwrap_or_else(|_| ResponseAPIError {
142                        error: Default::default(),
143                    });
144
145                Err(UbiClientError::APIResponseError {
146                    etype: api_error.error._type.unwrap_or_default(),
147                    message: api_error.error.message.unwrap_or_default(),
148                    details: api_error.error.details,
149                })
150            }
151        }
152    }};
153}
154
155// #[macro_export]
156// macro_rules! make_request {
157//     ($sel:ident, $method:path, $url:expr) => {{
158//         use reqwest;
159//         tracing::debug!(method = stringify!($method), url = $url,);
160//         let response: reqwest::Response = $sel.http_client.inner($method, $url)?.send().await?;
161//         use $crate::client::ResponseAPIError;
162
163//         let status_code = &response.status().as_u16();
164//         tracing::debug!("Received http status code: {}", status_code);
165//         tracing::debug!("Sending requests to url: {}", $url);
166
167//         if !(*status_code >= 200 && *status_code < 300) {
168//             if *status_code == 204 {
169//                 return Ok(());
170//             }
171//             let api_response: serde_json::Value = response.json().await?;
172//             tracing::debug!("Received api response: {:#?}", api_response);
173//             let api_response: ResponseAPIError = serde_json::from_value(api_response).unwrap();
174//             return Err(UbiClientError::APIResponseError {
175//                 etype: api_response.error._type.unwrap_or_default(),
176//                 message: api_response.error.message.unwrap_or_default(),
177//                 details: api_response.error.details,
178//             });
179//         }
180
181//         #[cfg(feature = "debug")]
182//         {
183//             let res: serde_json::Value = response.json().await?;
184//             tracing::debug!("Response {:?}", res);
185//             Ok(serde_json::from_value(res)?)
186//         }
187
188//         #[cfg(not(feature = "debug"))]
189//         {
190//             Ok(response.json().await?)
191//         }
192//     }};
193// }
194
195/// Represents an HTTP client for making requests to a specific base URL and API version.
196///
197/// The `HTTPClient` struct provides a convenient way to make HTTP requests to a specific base URL and API version.
198/// It wraps the `reqwest` library to provide a higher-level interface for making requests.
199///
200/// The `new` function creates a new `HTTPClient` instance by parsing the provided base URL and API version.
201/// The `inner` function is used to create a `reqwest::RequestBuilder` for a specific HTTP method and URL path.
202///
203/// This implementation is an internal detail of the crate and is not intended to be used directly by end-users.
204impl HTTPClient {
205    pub fn new<S, T>(base_url: S, client: reqwest::Client, version: T) -> HTTPClient
206    where
207        S: Into<String>,
208        T: Into<String>,
209    {
210        let parsed_url =
211            reqwest::Url::parse(&base_url.into()).expect("Failed to parse the base_url");
212
213        let ver = format!("{}/", version.into().replace('/', ""));
214        tracing::debug!("API Version is {}", &ver);
215        HTTPClient {
216            base_url: parsed_url,
217            client,
218            version: ver,
219        }
220    }
221
222    pub(crate) fn inner(
223        &self,
224        method: reqwest::Method,
225        query_url: &str,
226    ) -> Result<reqwest::RequestBuilder, UbiClientError> {
227        let qurl = query_url.trim_start_matches('/');
228        let url = self.base_url.join(&self.version)?.join(qurl)?;
229        tracing::debug!("URL is {:?}", &url);
230
231        // dbg!(&url);
232        let request_with_url_and_header: Result<reqwest::RequestBuilder, UbiClientError> =
233            match method {
234                reqwest::Method::GET => Ok(self.client.get(url)),
235                reqwest::Method::PUT => Ok(self.client.put(url)),
236                reqwest::Method::POST => Ok(self.client.post(url)),
237                reqwest::Method::DELETE => Ok(self.client.delete(url)),
238                _ => return Err(UbiClientError::UnsupportedMethod),
239            };
240        request_with_url_and_header
241    }
242}