1use core::{marker::PhantomData, str};
2
3use httparse::{Response, Status};
4use serde::{Deserialize, Serialize};
5use serde_json_core::ser;
6
7use crate::{
8 api_version::{ApiVersion, V3},
9 http_client_config::HttpClientConfig,
10 models::{
11 ApiResponse, ApiResponseFailedData, ApiResponseFailedEmpty, ApiStatus, HttpBody,
12 HttpMethod, HttpRequest, StatusCode,
13 },
14 HttpClientError, HttpClientErrorWrapper, HttpHandler,
15};
16
17const MAX_BODY_LENGTH: usize = 2048;
19const MAX_NUMBER_OF_HEADERS: usize = 64;
23const DEFAULT_API_PATH: &str = "/api";
24const PATH_SEPARATOR: &str = "/";
25const CONTENT_LENGTH_HEADER: &str = "Content-Length";
26
27pub struct HttpClient<'a, V: ApiVersion, H: HttpHandler> {
32 config: HttpClientConfig<'a>,
33 http_handler: H,
34 _marker: PhantomData<fn() -> V>,
37}
38
39impl<'a, H: HttpHandler> HttpClient<'a, V3, H> {
40 #[must_use]
42 pub fn new(config: HttpClientConfig<'a>, http_handler: H) -> Self {
43 Self {
44 config,
45 http_handler,
46 _marker: PhantomData,
47 }
48 }
49
50 #[must_use]
52 pub const fn config(&self) -> &HttpClientConfig {
53 &self.config
54 }
55
56 pub async fn send<'d, B, T>(
62 &'a self,
63 method: HttpMethod,
64 base_path: Option<&'a str>,
65 path: &'a str,
66 body: B,
67 out_buffer: &'d mut [u8],
69 ) -> Result<T, HttpClientErrorWrapper<'d, H>>
70 where
71 B: Serialize,
72 T: Deserialize<'d>,
73 {
74 let mut serialized: [u8; MAX_BODY_LENGTH] = [0; MAX_BODY_LENGTH];
76 let size = serde_json_core::to_slice(&body, &mut serialized).map_err(|e| match e {
77 ser::Error::BufferFull => HttpClientError::SerializationBufferFull(MAX_BODY_LENGTH),
78 _ => HttpClientError::Serialization,
79 })?;
80 #[allow(clippy::shadow_reuse)]
81 let serialized = &serialized[0..size];
82
83 let request = HttpRequest {
84 method,
85 domain_name: self.config.domain_name,
86 port: self.config.port,
87 user_agent: self.config.user_agent,
88 base_path: base_path.or(Some(const_format::concatcp!(
89 DEFAULT_API_PATH,
90 PATH_SEPARATOR,
91 V3::VERSION
92 ))),
93 path,
94 accept: "application/json",
95 body: Some(HttpBody {
96 content_type: "application/json",
97 body: serialized,
98 }),
99 };
100
101 self.http_handler
103 .http_call(request, out_buffer)
104 .await
105 .map_err(HttpClientError::SendRequest)?;
106
107 let (body, status_code) = Self::extract_body(out_buffer)?;
109
110 let (api_response, _bytes): (ApiResponse<'d, T>, usize) =
121 match serde_json_core::from_slice(body) {
122 Ok(response) => response,
123 Err(_) => match serde_json_core::from_slice::<ApiResponseFailedData<'d>>(body) {
124 Ok((response_failed, size)) => (ApiResponse::from(response_failed), size),
125
126 Err(_) => match serde_json_core::from_slice::<ApiResponseFailedEmpty<'d>>(body)
127 {
128 Ok((response_failed, size)) => (ApiResponse::from(response_failed), size),
129
130 Err(e) => return Err(HttpClientError::DeserializationFailed(e).into()),
131 },
132 },
133 };
134
135 match api_response.status {
137 Some(ApiStatus::Success) => {
138 Ok(api_response.data.ok_or(HttpClientError::MissingDataField)?)
139 }
140
141 Some(ApiStatus::Error) => api_response.message.map_or_else(
142 || Err(HttpClientError::FailedAndMissingMessageField.into()),
143 |message: &'d str| {
144 Err(HttpClientErrorWrapper::GenericApiError {
145 message,
146 status_code,
147 })
148 },
149 ),
150
151 None => Err(HttpClientError::MissingStatusField.into()),
152 }
153 }
154}
155
156impl<'a, V: ApiVersion, H: HttpHandler> HttpClient<'a, V, H> {
157 fn extract_body(buffer: &[u8]) -> Result<(&[u8], StatusCode), HttpClientError<H>> {
163 let mut headers = [httparse::EMPTY_HEADER; MAX_NUMBER_OF_HEADERS];
165 let mut response = Response::new(&mut headers);
166
167 let body_index = match response.parse(buffer).map_err(HttpClientError::ParseHttp)? {
168 Status::Complete(body_start_index) => body_start_index,
169 Status::Partial => {
170 return Err(HttpClientError::PartialHttpRequest);
171 }
172 };
173 let status_code = response.code.ok_or(HttpClientError::NoStatusCode)?;
174
175 let content_length_header = response
177 .headers
178 .iter()
179 .find(|header| header.name == CONTENT_LENGTH_HEADER)
180 .ok_or(HttpClientError::NoContentHeader)?;
181
182 let content_length = str::from_utf8(content_length_header.value)
183 .map_err(|e| HttpClientError::Utf8Conversion(e.valid_up_to()))?
184 .parse::<usize>()
185 .map_err(HttpClientError::ContentLengthConversion)?;
186
187 let body = &buffer[body_index..(body_index + content_length)];
188
189 Ok((body, StatusCode(status_code)))
190 }
191}
192
193#[cfg(test)]
196mod tests {
197 use httparse::Error as HttpParseError;
198 use indoc::indoc;
199
200 use super::*;
201
202 #[derive(Debug, PartialEq, Eq)]
203 struct TestHttpHandler<'a> {
204 response: &'a [u8],
205 }
206
207 #[derive(Debug, PartialEq, Eq)]
208 #[cfg_attr(feature = "log", derive(defmt::Format))]
209 enum TestHttpHandlerError {
210 InvalidPath,
211 }
212
213 #[derive(Debug, PartialEq, Eq, Deserialize)]
214 #[cfg_attr(feature = "log", derive(defmt::Format))]
215 struct TestDataStructure {
216 test: u8,
217 }
218
219 impl<'b> HttpHandler for TestHttpHandler<'b> {
220 type Error = TestHttpHandlerError;
221
222 async fn http_call<'a, 'd>(
223 &'a self,
224 request: HttpRequest<'_>,
225 out_buffer: &'d mut [u8],
226 ) -> Result<(), Self::Error> {
227 if request.path == INVALID_PATH {
228 return Err(TestHttpHandlerError::InvalidPath);
229 }
230
231 out_buffer[0..self.response.len()].copy_from_slice(self.response);
232
233 Ok(())
234 }
235 }
236
237 const VALID_TEST_DATA_STRUCTURE_RESPONSE: &str = indoc! {r#"
238 HTTP/1.1 200 OK
239 Content-Type: application/json; charset=utf-8
240 Content-Length: 39
241 Connection: close
242
243 {"status":"success","data":{"test":42}}
244 "#};
245
246 const ANY_METHOD: HttpMethod = HttpMethod::GET;
247 const ANY_PATH: &str = "any";
248 const ANY_BODY: &str = "any";
249 const INVALID_PATH: &str = "invalid_path";
250
251 fn given_http_client(response: &[u8]) -> (HttpClient<V3, TestHttpHandler<'_>>, [u8; 2048]) {
252 let http_handler = TestHttpHandler { response };
253 let http_client =
254 HttpClient::<'_, V3, TestHttpHandler>::new(HttpClientConfig::default(), http_handler);
255 let response_buffer: [u8; 2048] = [0; 2048];
256
257 (http_client, response_buffer)
258 }
259
260 #[tokio::test]
261 async fn given_valid_parameters_and_response_when_send_http_then_ok_t() {
262 let (http_client, mut response_buffer) =
263 given_http_client(VALID_TEST_DATA_STRUCTURE_RESPONSE.as_bytes());
264
265 let response: TestDataStructure = http_client
266 .send(ANY_METHOD, None, ANY_PATH, ANY_BODY, &mut response_buffer)
267 .await
268 .unwrap();
269
270 assert_eq!(42, response.test);
271 }
272
273 #[tokio::test]
274 async fn given_serialization_buffer_too_small_when_send_http_then_serialization_err() {
275 let (http_client, mut response_buffer) =
276 given_http_client(VALID_TEST_DATA_STRUCTURE_RESPONSE.as_bytes());
277 let body_buffer: [u8; MAX_BODY_LENGTH + 1] = [0; MAX_BODY_LENGTH + 1];
278 let body = str::from_utf8(&body_buffer).unwrap();
279
280 let response = http_client
281 .send::<_, ()>(ANY_METHOD, None, ANY_PATH, body, &mut response_buffer)
282 .await;
283
284 assert_eq!(
285 HttpClientErrorWrapper::OtherErrors(HttpClientError::SerializationBufferFull(2048)),
286 response.unwrap_err()
287 );
288 }
289
290 #[tokio::test]
291 async fn given_http_call_error_when_send_http_then_request_err() {
292 let (http_client, mut response_buffer) =
293 given_http_client(VALID_TEST_DATA_STRUCTURE_RESPONSE.as_bytes());
294
295 let response = http_client
296 .send::<_, ()>(
297 ANY_METHOD,
298 Some("/"),
299 INVALID_PATH,
300 ANY_BODY,
301 &mut response_buffer,
302 )
303 .await;
304
305 assert_eq!(
306 HttpClientErrorWrapper::OtherErrors(HttpClientError::SendRequest(
307 TestHttpHandlerError::InvalidPath
308 )),
309 response.unwrap_err()
310 );
311 }
312
313 #[tokio::test]
314 async fn given_no_status_field_when_send_http_then_missing_status_field() {
315 let response = indoc! {r#"
316 HTTP/1.1 200 OK
317 Content-Type: application/json; charset=utf-8
318 Content-Length: 91
319 Connection: close
320
321 {"data":{"configuration":null,"mqtt":{"host":"mqtts.fundamentum-iot-dev.com","port":8883}}}
322 "#};
323
324 let (http_client, mut response_buffer) = given_http_client(response.as_bytes());
325
326 let response = http_client
327 .send::<_, ()>(ANY_METHOD, None, ANY_PATH, ANY_BODY, &mut response_buffer)
328 .await;
329
330 assert_eq!(
331 HttpClientErrorWrapper::OtherErrors(HttpClientError::MissingStatusField),
332 response.unwrap_err()
333 );
334 }
335
336 #[tokio::test]
337 async fn given_no_data_field_when_send_http_then_missing_data_field() {
338 let response = indoc! {r#"
339 HTTP/1.1 200 OK
340 Content-Type: application/json; charset=utf-8
341 Content-Length: 20
342 Connection: close
343
344 {"status":"success"}
345 "#};
346 let (http_client, mut response_buffer) = given_http_client(response.as_bytes());
347
348 let response = http_client
349 .send::<_, ()>(ANY_METHOD, None, ANY_PATH, ANY_BODY, &mut response_buffer)
350 .await;
351
352 assert_eq!(
353 HttpClientErrorWrapper::OtherErrors(HttpClientError::MissingDataField),
354 response.unwrap_err()
355 );
356 }
357
358 #[tokio::test]
359 async fn given_no_message_field_when_send_http_then_missing_message_field() {
360 let response = indoc! {r#"
361 HTTP/1.1 200 OK
362 Content-Type: application/json; charset=utf-8
363 Content-Length: 28
364 Connection: close
365
366 {"status":"error","data":{}}
367 "#};
368 let (http_client, mut response_buffer) = given_http_client(response.as_bytes());
369
370 let response = http_client
371 .send::<_, ()>(ANY_METHOD, None, ANY_PATH, ANY_BODY, &mut response_buffer)
372 .await;
373
374 assert_eq!(
375 HttpClientErrorWrapper::OtherErrors(HttpClientError::FailedAndMissingMessageField),
376 response.unwrap_err()
377 );
378 }
379
380 #[test]
383 fn given_valid_response_when_extract_body_then_body_and_status_code() {
384 let buffer = indoc! {r#"
385 HTTP/1.1 200 OK
386 Content-Type: application/json; charset=utf-8
387 Content-Length: 110
388 Connection: close
389
390 {"status":"success","data":{"configuration":null,"mqtt":{"host":"mqtts.fundamentum-iot-dev.com","port":8883}}}
391 "#};
392 let expected_body = r#"{"status":"success","data":{"configuration":null,"mqtt":{"host":"mqtts.fundamentum-iot-dev.com","port":8883}}}"#;
393
394 let (body, status_code) =
395 HttpClient::<'_, V3, TestHttpHandler>::extract_body(buffer.as_bytes()).unwrap();
396
397 assert_eq!(expected_body, str::from_utf8(body).unwrap());
398 assert_eq!(StatusCode(200), status_code);
399 }
400
401 #[test]
402 fn given_bad_http_response_when_extract_body_then_parse_http_err() {
403 let buffer_missing_space = indoc! {r#"
404 HTTP/1.1 200 OK
405 Content-Type: application/json; charset=utf-8
406 Content-Length: 110
407 Connection: close
408 {"status":"success","data":{"configuration":null,"mqtt":{"host":"mqtts.fundamentum-iot-dev.com","port":8883}}}
409 "#};
410
411 let result =
412 HttpClient::<'_, V3, TestHttpHandler>::extract_body(buffer_missing_space.as_bytes());
413
414 assert_eq!(
415 HttpClientError::ParseHttp(HttpParseError::HeaderName),
416 result.unwrap_err()
417 );
418 }
419
420 #[test]
421 fn given_partial_http_response_when_extract_body_then_partial_http_err() {
422 let buffer_missing_headers_and_body = indoc! {"
423 HTTP/1.1 200 OK
424 Content-Type: application/json; charset=utf-8
425 Content-Length: 110"
426 };
427
428 let result = HttpClient::<'_, V3, TestHttpHandler>::extract_body(
429 buffer_missing_headers_and_body.as_bytes(),
430 );
431
432 assert_eq!(HttpClientError::PartialHttpRequest, result.unwrap_err());
433 }
434
435 #[test]
436 fn given_missing_content_length_header_when_extract_body_then_no_content_header_err() {
437 let buffer_missing_content_length = indoc! {r#"
438 HTTP/1.1 200 OK
439 Content-Type: application/json; charset=utf-8
440 Connection: close
441
442 {"status":"success","data":{"configuration":null,"mqtt":{"host":"mqtts.fundamentum-iot-dev.com","port":8883}}}
443 "#};
444
445 let result = HttpClient::<'_, V3, TestHttpHandler>::extract_body(
446 buffer_missing_content_length.as_bytes(),
447 );
448
449 assert_eq!(HttpClientError::NoContentHeader, result.unwrap_err());
450 }
451
452 #[test]
453 fn given_invalid_utf8_string_when_extract_body_then_utf8_err() {
454 let buffer_with_invalid_utf8 = [
455 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x31, 0x20, 0x32, 0x30, 0x30, 0x20, 0x4f,
456 0x4b, 0x0a, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x54, 0x79, 0x70, 0x65,
457 0x3a, 0x20, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f,
458 0x6a, 0x73, 0x6f, 0x6e, 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d,
459 0x75, 0x74, 0x66, 0x2d, 0x38, 0x0a, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d,
460 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x3a, 0x20, 0x31, 0xff, 0x30, 0x0a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20,
462 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x0a, 0x0a, 0x7b, 0x22, 0x73, 0x74, 0x61, 0x74, 0x75,
463 0x73, 0x22, 0x3a, 0x22, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x2c, 0x22,
464 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x7b, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
465 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x6e, 0x75, 0x6c, 0x6c, 0x2c,
466 0x22, 0x6d, 0x71, 0x74, 0x74, 0x22, 0x3a, 0x7b, 0x22, 0x68, 0x6f, 0x73, 0x74, 0x22,
467 0x3a, 0x22, 0x6d, 0x71, 0x74, 0x74, 0x73, 0x2e, 0x66, 0x75, 0x6e, 0x64, 0x61, 0x6d,
468 0x65, 0x6e, 0x74, 0x75, 0x6d, 0x2d, 0x69, 0x6f, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2e,
469 0x63, 0x6f, 0x6d, 0x22, 0x2c, 0x22, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x3a, 0x38, 0x38,
470 0x38, 0x33, 0x7d, 0x7d, 0x7d,
471 ];
472 let result = HttpClient::<'_, V3, TestHttpHandler>::extract_body(&buffer_with_invalid_utf8);
481
482 assert_eq!(HttpClientError::Utf8Conversion(1), result.unwrap_err());
483 }
484}