Skip to main content

alux_http_warp/
output.rs

1//! Renders each output kind as the reply warp answers with.
2
3use crate::WarpHandlerImpl;
4use alux_http::{
5    BytesOutAlg, EmptyOutAlg, FileOutAlg, HeaderNameAlg, HeaderOutAlg, HtmlOutAlg, HttpErrorAlg, HttpStatus,
6    JsonOutAlg, OutputAlg, RedirectOutAlg, ResultOutAlg, StatusOutAlg, TextOutAlg,
7};
8use core::fmt::Display;
9use core::marker::PhantomData;
10use serde::Serialize;
11use warp::http::{HeaderName, HeaderValue, StatusCode, header};
12use warp::reply::Response;
13
14/// Interprets a portable status as the one warp answers with.
15pub fn warp_status(status: HttpStatus) -> StatusCode {
16    StatusCode::from_u16(status.code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
17}
18
19/// States a reply carrying a status, a content type, and a body.
20fn answered(status: HttpStatus, content_type: &str, body: impl Into<Vec<u8>>) -> Response {
21    warp::http::Response::builder()
22        .status(warp_status(status))
23        .header(header::CONTENT_TYPE, content_type)
24        .body(body.into().into())
25        .unwrap_or_else(|_| warp::http::Response::new(Vec::new().into()))
26}
27
28/// Renders a semantic result as a JSON reply.
29pub struct WarpJsonOutput;
30
31impl<From> OutputAlg<From> for WarpJsonOutput
32where
33    From: Serialize,
34{
35    type Output = Response;
36
37    fn output(from: From) -> Self::Output {
38        match serde_json::to_vec(&from) {
39            Ok(body) => answered(HttpStatus::OK, "application/json", body),
40            Err(error) => answered(HttpStatus::INTERNAL, "text/plain; charset=utf-8", error.to_string()),
41        }
42    }
43}
44
45macro_rules! warp_text_outputs {
46    ($($output:ident => $content_type:literal, $meaning:literal),+ $(,)?) => {
47        $(
48            #[doc = concat!("Renders a semantic result as ", $meaning, ".")]
49            pub struct $output;
50
51            impl<From> OutputAlg<From> for $output
52            where
53                From: Display,
54            {
55                type Output = Response;
56
57                fn output(from: From) -> Self::Output {
58                    answered(HttpStatus::OK, $content_type, from.to_string())
59                }
60            }
61        )+
62    };
63}
64
65warp_text_outputs! {
66    WarpTextOutput => "text/plain; charset=utf-8", "a plain-text reply",
67    WarpHtmlOutput => "text/html; charset=utf-8", "an HTML reply",
68}
69
70/// Renders a semantic result as a raw-byte reply.
71pub struct WarpBytesOutput;
72
73impl<From> OutputAlg<From> for WarpBytesOutput
74where
75    From: Into<Vec<u8>>,
76{
77    type Output = Response;
78
79    fn output(from: From) -> Self::Output {
80        answered(HttpStatus::OK, "application/octet-stream", from)
81    }
82}
83
84/// Renders a handler that returns nothing as a reply with no body.
85pub struct WarpEmptyOutput;
86
87impl OutputAlg<()> for WarpEmptyOutput {
88    type Output = Response;
89
90    fn output((): ()) -> Self::Output {
91        warp::http::Response::builder()
92            .status(warp_status(HttpStatus::NO_CONTENT))
93            .body(Vec::new().into())
94            .unwrap_or_else(|_| warp::http::Response::new(Vec::new().into()))
95    }
96}
97
98/// Renders a semantic location as a redirect.
99pub struct WarpRedirectOutput;
100
101impl<From> OutputAlg<From> for WarpRedirectOutput
102where
103    From: Display,
104{
105    type Output = Response;
106
107    fn output(from: From) -> Self::Output {
108        warp::http::Response::builder()
109            .status(warp_status(HttpStatus::SEE_OTHER))
110            .header(header::LOCATION, from.to_string())
111            .body(Vec::new().into())
112            .unwrap_or_else(|_| warp::http::Response::new(Vec::new().into()))
113    }
114}
115
116/// Renders a semantic file result as a downloadable reply.
117pub struct WarpFileOutput;
118
119impl<File, Error> OutputAlg<(Result<File, Error>, String)> for WarpFileOutput
120where
121    File: Into<Vec<u8>>,
122    Error: HttpErrorAlg,
123{
124    type Output = Response;
125
126    fn output((file, name): (Result<File, Error>, String)) -> Self::Output {
127        match file {
128            Ok(file) => {
129                let name = name.replace(['\r', '\n', '"'], "_");
130                let mut answer = answered(HttpStatus::OK, "application/octet-stream", file);
131                if let Ok(value) = format!("attachment; filename=\"{name}\"").parse() {
132                    answer.headers_mut().insert(header::CONTENT_DISPOSITION, value);
133                }
134
135                answer
136            }
137            Err(error) => {
138                let mut answer = warp::http::Response::new(Vec::new().into());
139                *answer.status_mut() = warp_status(error.http_status());
140
141                answer
142            }
143        }
144    }
145}
146
147/// Answers with a header the handler stated, beside the body it stated.
148pub struct WarpHeaderOutput<Inner, Name>(PhantomData<fn(Inner, Name)>);
149
150impl<Inner, Name, Value, Rest> OutputAlg<(Value, Rest)> for WarpHeaderOutput<Inner, Name>
151where
152    Inner: OutputAlg<Rest, Output = Response>,
153    Name: HeaderNameAlg,
154    Value: Display,
155{
156    type Output = Response;
157
158    fn output((value, rest): (Value, Rest)) -> Self::Output {
159        let mut answer = Inner::output(rest);
160        if let Ok(value) = HeaderValue::from_str(&value.to_string()) {
161            answer.headers_mut().insert(HeaderName::from_static(Name::HEADER_NAME), value);
162        }
163
164        answer
165    }
166}
167
168/// Answers with the status an endpoint declared, around the body it already states.
169pub struct WarpStatusOutput<Inner, const CODE: u16>(PhantomData<Inner>);
170
171impl<Inner, From, const CODE: u16> OutputAlg<From> for WarpStatusOutput<Inner, CODE>
172where
173    Inner: OutputAlg<From, Output = Response>,
174{
175    type Output = Response;
176
177    fn output(from: From) -> Self::Output {
178        let mut answer = Inner::output(from);
179        *answer.status_mut() = warp_status(HttpStatus::new(CODE));
180
181        answer
182    }
183}
184
185/// Answers with what a failure means when the handler failed, and with the body it states otherwise.
186pub struct WarpResultOutput<Inner, Error>(PhantomData<fn(Inner, Error)>);
187
188impl<Inner, Error, Value> OutputAlg<Result<Value, Error>> for WarpResultOutput<Inner, Error>
189where
190    Inner: OutputAlg<Value, Output = Response>,
191    Error: HttpErrorAlg,
192{
193    type Output = Response;
194
195    fn output(from: Result<Value, Error>) -> Self::Output {
196        match from {
197            Ok(value) => Inner::output(value),
198            Err(error) => answered(error.http_status(), "text/plain; charset=utf-8", error.http_message()),
199        }
200    }
201}
202
203macro_rules! warp_outputs {
204    ($($alg:ident => $selected:ident, $output:ty),+ $(,)?) => {
205        $(
206            impl<Context> $alg for WarpHandlerImpl<Context> {
207                type $selected<From> = $output;
208            }
209        )+
210    };
211}
212
213warp_outputs! {
214    JsonOutAlg     => Json, WarpJsonOutput,
215    FileOutAlg     => File, WarpFileOutput,
216    TextOutAlg     => Text, WarpTextOutput,
217    HtmlOutAlg     => Html, WarpHtmlOutput,
218    BytesOutAlg    => Bytes, WarpBytesOutput,
219    EmptyOutAlg    => Empty, WarpEmptyOutput,
220    RedirectOutAlg => Redirect, WarpRedirectOutput,
221}
222
223impl<Context> HeaderOutAlg for WarpHandlerImpl<Context> {
224    type Header<Inner, Name> = WarpHeaderOutput<Inner, Name>;
225}
226
227impl<Context> StatusOutAlg for WarpHandlerImpl<Context> {
228    type Status<Inner, const CODE: u16> = WarpStatusOutput<Inner, CODE>;
229}
230
231impl<Context> ResultOutAlg for WarpHandlerImpl<Context> {
232    type Result<Inner, Error> = WarpResultOutput<Inner, Error>;
233}