1use std::collections::HashMap;
2use std::io::Write;
3
4use httpdate::HttpDate;
5
6use crate::http::StatusCode;
7
8pub struct HttpResponse {
10 pub status: StatusCode,
12 pub headers: HashMap<String, String>,
14 pub body: Option<Vec<u8>>,
16}
17
18impl HttpResponse {
19
20 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 pub fn builder() -> HttpResponseBuilder {
53 HttpResponseBuilder::default()
54 }
55
56 pub fn ok(content_type: &str) -> HttpResponseBuilder {
58 HttpResponse::builder().with_status(StatusCode::Ok).with_content_type(content_type)
59 }
60
61 pub fn not_found() -> HttpResponseBuilder {
63 HttpResponse::builder().with_status(StatusCode::NotFound).with_body(vec![])
64 }
65
66 pub fn method_not_allowed() -> HttpResponseBuilder {
68 HttpResponse::builder().with_status(StatusCode::MethodNotAllowed).with_body(vec![])
69 }
70
71 pub fn internal_server_error() -> HttpResponseBuilder {
73 HttpResponse::builder().with_status(StatusCode::InternalServerError).with_body(vec![])
74 }
75}
76
77#[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 pub fn with_status(mut self, status: StatusCode) -> Self {
90 self.status = Some(status);
91 self
92 }
93
94 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 pub fn with_content_type(self, content_type: &str) -> Self {
102 self.with_header("Content-Type", content_type)
103 }
104
105 pub fn with_body(mut self, body: Vec<u8>) -> Self {
107 self.body = Some(body);
108 self
109 }
110
111 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}