coa_website/
response.rs

1use std::collections::HashMap;
2use std::io::Write;
3
4use httpdate::HttpDate;
5
6use crate::http::StatusCode;
7
8/// Represents an HTTP Response send to a client
9pub struct HttpResponse {
10    /// HTTP status code
11    pub status: StatusCode,
12    /// HTTP headers
13    pub headers: HashMap<String, String>,
14    /// Body
15    pub body: Option<Vec<u8>>,
16}
17
18impl HttpResponse {
19
20    /// Converts the response to bytes to be sent in the TCPStream
21    pub fn to_bytes(&self) -> Vec<u8> {
22        let mut buffer = Vec::new();
23        let _ = write!(
24            &mut buffer,
25            "HTTP/1.1 {} {}\r\n",
26            self.status.code(),
27            self.status.reason()
28        );
29
30        for (k, v) in &self.headers {
31            let _ = write!(&mut buffer, "{}: {}\r\n", k, v);
32        }
33        buffer.extend_from_slice(b"\r\n");
34
35        match(self.headers.get("Transfer-Encoding").map(|v| v.eq_ignore_ascii_case("chunked")), &self.body) {
36            (Some(true), Some(body)) => {
37                if !body.is_empty() {
38                    let _ = write!(&mut buffer, "{:X}\r\n", body.len());
39                    buffer.extend_from_slice(body);
40                    buffer.extend_from_slice(b"\r\n");
41                }
42                buffer.extend_from_slice(b"0\r\n\r\n");
43            },
44            (_, Some(body)) => buffer.extend_from_slice(body),
45            _ => {}
46        }
47
48        buffer
49    }
50
51    /// Create a new builder to construct a HttpResponse
52    pub fn builder() -> HttpResponseBuilder {
53        HttpResponseBuilder::default()
54    }
55
56    /// Create a preconfigured builder for 200 OK response
57    pub fn ok(content_type: &str) -> HttpResponseBuilder {
58        HttpResponse::builder().with_status(StatusCode::Ok).with_content_type(content_type)
59    }
60
61    /// Create a preconfigured builder for 404 Not Found response
62    pub fn not_found() -> HttpResponseBuilder {
63        HttpResponse::builder().with_status(StatusCode::NotFound).with_body(vec![])
64    }
65
66    /// Create a preconfigured builder for 405 Method Not Allowed response
67    pub fn method_not_allowed() -> HttpResponseBuilder {
68        HttpResponse::builder().with_status(StatusCode::MethodNotAllowed).with_body(vec![])
69    }
70
71    /// Create a preconfigured builder for 500 Internal Server Error
72    pub fn internal_server_error() -> HttpResponseBuilder {
73        HttpResponse::builder().with_status(StatusCode::InternalServerError).with_body(vec![])
74    }
75}
76
77/// Builder pattern to build a `HttpResponse`
78#[derive(Debug, Default)]
79pub struct HttpResponseBuilder {
80    pub status: Option<StatusCode>,
81    pub headers: HashMap<String, String>,
82    pub body: Option<Vec<u8>>,
83}
84
85
86impl HttpResponseBuilder {
87
88    /// Sets the status code of the response
89    pub fn with_status(mut self, status: StatusCode) -> Self {
90        self.status = Some(status);
91        self
92    }
93
94    /// Adds an header to the response
95    pub fn with_header(mut self, key: &str, value: &str) -> Self {
96        self.headers.insert(key.to_string(), value.to_string());
97        self
98    }
99
100    /// Sets the Content-Type header
101    pub fn with_content_type(self, content_type: &str) -> Self {
102        self.with_header("Content-Type", content_type)
103    }
104
105    /// Sets the response body
106    pub fn with_body(mut self, body: Vec<u8>) -> Self {
107        self.body = Some(body);
108        self
109    }
110
111    /// Build the response into `HttpResponse`, adding default headers if missing
112    /// 
113    /// # Errors
114    /// Returns an error if the status is not specified
115    pub fn build(mut self) -> Result<HttpResponse, &'static str> {
116        if !self.headers.contains_key("Server") {
117            self.headers.insert("Server".to_string(), "CoaWebserver/1.0".to_string());
118        }
119        if !self.headers.contains_key("Date") {
120            self.headers.insert("Date".to_string(), format!("{}", HttpDate::from(std::time::SystemTime::now())));
121        }
122
123        if !self.headers.contains_key("Transfer-Encoding") && !self.headers.contains_key("Content-Length") {
124            if let Some(ref b) = self.body {
125                self.headers.insert("Content-Length".to_string(), b.len().to_string());
126            } else {
127                self.headers.insert("Transfer-Encoding".to_string(), "chunked".to_string());
128            }
129        }
130
131        match self.status.take() {
132            Some(status) => Ok(HttpResponse { status, headers: self.headers, body: self.body}),
133            None => Err("Status is not specified.")
134        }
135    }
136
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_response_to_bytes() {
145        let res = HttpResponse::ok("text/plain").with_body(b"Hello".to_vec()).build().unwrap();
146        let res_str = String::from_utf8(res.to_bytes()).unwrap();
147        assert!(res_str.contains("HTTP/1.1 200 OK"));
148        assert!(res_str.contains("Content-Type: text/plain"));
149        assert!(res_str.contains("Content-Length: 5"));
150        assert!(res_str.contains("Hello"));
151    }
152
153    #[test]
154    fn test_chunked_transfer_encoding() {
155        let response = HttpResponse::builder()
156            .with_status(StatusCode::Ok)
157            .with_header("Transfer-Encoding", "chunked")
158            .with_body(b"HelloChunk".to_vec())
159            .build()
160            .unwrap();
161
162        let bytes = response.to_bytes();
163        let output = String::from_utf8_lossy(&bytes);
164
165        assert!(output.contains("Transfer-Encoding: chunked"));
166        assert!(!output.contains("Content-Length"));
167        assert!(output.contains("A\r\nHelloChunk\r\n0\r\n\r\n"));
168    }
169
170    #[test]
171    fn test_chunked_empty_body() {
172        let res = HttpResponse::builder()
173            .with_status(StatusCode::Ok)
174            .with_header("Transfer-Encoding", "chunked")
175            .with_body(vec![])
176            .build()
177            .unwrap();
178
179        let bytes = res.to_bytes();
180        let out = String::from_utf8_lossy(&bytes);
181        assert!(out.contains("0\r\n\r\n"));
182    }
183
184    #[test]
185    fn test_auto_headers_inserted() {
186        let response = HttpResponse::builder()
187            .with_status(StatusCode::Ok)
188            .with_body(b"Ping".to_vec())
189            .build()
190            .unwrap();
191
192        let bytes = response.to_bytes();
193        let s = String::from_utf8_lossy(&bytes);
194        assert!(s.contains("Server: CoaWebserver/1.0"));
195        assert!(s.contains("Date:"));
196    }
197
198    #[test]
199    fn test_missing_status_should_fail() {
200        let result = HttpResponse::builder().with_body(b"Missing".to_vec()).build();
201        assert!(result.is_err());
202    }
203
204}