1use crate::{HttpHeader, StatusCode};
2use heapless::Vec;
3
4#[derive(Debug)]
6pub enum ResponseBody<'a> {
7 Text(&'a str),
9 Binary(&'a [u8]),
11 Empty,
13}
14
15impl ResponseBody<'_> {
16 #[must_use]
18 pub fn as_str(&self) -> Option<&str> {
19 match self {
20 ResponseBody::Text(s) => Some(s),
21 ResponseBody::Binary(bytes) => core::str::from_utf8(bytes).ok(),
22 ResponseBody::Empty => Some(""),
23 }
24 }
25
26 #[must_use]
28 pub fn as_bytes(&self) -> &[u8] {
29 match self {
30 ResponseBody::Text(s) => s.as_bytes(),
31 ResponseBody::Binary(bytes) => bytes,
32 ResponseBody::Empty => &[],
33 }
34 }
35
36 #[must_use]
38 pub fn is_empty(&self) -> bool {
39 match self {
40 ResponseBody::Text(s) => s.is_empty(),
41 ResponseBody::Binary(bytes) => bytes.is_empty(),
42 ResponseBody::Empty => true,
43 }
44 }
45
46 #[must_use]
48 pub fn len(&self) -> usize {
49 match self {
50 ResponseBody::Text(s) => s.len(),
51 ResponseBody::Binary(bytes) => bytes.len(),
52 ResponseBody::Empty => 0,
53 }
54 }
55}
56
57pub struct HttpResponse<'a> {
63 pub status_code: StatusCode,
65 pub headers: Vec<HttpHeader<'a>, 16>,
67 pub body: ResponseBody<'a>,
69}
70
71impl HttpResponse<'_> {
72 #[must_use]
74 pub fn get_header(&self, name: &str) -> Option<&str> {
75 self.headers
76 .iter()
77 .find(|h| h.name.eq_ignore_ascii_case(name))
78 .map(|h| h.value)
79 }
80
81 #[must_use]
83 pub fn content_type(&self) -> Option<&str> {
84 self.get_header("Content-Type")
85 }
86
87 #[must_use]
89 pub fn content_length(&self) -> Option<usize> {
90 self.get_header("Content-Length")?.parse().ok()
91 }
92
93 #[must_use]
95 pub fn is_success(&self) -> bool {
96 self.status_code.is_success()
97 }
98
99 #[must_use]
101 pub fn is_client_error(&self) -> bool {
102 self.status_code.is_client_error()
103 }
104
105 #[must_use]
107 pub fn is_server_error(&self) -> bool {
108 self.status_code.is_server_error()
109 }
110
111 #[must_use]
113 pub fn build_bytes<const MAX_RESPONSE_SIZE: usize>(&self) -> Vec<u8, MAX_RESPONSE_SIZE> {
114 let mut bytes = Vec::new();
115
116 write_status_line(&mut bytes, self.status_code);
118
119 for header in &self.headers {
121 let _ = bytes.extend_from_slice(header.name.as_bytes());
122 let _ = bytes.extend_from_slice(b": ");
123 let _ = bytes.extend_from_slice(header.value.as_bytes());
124 let _ = bytes.extend_from_slice(b"\r\n");
125 }
126
127 let body_bytes = self.body.as_bytes();
129 if !body_bytes.is_empty() {
130 let _ = bytes.extend_from_slice(b"Content-Length: ");
131 write_decimal_to_buffer(&mut bytes, body_bytes.len());
132 let _ = bytes.extend_from_slice(b"\r\n");
133 }
134
135 let _ = bytes.extend_from_slice(b"\r\n");
137
138 let _ = bytes.extend_from_slice(body_bytes);
140
141 bytes
142 }
143}
144
145fn write_status_line<const MAX_RESPONSE_SIZE: usize>(
147 bytes: &mut Vec<u8, MAX_RESPONSE_SIZE>,
148 status_code: StatusCode,
149) {
150 let _ = bytes.extend_from_slice(b"HTTP/1.1 ");
152
153 write_decimal_to_buffer(bytes, status_code.as_u16() as usize);
155
156 let _ = bytes.push(b' ');
158 let _ = bytes.extend_from_slice(status_code.text().as_bytes());
159 let _ = bytes.extend_from_slice(b"\r\n");
160}
161
162fn write_decimal_to_buffer<const MAX_RESPONSE_SIZE: usize>(
164 bytes: &mut Vec<u8, MAX_RESPONSE_SIZE>,
165 mut num: usize,
166) {
167 if num == 0 {
168 let _ = bytes.push(b'0');
169 return;
170 }
171
172 let mut digits = [0u8; 10];
173 let mut i = 0;
174
175 while num > 0 {
176 #[allow(clippy::cast_possible_truncation)]
177 {
178 digits[i] = (num % 10) as u8 + b'0';
179 }
180 num /= 10;
181 i += 1;
182 }
183
184 for j in (0..i).rev() {
186 let _ = bytes.push(digits[j]);
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::header::HttpHeader;
194 use heapless::Vec;
195
196 #[test]
197 fn test_response_body_as_str_and_bytes() {
198 let text = ResponseBody::Text("hello");
199 assert_eq!(text.as_str(), Some("hello"));
200 assert_eq!(text.as_bytes(), b"hello");
201 let bin = ResponseBody::Binary(b"bin");
202 assert_eq!(bin.as_str(), Some("bin"));
203 assert_eq!(bin.as_bytes(), b"bin");
204 let empty = ResponseBody::Empty;
205 assert_eq!(empty.as_str(), Some(""));
206 assert_eq!(empty.as_bytes(), b"");
207 }
208
209 #[test]
210 fn test_response_body_is_empty_and_len() {
211 let text = ResponseBody::Text("");
212 assert!(text.is_empty());
213 assert_eq!(text.len(), 0);
214 let bin = ResponseBody::Binary(b"");
215 assert!(bin.is_empty());
216 assert_eq!(bin.len(), 0);
217 let nonempty = ResponseBody::Text("abc");
218 assert!(!nonempty.is_empty());
219 assert_eq!(nonempty.len(), 3);
220 }
221
222 #[test]
223 fn test_http_response_get_header() {
224 let mut headers: Vec<HttpHeader, 16> = Vec::new();
225 headers
226 .push(HttpHeader {
227 name: "Content-Type",
228 value: "text/plain",
229 })
230 .unwrap();
231 let resp = HttpResponse {
232 status_code: StatusCode::Ok,
233 headers,
234 body: ResponseBody::Empty,
235 };
236 assert_eq!(resp.get_header("content-type"), Some("text/plain"));
237 assert_eq!(resp.get_header("missing"), None);
238 }
239
240 #[test]
241 fn test_build_http_response_ok() {
242 let mut headers = Vec::new();
243 let _ = headers.push(HttpHeader::new("Content-Type", "text/html"));
244 let _ = headers.push(HttpHeader::new("Content-Length", "12"));
245
246 let response = HttpResponse {
247 status_code: StatusCode::Ok,
248 headers,
249 body: ResponseBody::Text("Hello World!"),
250 };
251
252 let bytes = response.build_bytes::<4096>();
253 let response_str = core::str::from_utf8(&bytes).unwrap();
254
255 assert!(response_str.starts_with("HTTP/1.1 200 OK\r\n"));
256 assert!(response_str.contains("Content-Type: text/html\r\n"));
257 assert!(response_str.contains("Content-Length: 12\r\n"));
258 assert!(response_str.ends_with("Hello World!"));
259 }
260
261 #[test]
262 fn test_build_http_response_not_found() {
263 let response = HttpResponse {
264 status_code: StatusCode::NotFound,
265 headers: Vec::new(),
266 body: ResponseBody::Text("Not Found"),
267 };
268
269 let bytes = response.build_bytes::<4096>();
270 let response_str = core::str::from_utf8(&bytes).unwrap();
271
272 assert!(response_str.starts_with("HTTP/1.1 404 Not Found\r\n"));
273 assert!(response_str.contains("Content-Length: 9\r\n"));
274 assert!(response_str.ends_with("Not Found"));
275 }
276
277 #[test]
278 fn test_build_http_response_empty_body() {
279 let response = HttpResponse {
280 status_code: StatusCode::NoContent,
281 headers: Vec::new(),
282 body: ResponseBody::Empty,
283 };
284
285 let bytes = response.build_bytes::<4096>();
286 let response_str = core::str::from_utf8(&bytes).unwrap();
287
288 assert!(response_str.starts_with("HTTP/1.1 204 No Content\r\n"));
289 assert!(!response_str.contains("Content-Length"));
290 assert!(response_str.ends_with("\r\n\r\n"));
291 }
292
293 #[test]
294 fn test_build_http_response_binary_body() {
295 let binary_data = b"\x00\x01\x02\x03";
296 let response = HttpResponse {
297 status_code: StatusCode::Ok,
298 headers: Vec::new(),
299 body: ResponseBody::Binary(binary_data),
300 };
301
302 let bytes = response.build_bytes::<4096>();
303
304 assert!(bytes.ends_with(binary_data));
306
307 let response_str = core::str::from_utf8(&bytes[..bytes.len() - binary_data.len()]).unwrap();
309 assert!(response_str.contains("Content-Length: 4\r\n"));
310 }
311
312 #[test]
313 fn test_write_decimal_to_buffer() {
314 let mut bytes: Vec<u8, 64> = Vec::new();
315
316 write_decimal_to_buffer(&mut bytes, 0);
318 assert_eq!(bytes, b"0");
319
320 bytes.clear();
322 write_decimal_to_buffer(&mut bytes, 5);
323 assert_eq!(bytes, b"5");
324
325 bytes.clear();
327 write_decimal_to_buffer(&mut bytes, 42);
328 assert_eq!(bytes, b"42");
329
330 bytes.clear();
331 write_decimal_to_buffer(&mut bytes, 123);
332 assert_eq!(bytes, b"123");
333
334 bytes.clear();
335 write_decimal_to_buffer(&mut bytes, 9999);
336 assert_eq!(bytes, b"9999");
337 }
338
339 #[test]
340 fn test_write_status_line() {
341 let mut bytes: Vec<u8, 64> = Vec::new();
342
343 write_status_line(&mut bytes, StatusCode::Ok);
345 assert_eq!(bytes, b"HTTP/1.1 200 OK\r\n");
346
347 bytes.clear();
348 write_status_line(&mut bytes, StatusCode::NotFound);
349 assert_eq!(bytes, b"HTTP/1.1 404 Not Found\r\n");
350
351 bytes.clear();
352 write_status_line(&mut bytes, StatusCode::InternalServerError);
353 assert_eq!(bytes, b"HTTP/1.1 500 Internal Server Error\r\n");
354
355 bytes.clear();
356 write_status_line(&mut bytes, StatusCode::Created);
357 assert_eq!(bytes, b"HTTP/1.1 201 Created\r\n");
358 }
359
360 #[test]
361 fn test_content_length_calculation() {
362 let long_text_a = "A".repeat(100);
364 let long_text_b = "B".repeat(999);
365 let test_cases = [
366 ("", 0),
367 ("a", 1),
368 ("hello", 5),
369 ("0123456789", 10),
370 ("Lorem ipsum dolor sit amet", 26),
371 (long_text_a.as_str(), 100),
372 (long_text_b.as_str(), 999),
373 ];
374
375 for (body_text, expected_len) in &test_cases {
376 let response = HttpResponse {
377 status_code: StatusCode::Ok,
378 headers: Vec::new(),
379 body: ResponseBody::Text(body_text),
380 };
381
382 let bytes = response.build_bytes::<4096>();
383 let response_str = core::str::from_utf8(&bytes).unwrap();
384
385 if *expected_len > 0 {
386 let expected_header = format!("Content-Length: {expected_len}\r\n");
387 assert!(
388 response_str.contains(&expected_header),
389 "Expected '{expected_header}' in response for body length {expected_len}"
390 );
391 }
392 }
393 }
394}