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
8pub 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
19pub struct PoemFileOutput;
21
22pub 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
33pub trait PoemFileErrorAlg {
35 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}