rustlavel_http/
response.rs1use crate::cookie::Cookie;
2use crate::headers::Headers;
3use crate::status::Status;
4use rustlavel_core::{Error, Json};
5
6#[derive(Clone)]
8pub struct Response {
9 pub status: Status,
10 pub headers: Headers,
11 pub body: Vec<u8>,
12 pub(crate) upgrade: Option<crate::upgrade::Upgrader>,
14}
15
16impl std::fmt::Debug for Response {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 f.debug_struct("Response")
19 .field("status", &self.status)
20 .field("headers", &self.headers)
21 .field("body_len", &self.body.len())
22 .field("upgrades", &self.upgrade.is_some())
23 .finish()
24 }
25}
26
27impl Response {
28 pub fn new(status: Status) -> Self {
29 Response { status, headers: Headers::new(), body: Vec::new(), upgrade: None }
30 }
31
32 pub fn upgrading(mut self, upgrade: impl crate::upgrade::Upgrade) -> Self {
37 self.status = Status(101);
38 self.upgrade = Some(std::sync::Arc::new(upgrade));
39 self
40 }
41
42 pub fn streaming(mut self, body: impl crate::upgrade::Upgrade) -> Self {
51 self.upgrade = Some(std::sync::Arc::new(body));
52 self
53 }
54
55 pub fn upgrades(&self) -> bool {
57 self.upgrade.is_some()
58 }
59
60 pub fn take_upgrade(&mut self) -> Option<crate::upgrade::Upgrader> {
65 self.upgrade.take()
66 }
67
68 pub fn ok() -> Self {
69 Response::new(Status::OK)
70 }
71
72 pub fn no_content() -> Self {
73 Response::new(Status::NO_CONTENT)
74 }
75
76 pub fn not_found() -> Self {
77 Response::new(Status::NOT_FOUND).with_html("<h1>404 Not Found</h1>")
78 }
79
80 pub fn redirect(location: impl Into<String>) -> Self {
83 Response::new(Status::FOUND).with_header("location", location)
84 }
85
86 pub fn see_other(location: impl Into<String>) -> Self {
87 Response::new(Status::SEE_OTHER).with_header("location", location)
88 }
89
90 pub fn json(value: impl Into<Json>) -> Self {
91 Response::ok().with_json(value)
92 }
93
94 pub fn html(body: impl Into<String>) -> Self {
95 Response::ok().with_html(body)
96 }
97
98 pub fn text(body: impl Into<String>) -> Self {
99 Response::ok().with_text(body)
100 }
101
102 pub fn with_status(mut self, status: impl Into<Status>) -> Self {
103 self.status = status.into();
104 self
105 }
106
107 pub fn with_header(mut self, name: &str, value: impl Into<String>) -> Self {
108 self.headers.set(name, value);
109 self
110 }
111
112 pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
113 self.body = body.into();
114 self
115 }
116
117 pub fn with_json(self, value: impl Into<Json>) -> Self {
118 self.with_header("content-type", "application/json; charset=utf-8")
119 .with_body(value.into().to_string())
120 }
121
122 pub fn with_html(self, body: impl Into<String>) -> Self {
123 self.with_header("content-type", "text/html; charset=utf-8").with_body(body.into())
124 }
125
126 pub fn with_text(self, body: impl Into<String>) -> Self {
127 self.with_header("content-type", "text/plain; charset=utf-8").with_body(body.into())
128 }
129
130 pub fn with_cookie(mut self, cookie: Cookie) -> Self {
131 self.headers.append("set-cookie", cookie.to_header());
132 self
133 }
134
135 pub fn without_cookie(self, name: &str) -> Self {
137 self.with_cookie(Cookie::forget(name))
138 }
139
140 pub fn body_string(&self) -> String {
141 String::from_utf8_lossy(&self.body).into_owned()
142 }
143
144 pub fn to_bytes(&self, include_body: bool) -> Vec<u8> {
149 let bodyless = self.status.is_bodyless();
150 let body: &[u8] = if bodyless { &[] } else { &self.body };
151
152 let mut head = format!("HTTP/1.1 {} {}\r\n", self.status.code(), self.status.reason());
153 for (name, value) in self.headers.iter() {
154 if name == "content-length" {
155 continue;
156 }
157 head.push_str(&format!("{name}: {value}\r\n"));
158 }
159 if !bodyless && self.upgrade.is_none() {
164 head.push_str(&format!("content-length: {}\r\n", body.len()));
165 }
166 head.push_str("\r\n");
167
168 let mut out = head.into_bytes();
169 if include_body && !bodyless {
170 out.extend_from_slice(body);
171 }
172 out
173 }
174}
175
176impl Default for Response {
177 fn default() -> Self {
178 Response::ok()
179 }
180}
181
182pub trait IntoResponse {
187 fn into_response(self) -> Response;
188}
189
190impl IntoResponse for Response {
191 fn into_response(self) -> Response {
192 self
193 }
194}
195
196impl IntoResponse for Status {
197 fn into_response(self) -> Response {
198 Response::new(self)
199 }
200}
201
202impl IntoResponse for String {
203 fn into_response(self) -> Response {
204 Response::html(self)
205 }
206}
207
208impl IntoResponse for &str {
209 fn into_response(self) -> Response {
210 Response::html(self.to_string())
211 }
212}
213
214impl IntoResponse for Json {
215 fn into_response(self) -> Response {
216 Response::json(self)
217 }
218}
219
220impl IntoResponse for () {
221 fn into_response(self) -> Response {
222 Response::no_content()
223 }
224}
225
226impl<T: IntoResponse> IntoResponse for (u16, T) {
227 fn into_response(self) -> Response {
228 let (status, inner) = self;
229 inner.into_response().with_status(status)
230 }
231}
232
233impl<T: IntoResponse> IntoResponse for Option<T> {
236 fn into_response(self) -> Response {
237 match self {
238 Some(value) => value.into_response(),
239 None => Response::not_found(),
240 }
241 }
242}
243
244impl IntoResponse for Error {
247 fn into_response(self) -> Response {
248 crate::error_page::response_for(&self)
249 }
250}
251
252impl<T: IntoResponse, E: IntoResponse> IntoResponse for Result<T, E> {
258 fn into_response(self) -> Response {
259 match self {
260 Ok(value) => value.into_response(),
261 Err(error) => error.into_response(),
262 }
263 }
264}
265
266impl IntoResponse for std::io::Error {
269 fn into_response(self) -> Response {
270 crate::error_page::response_for(&Error::Io(self))
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn serializes_a_wire_response() {
280 let response = Response::json(Json::object([("ok", true.into())]));
281 let wire = String::from_utf8(response.to_bytes(true)).unwrap();
282
283 assert!(wire.starts_with("HTTP/1.1 200 OK\r\n"));
284 assert!(wire.contains("content-type: application/json; charset=utf-8\r\n"));
285 assert!(wire.contains("content-length: 11\r\n"));
286 assert!(wire.ends_with("\r\n\r\n{\"ok\":true}"));
287 }
288
289 #[test]
290 fn head_requests_keep_the_length_but_drop_the_body() {
291 let response = Response::text("hello");
292 let wire = String::from_utf8(response.to_bytes(false)).unwrap();
293
294 assert!(wire.contains("content-length: 5\r\n"));
295 assert!(wire.ends_with("\r\n\r\n"));
296 }
297
298 #[test]
299 fn bodyless_statuses_send_no_content_length() {
300 let wire = String::from_utf8(Response::no_content().to_bytes(true)).unwrap();
301 assert!(!wire.contains("content-length"));
302 }
303
304 #[test]
305 fn repeated_cookies_each_get_their_own_header() {
306 let response = Response::ok()
307 .with_cookie(Cookie::new("a", "1"))
308 .with_cookie(Cookie::new("b", "2"));
309 let wire = String::from_utf8(response.to_bytes(true)).unwrap();
310
311 assert_eq!(wire.matches("set-cookie:").count(), 2);
312 }
313
314 #[test]
315 fn common_return_types_convert() {
316 assert_eq!("hi".into_response().status, Status::OK);
317 assert_eq!(().into_response().status, Status::NO_CONTENT);
318 assert_eq!(Option::<String>::None.into_response().status, Status::NOT_FOUND);
319 assert_eq!((201, "made").into_response().status, Status::CREATED);
320
321 let failed: Result<&str, Error> = Err(Error::msg("boom"));
322 assert!(failed.into_response().status.is_error());
323 }
324}