fundamentum_sdk_api/client/
sdk_client.rs1use super::{config::ClientConfig, errors::SdkClientError};
4use crate::models::{
5 api_response::{ApiResponse, ApiStatus},
6 constants::{OPEN_API_DOWNLOAD_PATH, STATUS_PATH},
7 general::Status,
8};
9use reqwest::{Client, Method, StatusCode};
10use serde::{de::DeserializeOwned, Serialize};
11use std::path::PathBuf;
12use url::Url;
13
14#[derive(Clone)]
17pub struct SdkClient {
18 inner_client: Client,
19 config: ClientConfig,
20 api_version: String,
21}
22
23impl SdkClient {
24 #[must_use]
26 pub fn new(config: ClientConfig, api_version: &str) -> Self {
27 Self {
28 inner_client: Client::new(),
29 config,
30 api_version: api_version.to_owned(),
31 }
32 }
33
34 #[must_use]
36 pub const fn config(&self) -> &ClientConfig {
37 &self.config
38 }
39
40 pub async fn status(&self) -> Result<Status, SdkClientError> {
47 let url = Url::parse(&format!("{}{}", self.config().base_path, STATUS_PATH))
48 .map_err(SdkClientError::BasePathError)?;
49
50 let response = self
51 .inner_client
52 .request(Method::GET, url)
53 .send()
54 .await
55 .map_err(SdkClientError::ReqwestError)?;
56 let status_code = response.status();
57
58 Self::extract_response(
59 response
60 .json()
61 .await
62 .map_err(|e| SdkClientError::BodyToJsonError(e, status_code))?,
63 status_code,
64 )
65 }
66
67 pub async fn get_openapi_json(&self) -> Result<serde_json::Value, SdkClientError> {
74 let url = Url::parse(&format!(
75 "{}{}",
76 self.config().base_path,
77 OPEN_API_DOWNLOAD_PATH
78 ))
79 .map_err(SdkClientError::BasePathError)?;
80
81 let response = self
82 .inner_client
83 .request(Method::GET, url)
84 .send()
85 .await
86 .map_err(SdkClientError::ReqwestError)?;
87 let status_code = response.status();
88
89 response
90 .json::<serde_json::Value>()
91 .await
92 .map_err(|e| SdkClientError::BodyToJsonError(e, status_code))
93 }
94
95 pub async fn get<T, S>(&self, path: S) -> Result<T, SdkClientError>
101 where
102 T: DeserializeOwned,
103 S: Into<String> + Send,
104 {
105 self.send(Method::GET, path, Option::<&str>::None).await
106 }
107
108 pub async fn delete<T, S>(&self, path: S) -> Result<T, SdkClientError>
114 where
115 T: DeserializeOwned,
116 S: Into<String> + Send,
117 {
118 self.send(Method::DELETE, path, Option::<&str>::None).await
119 }
120
121 pub async fn post_body<T, S, B>(&self, path: S, body: B) -> Result<T, SdkClientError>
127 where
128 T: DeserializeOwned,
129 S: Into<String> + Send,
130 B: Serialize + Send + Sync,
131 {
132 self.send(Method::POST, path, Some(body)).await
133 }
134
135 pub async fn put_body<T, S, B>(&self, path: S, body: B) -> Result<T, SdkClientError>
141 where
142 T: DeserializeOwned,
143 S: Into<String> + Send,
144 B: Serialize + Send + Sync,
145 {
146 self.send(Method::PUT, path, Some(body)).await
147 }
148
149 async fn send<T, S, B>(
155 &self,
156 method: Method,
157 path: S,
158 maybe_body: Option<B>,
159 ) -> Result<T, SdkClientError>
160 where
161 T: DeserializeOwned,
162 S: Into<String> + Send,
163 B: Serialize + Send + Sync,
164 {
165 let url = self.make_url(&path.into())?;
166
167 let request = self.inner_client.request(method, url);
168 let request = match self.config().api_token {
169 Some(ref api_token) => request.header("Authorization", format!("Bearer {api_token}")),
170 _ => request,
171 };
172
173 let response = match maybe_body {
175 Some(body) => request
176 .json(&body)
177 .send()
178 .await
179 .map_err(SdkClientError::ReqwestError)?,
180 None => request.send().await.map_err(SdkClientError::ReqwestError)?,
181 };
182 let status_code = response.status();
183
184 Self::extract_response(
186 response
187 .json()
188 .await
189 .map_err(|e| SdkClientError::BodyToJsonError(e, status_code))?,
190 status_code,
191 )
192 }
193
194 fn make_url(&self, end_path: &str) -> Result<Url, SdkClientError> {
195 let buffer = PathBuf::from("/api/")
196 .join(&self.api_version)
197 .join(end_path.strip_prefix('/').unwrap_or(end_path));
198
199 let url_string = buffer
200 .to_str()
201 .ok_or(SdkClientError::JoinPathError(buffer.clone()))?
202 .to_owned();
203
204 Url::parse(&format!("{}{}", self.config().base_path, url_string))
205 .map_err(SdkClientError::BasePathError)
206 }
207
208 fn extract_response<T: DeserializeOwned>(
209 api_response: ApiResponse,
210 status_code: StatusCode,
211 ) -> Result<T, SdkClientError> {
212 match api_response.status {
213 Some(ApiStatus::Success) => serde_json::from_value::<T>(api_response.data.into())
214 .map_err(SdkClientError::DataToJsonError),
215
216 Some(ApiStatus::Error) => api_response.clone().message.map_or_else(
217 || {
218 Err(SdkClientError::MissingMessageField(
219 api_response.clone(),
220 status_code,
221 ))
222 },
223 |message| {
224 Err(SdkClientError::GenericApiError(
225 message,
226 api_response.clone(),
227 status_code,
228 ))
229 },
230 ),
231
232 None => Err(SdkClientError::MissingStatusField(
233 api_response,
234 status_code,
235 )),
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::models::constants::PROVISIONING_PATH;
244
245 #[tokio::test]
246 async fn when_status_request_then_returns_good_status() {
247 let client = SdkClient::new(
248 ClientConfig::builder("https://devices.fundamentum-iot-dev.com".to_owned()).build(),
249 "v3",
250 );
251
252 let status = client.status().await;
253
254 assert!(status.is_ok(), "{status:?}");
255 }
256
257 #[tokio::test]
258 async fn when_getting_openapi_file_then_returns_json_file() {
259 let client = SdkClient::new(
260 ClientConfig::builder("https://devices.fundamentum-iot-dev.com".to_owned()).build(),
261 "v3",
262 );
263
264 let json_file = client.get_openapi_json().await;
265
266 assert!(json_file.is_ok(), "{json_file:?}");
267 }
268
269 #[test]
270 fn given_valid_path_when_make_url_then_returns_good_url() {
271 let client = SdkClient::new(
272 ClientConfig::builder("https://devices.fundamentum-iot-dev.com".to_owned()).build(),
273 "v3",
274 );
275 let path = PROVISIONING_PATH;
276 let valid_url = Url::parse(&format!(
277 "{}/api/{}{}",
278 client.config().base_path,
279 client.api_version,
280 path
281 ))
282 .expect("valid_url must be valid");
283
284 let result = client.make_url(path);
285
286 assert!(result.is_ok(), "{result:?}");
287 let url = result.expect("Must be a valid url");
288 assert_eq!(url, valid_url);
289 }
290}