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 /// Insert (or replace) a header, returning `self` for chaining.
142 ///
143 /// ```
144 /// use churust_core::Response;
145 /// use http::{header::LOCATION, HeaderValue};
146 ///
147 /// let res = Response::new(http::StatusCode::FOUND)
148 /// .with_header(LOCATION, HeaderValue::from_static("/login"));
149 /// assert_eq!(res.headers.get(LOCATION).unwrap(), "/login");
150 /// ```
151 pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
152 self.headers.insert(name, value);
153 self
154 }
155}
156
157/// Convert a handler return value into a [`Response`].
158///
159/// This is what lets handlers return ergonomic values instead of building a
160/// [`Response`] by hand. The crate implements it for the common cases:
161///
162/// - `()` — an empty `200 OK`.
163/// - `&'static str` / [`String`] — a `text/plain` body.
164/// - [`StatusCode`] — an empty body with that status.
165/// - `(StatusCode, T)` where `T: IntoResponse` — `T`'s response with the status
166/// overridden.
167/// - [`Error`] — rendered using its own status, message, and
168/// headers.
169/// - [`Result<T>`](crate::Result) where `T: IntoResponse` — the `Ok` value's
170/// response, or the `Err`'s.
171/// - [`Response`] — returned unchanged (identity).
172///
173/// Implement it for your own types to make them returnable from handlers.
174///
175/// ```
176/// use churust_core::{IntoResponse, Response};
177/// use http::StatusCode;
178///
179/// // The `(StatusCode, &str)` tuple impl in action:
180/// let res = (StatusCode::CREATED, "made").into_response();
181/// assert_eq!(res.status, StatusCode::CREATED);
182/// assert_eq!(res.body.as_slice(), Some(&b"made"[..]));
183/// ```
184pub trait IntoResponse {
185 /// Consume `self` and produce the [`Response`] to send.
186 fn into_response(self) -> Response;
187}
188
189impl IntoResponse for Response {
190 fn into_response(self) -> Response {
191 self
192 }
193}
194
195impl IntoResponse for () {
196 fn into_response(self) -> Response {
197 Response::new(StatusCode::OK)
198 }
199}
200
201impl IntoResponse for &'static str {
202 fn into_response(self) -> Response {
203 Response::text(self)
204 }
205}
206
207impl IntoResponse for String {
208 fn into_response(self) -> Response {
209 Response::text(self)
210 }
211}
212
213impl IntoResponse for StatusCode {
214 fn into_response(self) -> Response {
215 Response::new(self)
216 }
217}
218
219impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
220 fn into_response(self) -> Response {
221 let (status, inner) = self;
222 inner.into_response().with_status(status)
223 }
224}
225
226impl IntoResponse for Error {
227 fn into_response(self) -> Response {
228 let mut res = Response::text(self.message().to_string()).with_status(self.status());
229 for (name, value) in self.response_headers() {
230 res.headers.insert(name.clone(), value.clone());
231 }
232 res
233 }
234}
235
236/// A `Result` whose `Ok`/`Err` both render to a response.
237impl<T: IntoResponse> IntoResponse for crate::error::Result<T> {
238 fn into_response(self) -> Response {
239 match self {
240 Ok(v) => v.into_response(),
241 Err(e) => e.into_response(),
242 }
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn text_sets_content_type_and_body() {
252 let r = Response::text("hi");
253 assert_eq!(r.status, StatusCode::OK);
254 assert_eq!(r.body, Bytes::from("hi"));
255 assert_eq!(
256 r.headers.get(CONTENT_TYPE).unwrap(),
257 "text/plain; charset=utf-8"
258 );
259 }
260
261 #[test]
262 fn status_tuple_overrides_status() {
263 let r = (StatusCode::CREATED, "made").into_response();
264 assert_eq!(r.status, StatusCode::CREATED);
265 assert_eq!(r.body, Bytes::from("made"));
266 }
267
268 #[test]
269 fn error_renders_with_its_status() {
270 let r = Error::bad_request("x").into_response();
271 assert_eq!(r.status, StatusCode::BAD_REQUEST);
272 }
273}