predawn/response/
download.rs1use 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}