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}