predawn/response/
download.rs

1use std::{borrow::Cow, collections::BTreeMap};
2
3use http::{
4    header::{CONTENT_DISPOSITION, CONTENT_TYPE},
5    HeaderValue, StatusCode,
6};
7use predawn_core::{
8    api_response::ApiResponse,
9    either::Either,
10    into_response::IntoResponse,
11    media_type::{MediaType, MultiResponseMediaType, ResponseMediaType, SingleMediaType},
12    openapi::{self, Schema},
13    response::{MultiResponse, Response, SingleResponse},
14};
15use predawn_schema::ToSchema;
16
17use crate::response_error::{InvalidContentDisposition, InvalidContentDispositionSnafu};
18
19#[derive(Debug)]
20enum DownloadType {
21    Inline,
22    Attachment,
23}
24
25impl DownloadType {
26    fn as_str(&self) -> &'static str {
27        match self {
28            DownloadType::Inline => "inline",
29            DownloadType::Attachment => "attachment",
30        }
31    }
32}
33
34#[derive(Debug)]
35pub struct Download<T> {
36    data: T,
37    ty: DownloadType,
38    file_name: Box<str>,
39}
40
41impl<T> Download<T> {
42    pub fn inline<N>(data: T, file_name: N) -> Self
43    where
44        N: Into<Box<str>>,
45    {
46        fn inner_inline<T>(data: T, file_name: Box<str>) -> Download<T> {
47            Download {
48                data,
49                ty: DownloadType::Inline,
50                file_name,
51            }
52        }
53
54        inner_inline(data, file_name.into())
55    }
56
57    pub fn attachment<N>(data: T, file_name: N) -> Self
58    where
59        N: Into<Box<str>>,
60    {
61        fn inner_attachment<T>(data: T, file_name: Box<str>) -> Download<T> {
62            Download {
63                data,
64                ty: DownloadType::Attachment,
65                file_name,
66            }
67        }
68
69        inner_attachment(data, file_name.into())
70    }
71
72    fn content_disposition(
73        ty: DownloadType,
74        file_name: Box<str>,
75    ) -> Result<HeaderValue, InvalidContentDisposition> {
76        let value = format!("{}; filename=\"{}\"", ty.as_str(), file_name);
77
78        HeaderValue::from_str(&value).map_err(|_| InvalidContentDispositionSnafu { value }.build())
79    }
80}
81
82impl<T: IntoResponse + MediaType> IntoResponse for Download<T> {
83    type Error = Either<T::Error, InvalidContentDisposition>;
84
85    fn into_response(self) -> Result<Response, Self::Error> {
86        let Download {
87            data,
88            ty,
89            file_name,
90        } = self;
91
92        let mut response = data.into_response().map_err(Either::Left)?;
93
94        let headers = response.headers_mut();
95
96        headers.insert(
97            CONTENT_TYPE,
98            HeaderValue::from_static(<Self as MediaType>::MEDIA_TYPE),
99        );
100
101        headers.insert(
102            CONTENT_DISPOSITION,
103            Self::content_disposition(ty, file_name).map_err(Either::Right)?,
104        );
105
106        Ok(response)
107    }
108}
109
110impl<T: MediaType + ResponseMediaType> ApiResponse for Download<T> {
111    fn responses(
112        schemas: &mut BTreeMap<String, Schema>,
113        schemas_in_progress: &mut Vec<String>,
114    ) -> Option<BTreeMap<StatusCode, openapi::Response>> {
115        Some(<Self as MultiResponse>::responses(
116            schemas,
117            schemas_in_progress,
118        ))
119    }
120}
121
122impl<T> ToSchema for Download<T> {
123    fn title() -> Cow<'static, str> {
124        "Download".into()
125    }
126
127    fn key() -> String {
128        let type_name = std::any::type_name::<Self>();
129
130        type_name
131            .find('<')
132            .map_or(type_name, |lt_token| &type_name[..lt_token])
133            .replace("::", ".")
134    }
135
136    fn schema(_: &mut BTreeMap<String, Schema>, _: &mut Vec<String>) -> openapi::Schema {
137        crate::util::binary_schema(Self::title())
138    }
139}
140
141impl<T: MediaType> MediaType for Download<T> {
142    const MEDIA_TYPE: &'static str = T::MEDIA_TYPE;
143}
144
145impl<T: ResponseMediaType> ResponseMediaType for Download<T> {}
146
147impl<T> SingleMediaType for Download<T> {
148    fn media_type(
149        schemas: &mut BTreeMap<String, Schema>,
150        schemas_in_progress: &mut Vec<String>,
151    ) -> openapi::MediaType {
152        openapi::MediaType {
153            schema: Some(<Self as ToSchema>::schema_ref(schemas, schemas_in_progress)),
154            ..Default::default()
155        }
156    }
157}
158
159impl<T: MediaType + ResponseMediaType> SingleResponse for Download<T> {
160    fn response(
161        schemas: &mut BTreeMap<String, Schema>,
162        schemas_in_progress: &mut Vec<String>,
163    ) -> openapi::Response {
164        openapi::Response {
165            content: <Self as MultiResponseMediaType>::content(schemas, schemas_in_progress),
166            ..Default::default()
167        }
168    }
169}