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
27pub(crate) fn encode_param(param: &str) -> String {
29 percent_encode(param.as_bytes(), NON_ALPHANUMERIC).to_string()
30}
31
32#[macro_export]
34macro_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#[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
155impl 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 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}