Skip to main content

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}