Skip to main content

alux_http_axum/
output.rs

1use crate::AxumHandlerImpl;
2use alux_http::{
3    BytesOutAlg, ChunksAlg, ChunksExt, EmptyOutAlg, FileOutAlg, HeaderNameAlg, HeaderOutAlg, HtmlOutAlg, HttpErrorAlg,
4    HttpStatus, JsonOutAlg, OutputAlg, RedirectOutAlg, ResultOutAlg, StatusOutAlg, StreamOutAlg, TextOutAlg,
5};
6use axum::Json;
7use axum::body::Body;
8use axum::http::{HeaderName, HeaderValue, StatusCode, header};
9use axum::response::{Html, IntoResponse, Redirect, Response};
10use core::fmt::Display;
11use core::marker::PhantomData;
12use futures::{Stream, TryStreamExt};
13use std::io::Error as IoError;
14
15/// Interprets a portable status as the one axum answers with.
16pub fn axum_status(status: HttpStatus) -> StatusCode {
17    StatusCode::from_u16(status.code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
18}
19
20/// Converts semantic results into axum JSON responses.
21pub struct AxumJsonOutput;
22
23impl<From> OutputAlg<From> for AxumJsonOutput {
24    type Output = Json<From>;
25
26    fn output(from: From) -> Self::Output {
27        Json(from)
28    }
29}
30
31/// Converts semantic results into axum plain-text responses.
32pub struct AxumTextOutput;
33
34impl<From> OutputAlg<From> for AxumTextOutput
35where
36    From: Display,
37{
38    type Output = String;
39
40    fn output(from: From) -> Self::Output {
41        from.to_string()
42    }
43}
44
45/// Converts semantic results into axum HTML responses.
46pub struct AxumHtmlOutput;
47
48impl<From> OutputAlg<From> for AxumHtmlOutput {
49    type Output = Html<From>;
50
51    fn output(from: From) -> Self::Output {
52        Html(from)
53    }
54}
55
56/// Converts semantic results into axum raw-byte responses.
57pub struct AxumBytesOutput;
58
59impl<From> OutputAlg<From> for AxumBytesOutput
60where
61    From: Into<Vec<u8>>,
62{
63    type Output = Vec<u8>;
64
65    fn output(from: From) -> Self::Output {
66        from.into()
67    }
68}
69
70/// Converts a handler that returns nothing into an answer with no body.
71pub struct AxumEmptyOutput;
72
73impl OutputAlg<()> for AxumEmptyOutput {
74    type Output = StatusCode;
75
76    fn output((): ()) -> Self::Output {
77        StatusCode::NO_CONTENT
78    }
79}
80
81/// Converts a semantic location into an axum redirect.
82pub struct AxumRedirectOutput;
83
84impl<From> OutputAlg<From> for AxumRedirectOutput
85where
86    From: Display,
87{
88    type Output = Redirect;
89
90    fn output(from: From) -> Self::Output {
91        Redirect::to(&from.to_string())
92    }
93}
94
95/// Converts a semantic stream of chunks into an axum streamed body.
96pub struct AxumStreamOutput;
97
98/// Reads a body stated as chunks as the bytes this framework moves.
99fn moving<Chunks>(chunks: Chunks) -> impl Stream<Item = Result<Vec<u8>, IoError>> + Send
100where
101    Chunks: ChunksAlg + Send + 'static,
102    Chunks::Chunk: Into<Vec<u8>> + Send,
103    Chunks::Error: Display,
104{
105    chunks.moving().map_ok(Into::into).map_err(|error| IoError::other(error.to_string()))
106}
107
108impl<From> OutputAlg<From> for AxumStreamOutput
109where
110    From: ChunksAlg + Send + 'static,
111    From::Chunk: Into<Vec<u8>> + Send,
112    From::Error: Display,
113{
114    type Output = Body;
115
116    fn output(from: From) -> Self::Output {
117        Body::from_stream(moving(from))
118    }
119}
120
121/// Answers with a header the handler stated, beside the body it stated.
122pub struct AxumHeaderOutput<Inner, Name>(PhantomData<fn(Inner, Name)>);
123
124/// Carries a converted body and a header's value until axum writes them.
125pub struct AxumCarrying<Output, Name> {
126    body: Output,
127    value: String,
128    name: PhantomData<fn(Name)>,
129}
130
131impl<Inner, Name, Value, Rest> OutputAlg<(Value, Rest)> for AxumHeaderOutput<Inner, Name>
132where
133    Inner: OutputAlg<Rest>,
134    Value: Display,
135{
136    type Output = AxumCarrying<Inner::Output, Name>;
137
138    fn output((value, rest): (Value, Rest)) -> Self::Output {
139        AxumCarrying { body: Inner::output(rest), value: value.to_string(), name: PhantomData }
140    }
141}
142
143impl<Output, Name> IntoResponse for AxumCarrying<Output, Name>
144where
145    Output: IntoResponse,
146    Name: HeaderNameAlg,
147{
148    fn into_response(self) -> Response {
149        let mut response = self.body.into_response();
150        if let Ok(value) = HeaderValue::from_str(&self.value) {
151            response.headers_mut().insert(HeaderName::from_static(Name::HEADER_NAME), value);
152        }
153
154        response
155    }
156}
157
158/// Answers with the status an endpoint declared, around the body it already states.
159pub struct AxumStatusOutput<Inner, const CODE: u16>(PhantomData<Inner>);
160
161/// Carries a converted body until axum gives it the declared status.
162pub struct AxumStatusResponse<Output, const CODE: u16>(Output);
163
164impl<Inner, From, const CODE: u16> OutputAlg<From> for AxumStatusOutput<Inner, CODE>
165where
166    Inner: OutputAlg<From>,
167{
168    type Output = AxumStatusResponse<Inner::Output, CODE>;
169
170    fn output(from: From) -> Self::Output {
171        AxumStatusResponse(Inner::output(from))
172    }
173}
174
175impl<Output, const CODE: u16> IntoResponse for AxumStatusResponse<Output, CODE>
176where
177    Output: IntoResponse,
178{
179    fn into_response(self) -> Response {
180        let mut response = self.0.into_response();
181        *response.status_mut() = axum_status(HttpStatus::new(CODE));
182
183        response
184    }
185}
186
187/// Answers with what a failure means when the handler failed, and with the body it states otherwise.
188pub struct AxumResultOutput<Inner, Error>(PhantomData<fn(Inner, Error)>);
189
190/// Carries either a converted body or the meaning of a failure until axum answers with it.
191pub struct AxumResultResponse<Output>(Result<Output, (HttpStatus, String)>);
192
193impl<Inner, Error, Value> OutputAlg<Result<Value, Error>> for AxumResultOutput<Inner, Error>
194where
195    Inner: OutputAlg<Value>,
196    Error: HttpErrorAlg,
197{
198    type Output = AxumResultResponse<Inner::Output>;
199
200    fn output(from: Result<Value, Error>) -> Self::Output {
201        AxumResultResponse(from.map(Inner::output).map_err(|error| (error.http_status(), error.http_message())))
202    }
203}
204
205impl<Output> IntoResponse for AxumResultResponse<Output>
206where
207    Output: IntoResponse,
208{
209    fn into_response(self) -> Response {
210        match self.0 {
211            Ok(output) => output.into_response(),
212            Err((status, message)) => (axum_status(status), message).into_response(),
213        }
214    }
215}
216
217/// Converts semantic file results into downloadable axum responses.
218pub struct AxumFileOutput;
219
220/// Carries a file result and filename until axum creates the response.
221pub struct AxumFileResponse<From>(From);
222
223impl<From> OutputAlg<From> for AxumFileOutput {
224    type Output = AxumFileResponse<From>;
225
226    fn output(from: From) -> Self::Output {
227        AxumFileResponse(from)
228    }
229}
230
231impl<File, Error> IntoResponse for AxumFileResponse<(Result<File, Error>, String)>
232where
233    File: IntoResponse,
234    Error: HttpErrorAlg + Send,
235{
236    fn into_response(self) -> Response {
237        let (file, name) = self.0;
238        match file {
239            Ok(file) => {
240                let mut response = file.into_response();
241                let name = name.replace(['\r', '\n', '"'], "_");
242                if let Ok(value) = HeaderValue::from_str(&format!("attachment; filename=\"{name}\"")) {
243                    response.headers_mut().insert(header::CONTENT_DISPOSITION, value);
244                }
245                response
246            }
247            Err(error) => axum_status(error.http_status()).into_response(),
248        }
249    }
250}
251
252macro_rules! axum_outputs {
253    ($($alg:ident => $selected:ident, $output:ty),+ $(,)?) => {
254        $(
255            impl<Context> $alg for AxumHandlerImpl<Context> {
256                type $selected<From> = $output;
257            }
258        )+
259    };
260}
261
262axum_outputs! {
263    JsonOutAlg     => Json, AxumJsonOutput,
264    FileOutAlg     => File, AxumFileOutput,
265    TextOutAlg     => Text, AxumTextOutput,
266    HtmlOutAlg     => Html, AxumHtmlOutput,
267    BytesOutAlg    => Bytes, AxumBytesOutput,
268    EmptyOutAlg    => Empty, AxumEmptyOutput,
269    RedirectOutAlg => Redirect, AxumRedirectOutput,
270    StreamOutAlg   => Stream, AxumStreamOutput,
271}
272
273impl<Context> HeaderOutAlg for AxumHandlerImpl<Context> {
274    type Header<Inner, Name> = AxumHeaderOutput<Inner, Name>;
275}
276
277impl<Context> StatusOutAlg for AxumHandlerImpl<Context> {
278    type Status<Inner, const CODE: u16> = AxumStatusOutput<Inner, CODE>;
279}
280
281impl<Context> ResultOutAlg for AxumHandlerImpl<Context> {
282    type Result<Inner, Error> = AxumResultOutput<Inner, Error>;
283}