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    /// 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}