Skip to main content

alux_http_poem/
output.rs

1use alux_http::OutputAlg;
2use core::convert::Infallible;
3use poem::http::{HeaderValue, StatusCode, header};
4use poem::web::Json;
5use poem::{IntoResponse, Response};
6use std::io::{Error as IoError, ErrorKind};
7
8/// Converts semantic results into Poem JSON responses.
9pub struct PoemJsonOutput;
10
11impl<From> OutputAlg<From> for PoemJsonOutput {
12    type Output = Json<From>;
13
14    fn output(from: From) -> Self::Output {
15        Json(from)
16    }
17}
18
19/// Converts semantic file results into downloadable Poem responses.
20pub struct PoemFileOutput;
21
22/// Carries a file result and filename until Poem creates the response.
23pub struct PoemFileResponse<From>(From);
24
25impl<From> OutputAlg<From> for PoemFileOutput {
26    type Output = PoemFileResponse<From>;
27
28    fn output(from: From) -> Self::Output {
29        PoemFileResponse(from)
30    }
31}
32
33/// Maps file-opening failures to their HTTP response status.
34pub trait PoemFileErrorAlg {
35    /// Returns the HTTP status represented by this failure.
36    fn status(&self) -> StatusCode;
37}
38
39impl PoemFileErrorAlg for IoError {
40    fn status(&self) -> StatusCode {
41        if self.kind() == ErrorKind::NotFound { StatusCode::NOT_FOUND } else { StatusCode::INTERNAL_SERVER_ERROR }
42    }
43}
44
45impl PoemFileErrorAlg for Infallible {
46    fn status(&self) -> StatusCode {
47        match *self {}
48    }
49}
50
51impl<File, Error> IntoResponse for PoemFileResponse<(Result<File, Error>, String)>
52where
53    File: IntoResponse,
54    Error: PoemFileErrorAlg + Send,
55{
56    fn into_response(self) -> Response {
57        let (file, name) = self.0;
58        match file {
59            Ok(file) => {
60                let mut response = file.into_response();
61                let name = name.replace(['\r', '\n', '"'], "_");
62                if let Ok(value) = HeaderValue::from_str(&format!("attachment; filename=\"{name}\"")) {
63                    response.headers_mut().insert(header::CONTENT_DISPOSITION, value);
64                }
65                response
66            }
67            Err(error) => Response::builder().status(error.status()).finish(),
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::PoemFileOutput;
75    use alux_http::OutputAlg;
76    use poem::http::{StatusCode, header};
77    use poem::{Body, IntoResponse};
78    use std::io::{Error as IoError, ErrorKind};
79
80    fn download(file: Result<Body, IoError>) -> poem::Response {
81        PoemFileOutput::output((file, "data.bin".to_owned())).into_response()
82    }
83
84    #[tokio::test]
85    async fn names_the_file_in_a_download_response() {
86        let mut response = download(Ok(Body::from_bytes((&b"data"[..]).into())));
87        let disposition = response.headers().get(header::CONTENT_DISPOSITION).unwrap().to_str().unwrap().to_owned();
88
89        assert!(response.status().is_success());
90        assert_eq!(disposition, "attachment; filename=\"data.bin\"");
91        assert_eq!(response.take_body().into_string().await.unwrap(), "data");
92    }
93
94    #[test]
95    fn maps_a_missing_file_to_its_response_status() {
96        let response = download(Err(IoError::new(ErrorKind::NotFound, "gone")));
97
98        assert_eq!(response.status(), StatusCode::NOT_FOUND);
99    }
100}