Skip to main content

nanofish_client/
response.rs

1use crate::{HttpHeader, StatusCode};
2use heapless::Vec;
3
4/// HTTP Response body that can handle both text and binary data using zero-copy references
5#[derive(Debug)]
6pub enum ResponseBody<'a> {
7    /// Text content (UTF-8 encoded) - borrowed from the response buffer
8    Text(&'a str),
9    /// Binary content (raw bytes) - borrowed from the response buffer
10    Binary(&'a [u8]),
11    /// Empty body (e.g., for HEAD requests or 204 No Content)
12    Empty,
13}
14
15impl ResponseBody<'_> {
16    /// Try to get the body as a UTF-8 string
17    #[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    /// Get the body as raw bytes
27    #[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    /// Check if the body is empty
37    #[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    /// Get the length of the body in bytes
47    #[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
57/// HTTP Response struct with status code, headers and body
58///
59/// This struct represents the response received from an HTTP server.
60/// It contains the status code, headers, and the response body which can be
61/// either text or binary data using zero-copy references.
62pub struct HttpResponse<'a> {
63    /// The HTTP status code (e.g., 200 for OK, 404 for Not Found)
64    pub status_code: StatusCode,
65    /// A collection of response headers with both names and values
66    pub headers: Vec<HttpHeader<'a>, 16>,
67    /// The response body that can handle both text and binary data
68    pub body: ResponseBody<'a>,
69}
70
71impl HttpResponse<'_> {
72    /// Get a header value by name (case-insensitive)
73    #[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    /// Get the Content-Type header value
82    #[must_use]
83    pub fn content_type(&self) -> Option<&str> {
84        self.get_header("Content-Type")
85    }
86
87    /// Get the Content-Length header value as a number
88    #[must_use]
89    pub fn content_length(&self) -> Option<usize> {
90        self.get_header("Content-Length")?.parse().ok()
91    }
92
93    /// Check if the response indicates success (2xx status codes)
94    #[must_use]
95    pub fn is_success(&self) -> bool {
96        self.status_code.is_success()
97    }
98
99    /// Check if the response is a client error (4xx status codes)
100    #[must_use]
101    pub fn is_client_error(&self) -> bool {
102        self.status_code.is_client_error()
103    }
104
105    /// Check if the response is a server error (5xx status codes)
106    #[must_use]
107    pub fn is_server_error(&self) -> bool {
108        self.status_code.is_server_error()
109    }
110
111    /// Build HTTP response bytes from this `HttpResponse`
112    #[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        // Status line: HTTP/1.1 <code> <reason>\r\n
117        write_status_line(&mut bytes, self.status_code);
118
119        // Headers
120        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        // Content-Length header if body is present
128        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        // End of headers
136        let _ = bytes.extend_from_slice(b"\r\n");
137
138        // Body
139        let _ = bytes.extend_from_slice(body_bytes);
140
141        bytes
142    }
143}
144
145/// Write HTTP status line to the given buffer
146fn write_status_line<const MAX_RESPONSE_SIZE: usize>(
147    bytes: &mut Vec<u8, MAX_RESPONSE_SIZE>,
148    status_code: StatusCode,
149) {
150    // Write "HTTP/1.1 "
151    let _ = bytes.extend_from_slice(b"HTTP/1.1 ");
152
153    // Write status code as decimal
154    write_decimal_to_buffer(bytes, status_code.as_u16() as usize);
155
156    // Write " <reason>\r\n"
157    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
162/// Write a decimal number to the buffer
163fn 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    // Write digits in reverse order
185    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        // Check that the response contains the binary data at the end
305        assert!(bytes.ends_with(binary_data));
306
307        // Check that content-length is correct
308        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        // Test zero
317        write_decimal_to_buffer(&mut bytes, 0);
318        assert_eq!(bytes, b"0");
319
320        // Test single digit
321        bytes.clear();
322        write_decimal_to_buffer(&mut bytes, 5);
323        assert_eq!(bytes, b"5");
324
325        // Test multi-digit numbers
326        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        // Test common status codes
344        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        // Test various body lengths
363        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}