viz_core/
response.rs

1use http_body_util::Full;
2
3use crate::{Body, BoxError, Bytes, Response, Result, StatusCode, header};
4
5/// The [`Response`] Extension.
6pub trait ResponseExt: private::Sealed + Sized {
7    /// Creates a response with an empty body.
8    #[must_use]
9    fn empty() -> Response {
10        Response::new(Body::empty())
11    }
12
13    /// Get the size of this response's body.
14    fn content_length(&self) -> Option<u64>;
15
16    /// Get the media type of this response.
17    fn content_type(&self) -> Option<mime::Mime>;
18
19    /// Get a header with the key.
20    fn header<K, T>(&self, key: K) -> Option<T>
21    where
22        K: header::AsHeaderName,
23        T: std::str::FromStr;
24
25    /// The response was successful (status in the range [`200-299`][mdn]) or not.
26    ///
27    /// [mdn]: <https://developer.mozilla.org/en-US/docs/Web/API/Response/ok>
28    fn ok(&self) -> bool;
29
30    /// The response with the specified [`Content-Type`][mdn].
31    ///
32    /// [mdn]: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type>
33    fn with<B>(body: B, content_type: &str) -> Response
34    where
35        B: Into<Body>,
36    {
37        let mut resp = Response::new(body.into());
38        resp.headers_mut().insert(
39            header::CONTENT_TYPE,
40            header::HeaderValue::from_str(content_type).expect("Invalid content type"),
41        );
42        resp
43    }
44
45    /// The response with `text/plain; charset=utf-8` media type.
46    fn text<B>(body: B) -> Response
47    where
48        B: Into<Full<Bytes>>,
49    {
50        Self::with(body.into(), mime::TEXT_PLAIN_UTF_8.as_ref())
51    }
52
53    /// The response with `text/html; charset=utf-8` media type.
54    fn html<B>(body: B) -> Response
55    where
56        B: Into<Full<Bytes>>,
57    {
58        Self::with(body.into(), mime::TEXT_HTML_UTF_8.as_ref())
59    }
60
61    /// The response with `application/octet-stream` media type.
62    fn binary<B>(body: B) -> Response
63    where
64        B: Into<Full<Bytes>>,
65    {
66        Self::with(body.into(), mime::APPLICATION_OCTET_STREAM.as_ref())
67    }
68
69    /// The response with `application/javascript; charset=utf-8` media type.
70    ///
71    /// # Errors
72    ///
73    /// Throws an error if serialization fails.
74    #[cfg(feature = "json")]
75    fn json<T>(body: T) -> Result<Response, crate::types::PayloadError>
76    where
77        T: serde::Serialize,
78    {
79        let body = serde_json::to_vec(&body).map_err(crate::types::PayloadError::Json)?;
80        Ok(Response::with(
81            Full::from(body),
82            mime::APPLICATION_JSON.as_ref(),
83        ))
84    }
85
86    /// Responds to a stream.
87    fn stream<S, D, E>(stream: S) -> Response
88    where
89        S: futures_util::Stream<Item = Result<D, E>> + Send + 'static,
90        D: Into<Bytes> + 'static,
91        E: Into<BoxError> + 'static,
92    {
93        Response::new(Body::from_stream(stream))
94    }
95
96    /// Downloads transfers the file from path as an attachment.
97    #[cfg(feature = "fs")]
98    fn download<T>(
99        path: T,
100        name: Option<&str>,
101    ) -> impl std::future::Future<Output = Result<Self>> + Send
102    where
103        T: AsRef<std::path::Path> + Send;
104
105    /// The [`Content-Disposition`][mdn] header indicates if the content is expected to be
106    /// displayed inline in the browser, that is, as a Web page or as part of a Web page,
107    /// or as an attachment, that is downloaded and saved locally.
108    ///
109    /// [mdn]: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition>
110    fn attachment(value: &str) -> Response {
111        let val = header::HeaderValue::from_str(value)
112            .expect("content-disposition is not the correct value");
113        let mut resp = Response::default();
114        resp.headers_mut().insert(header::CONTENT_DISPOSITION, val);
115        resp
116    }
117
118    /// The [`Content-Location`][mdn] header indicates an alternate location for the returned data.
119    ///
120    /// [mdn]: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Location>
121    fn location<T>(location: T) -> Response
122    where
123        T: AsRef<str>,
124    {
125        let val = header::HeaderValue::try_from(location.as_ref())
126            .expect("location is not the correct value");
127        let mut resp = Response::default();
128        resp.headers_mut().insert(header::CONTENT_LOCATION, val);
129        resp
130    }
131
132    /// The response redirects to the specified URL.
133    ///
134    /// [mdn]: <https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect>
135    fn redirect<T>(url: T) -> Response
136    where
137        T: AsRef<str>,
138    {
139        let val =
140            header::HeaderValue::try_from(url.as_ref()).expect("url is not the correct value");
141        let mut resp = Response::default();
142        resp.headers_mut().insert(header::LOCATION, val);
143        resp
144    }
145
146    /// The response redirects to the specified URL and the status code.
147    ///
148    /// [mdn]: <https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect>
149    fn redirect_with_status<T>(url: T, status: StatusCode) -> Response
150    where
151        T: AsRef<str>,
152    {
153        assert!(status.is_redirection(), "not a redirection status code");
154
155        let mut resp = Self::redirect(url);
156        *resp.status_mut() = status;
157        resp
158    }
159
160    /// The response redirects to the [`303`][mdn].
161    ///
162    /// [mdn]: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303>
163    fn see_other<T>(url: T) -> Response
164    where
165        T: AsRef<str>,
166    {
167        Self::redirect_with_status(url, StatusCode::SEE_OTHER)
168    }
169
170    /// The response redirects to the [`307`][mdn].
171    ///
172    /// [mdn]: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307>
173    fn temporary<T>(url: T) -> Response
174    where
175        T: AsRef<str>,
176    {
177        Self::redirect_with_status(url, StatusCode::TEMPORARY_REDIRECT)
178    }
179
180    /// The response redirects to the [`308`][mdn].
181    ///
182    /// [mdn]: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308>
183    fn permanent<T>(url: T) -> Response
184    where
185        T: AsRef<str>,
186    {
187        Self::redirect_with_status(url, StatusCode::PERMANENT_REDIRECT)
188    }
189}
190
191impl ResponseExt for Response {
192    fn content_length(&self) -> Option<u64> {
193        self.headers()
194            .get(header::CONTENT_LENGTH)
195            .map(header::HeaderValue::to_str)
196            .and_then(Result::ok)
197            .map(str::parse)
198            .and_then(Result::ok)
199    }
200
201    fn content_type(&self) -> Option<mime::Mime> {
202        self.header(header::CONTENT_TYPE)
203    }
204
205    fn header<K, T>(&self, key: K) -> Option<T>
206    where
207        K: header::AsHeaderName,
208        T: std::str::FromStr,
209    {
210        self.headers()
211            .get(key)
212            .map(header::HeaderValue::to_str)
213            .and_then(Result::ok)
214            .map(str::parse)
215            .and_then(Result::ok)
216    }
217
218    fn ok(&self) -> bool {
219        self.status().is_success()
220    }
221
222    #[cfg(feature = "fs")]
223    async fn download<T>(path: T, name: Option<&str>) -> Result<Self>
224    where
225        T: AsRef<std::path::Path> + Send,
226    {
227        let value = name
228            .unwrap_or_else(|| {
229                path.as_ref()
230                    .file_name()
231                    .and_then(std::ffi::OsStr::to_str)
232                    .map_or("download", |filename| filename)
233            })
234            .escape_default();
235
236        let mut resp = Self::attachment(&format!("attachment; filename=\"{value}\""));
237        *resp.body_mut() = Body::from_stream(tokio_util::io::ReaderStream::new(
238            tokio::fs::File::open(path)
239                .await
240                .map_err(crate::Error::from)?,
241        ));
242        Ok(resp)
243    }
244}
245
246mod private {
247    pub trait Sealed {}
248    impl Sealed for super::Response {}
249}