churust_core/response.rs
1//! The buffered [`Response`] type and the [`IntoResponse`] conversion trait.
2//!
3//! A [`Response`] is the in-memory result of handling a request: a status, a
4//! header map, and a body buffered as [`Bytes`]. Handlers rarely build one by
5//! hand — instead they return any value implementing [`IntoResponse`] (a
6//! `&str`, `String`, [`StatusCode`], a `(StatusCode, T)` tuple, an
7//! [`Error`], or a [`Result`](crate::error::Result)) and the framework
8//! converts it for you.
9
10use crate::body::Body;
11use crate::error::Error;
12use bytes::Bytes;
13use http::header::{HeaderName, CONTENT_TYPE};
14use http::{HeaderMap, HeaderValue, StatusCode};
15
16/// A fully-buffered HTTP response: status line, headers, and an in-memory body.
17///
18/// The three fields are public so middleware can post-process a response (for
19/// example inserting a header). Construct one with [`Response::new`] (status
20/// only), [`Response::text`] (a `text/plain` body), or [`Response::bytes`] (an
21/// arbitrary content type), then refine it with the chainable
22/// [`with_status`](Response::with_status) and
23/// [`with_header`](Response::with_header) builders. Most handlers return an
24/// [`IntoResponse`] value instead of building this directly.
25///
26/// ```
27/// use churust_core::Response;
28/// use http::StatusCode;
29///
30/// let res = Response::text("created").with_status(StatusCode::CREATED);
31/// assert_eq!(res.status, StatusCode::CREATED);
32/// assert_eq!(res.body.as_slice(), Some(&b"created"[..]));
33/// ```
34#[derive(Debug)]
35pub struct Response {
36 /// The HTTP status code of the response.
37 pub status: StatusCode,
38 /// The response headers.
39 pub headers: HeaderMap,
40 /// The response body — buffered bytes or a lazy stream.
41 pub body: Body,
42}
43
44impl Response {
45 /// Create an empty-bodied response with the given `status` and no headers.
46 ///
47 /// ```
48 /// use churust_core::Response;
49 /// use http::StatusCode;
50 ///
51 /// let res = Response::new(StatusCode::NO_CONTENT);
52 /// assert_eq!(res.status, StatusCode::NO_CONTENT);
53 /// assert!(res.body.is_empty());
54 /// ```
55 pub fn new(status: StatusCode) -> Self {
56 Self {
57 status,
58 headers: HeaderMap::new(),
59 body: Body::empty(),
60 }
61 }
62
63 /// Create a `200 OK` response with a `text/plain; charset=utf-8` body.
64 ///
65 /// ```
66 /// use churust_core::Response;
67 /// use http::StatusCode;
68 ///
69 /// let res = Response::text("hello");
70 /// assert_eq!(res.status, StatusCode::OK);
71 /// assert_eq!(
72 /// res.headers.get(http::header::CONTENT_TYPE).unwrap(),
73 /// "text/plain; charset=utf-8"
74 /// );
75 /// ```
76 pub fn text(body: impl Into<String>) -> Self {
77 let mut r = Self::new(StatusCode::OK);
78 r.headers.insert(
79 CONTENT_TYPE,
80 HeaderValue::from_static("text/plain; charset=utf-8"),
81 );
82 r.body = Body::from(body.into());
83 r
84 }
85
86 /// Create a `200 OK` response with a raw byte body and an explicit
87 /// `Content-Type`. Use this for non-text payloads (JSON, images, etc.); the
88 /// `content_type` must be a `'static` string.
89 ///
90 /// ```
91 /// use churust_core::Response;
92 ///
93 /// let res = Response::bytes("application/octet-stream", vec![1u8, 2, 3]);
94 /// assert_eq!(res.body.as_slice(), Some(&[1u8, 2, 3][..]));
95 /// assert_eq!(
96 /// res.headers.get(http::header::CONTENT_TYPE).unwrap(),
97 /// "application/octet-stream"
98 /// );
99 /// ```
100 pub fn bytes(content_type: &'static str, body: impl Into<Bytes>) -> Self {
101 let mut r = Self::new(StatusCode::OK);
102 r.headers
103 .insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
104 r.body = Body::from(body.into());
105 r
106 }
107
108 /// Create a `200 OK` response whose body is produced lazily from `stream`,
109 /// with an explicit `Content-Type`. Use for large or dynamic payloads.
110 ///
111 /// ```
112 /// use churust_core::{Body, Response};
113 /// use bytes::Bytes;
114 ///
115 /// let chunks = futures_util::stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from("hi"))]);
116 /// let res = Response::stream("text/plain", Body::from_stream(chunks));
117 /// assert!(res.body.as_bytes().is_none());
118 /// ```
119 pub fn stream(content_type: &'static str, body: Body) -> Self {
120 let mut r = Self::new(StatusCode::OK);
121 r.headers
122 .insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
123 r.body = body;
124 r
125 }
126
127 /// Override the status code, returning `self` for chaining.
128 ///
129 /// ```
130 /// use churust_core::Response;
131 /// use http::StatusCode;
132 ///
133 /// let res = Response::text("teapot").with_status(StatusCode::IM_A_TEAPOT);
134 /// assert_eq!(res.status, StatusCode::IM_A_TEAPOT);
135 /// ```
136 pub fn with_status(mut self, status: StatusCode) -> Self {
137 self.status = status;
138 self
139 }
140
141 /// Append a `Set-Cookie` header.
142 ///
143 /// Appends rather than replaces, so several cookies each get their own
144 /// header — which is what the spec requires; folding them into one comma-
145 /// separated value does not work in practice.
146 pub fn with_cookie(mut self, cookie: crate::cookie::Cookie) -> Self {
147 if let Ok(v) = HeaderValue::from_str(&cookie.to_header_value()) {
148 self.headers.append(http::header::SET_COOKIE, v);
149 }
150 self
151 }
152
153 /// Insert (or replace) a header, returning `self` for chaining.
154 ///
155 /// ```
156 /// use churust_core::Response;
157 /// use http::{header::LOCATION, HeaderValue};
158 ///
159 /// let res = Response::new(http::StatusCode::FOUND)
160 /// .with_header(LOCATION, HeaderValue::from_static("/login"));
161 /// assert_eq!(res.headers.get(LOCATION).unwrap(), "/login");
162 /// ```
163 pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
164 self.headers.insert(name, value);
165 self
166 }
167}
168
169/// Convert a handler return value into a [`Response`].
170///
171/// This is what lets handlers return ergonomic values instead of building a
172/// [`Response`] by hand. The crate implements it for the common cases:
173///
174/// - `()` — an empty `200 OK`.
175/// - `&'static str` / [`String`] — a `text/plain` body.
176/// - [`StatusCode`] — an empty body with that status.
177/// - `(StatusCode, T)` where `T: IntoResponse` — `T`'s response with the status
178/// overridden.
179/// - [`Error`] — rendered using its own status, message, and
180/// headers.
181/// - [`Result<T>`](crate::Result) where `T: IntoResponse` — the `Ok` value's
182/// response, or the `Err`'s.
183/// - [`Response`] — returned unchanged (identity).
184///
185/// Implement it for your own types to make them returnable from handlers.
186///
187/// ```
188/// use churust_core::{IntoResponse, Response};
189/// use http::StatusCode;
190///
191/// // The `(StatusCode, &str)` tuple impl in action:
192/// let res = (StatusCode::CREATED, "made").into_response();
193/// assert_eq!(res.status, StatusCode::CREATED);
194/// assert_eq!(res.body.as_slice(), Some(&b"made"[..]));
195/// ```
196pub trait IntoResponse {
197 /// Consume `self` and produce the [`Response`] to send.
198 fn into_response(self) -> Response;
199}
200
201impl IntoResponse for Response {
202 fn into_response(self) -> Response {
203 self
204 }
205}
206
207impl IntoResponse for () {
208 fn into_response(self) -> Response {
209 Response::new(StatusCode::OK)
210 }
211}
212
213impl IntoResponse for &'static str {
214 fn into_response(self) -> Response {
215 Response::text(self)
216 }
217}
218
219impl IntoResponse for String {
220 fn into_response(self) -> Response {
221 Response::text(self)
222 }
223}
224
225impl IntoResponse for StatusCode {
226 fn into_response(self) -> Response {
227 Response::new(self)
228 }
229}
230
231impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
232 fn into_response(self) -> Response {
233 let (status, inner) = self;
234 inner.into_response().with_status(status)
235 }
236}
237
238impl IntoResponse for Error {
239 fn into_response(self) -> Response {
240 let mut res = Response::text(self.message().to_string()).with_status(self.status());
241 for (name, value) in self.response_headers() {
242 res.headers.insert(name.clone(), value.clone());
243 }
244 res
245 }
246}
247
248/// A `Result` whose `Ok`/`Err` both render to a response.
249impl<T: IntoResponse> IntoResponse for crate::error::Result<T> {
250 fn into_response(self) -> Response {
251 match self {
252 Ok(v) => v.into_response(),
253 Err(e) => e.into_response(),
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn text_sets_content_type_and_body() {
264 let r = Response::text("hi");
265 assert_eq!(r.status, StatusCode::OK);
266 assert_eq!(r.body, Bytes::from("hi"));
267 assert_eq!(
268 r.headers.get(CONTENT_TYPE).unwrap(),
269 "text/plain; charset=utf-8"
270 );
271 }
272
273 #[test]
274 fn status_tuple_overrides_status() {
275 let r = (StatusCode::CREATED, "made").into_response();
276 assert_eq!(r.status, StatusCode::CREATED);
277 assert_eq!(r.body, Bytes::from("made"));
278 }
279
280 #[test]
281 fn error_renders_with_its_status() {
282 let r = Error::bad_request("x").into_response();
283 assert_eq!(r.status, StatusCode::BAD_REQUEST);
284 }
285}