Skip to main content

alux_http_rocket/
output.rs

1//! Renders each output kind as the response Rocket answers with.
2
3use crate::RocketHandlerImpl;
4use alux_http::{
5    BytesOutAlg, ChunksAlg, ChunksExt, EmptyOutAlg, FileOutAlg, HeaderNameAlg, HeaderOutAlg, HtmlOutAlg, HttpErrorAlg,
6    HttpStatus, JsonOutAlg, OutputAlg, RedirectOutAlg, ResultOutAlg, StatusOutAlg, StreamOutAlg, TextOutAlg,
7};
8use core::fmt::{self, Debug, Display};
9use core::marker::PhantomData;
10use core::pin::Pin;
11use futures::{Stream, TryStreamExt};
12use serde::Serialize;
13use std::io::Error as IoError;
14
15/// What one endpoint answers with, before Rocket builds a response from it.
16///
17/// Rocket's own response borrows the request it answers, so what an output kind states here is the
18/// answer itself and the endpoint hands it over.
19#[derive(Debug)]
20pub struct RocketAnswer {
21    pub(crate) status: HttpStatus,
22    pub(crate) headers: Vec<(String, String)>,
23    pub(crate) body: RocketBody,
24}
25
26/// What an answer carries, which is either bytes already in hand or bytes still to come.
27pub enum RocketBody {
28    /// Bytes that are already there.
29    Stated(Vec<u8>),
30    /// Bytes produced over time, and what produces them.
31    Produced(Chunks),
32}
33
34impl Debug for RocketBody {
35    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            Self::Stated(body) => formatter.debug_tuple("Stated").field(body).finish(),
38            Self::Produced(_) => formatter.write_str("Produced(..)"),
39        }
40    }
41}
42
43/// What produces a body over time, once this interpretation has chosen how to carry it.
44pub type Chunks = Pin<Box<dyn Stream<Item = Result<Vec<u8>, IoError>> + Send>>;
45
46impl RocketAnswer {
47    /// States an answer carrying a status and nothing else.
48    pub fn new(status: HttpStatus) -> Self {
49        Self { status, headers: Vec::new(), body: RocketBody::Stated(Vec::new()) }
50    }
51
52    /// States an answer carrying a status, a content type, and a body.
53    pub fn content(status: HttpStatus, content_type: &str, body: impl Into<Vec<u8>>) -> Self {
54        Self::new(status).with_header("content-type", content_type).with_body(body)
55    }
56
57    /// States one header on this answer.
58    #[must_use]
59    pub fn with_header(mut self, name: &str, value: &str) -> Self {
60        self.headers.push((name.to_owned(), value.to_owned()));
61        self
62    }
63
64    /// States the body of this answer.
65    #[must_use]
66    pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
67        self.body = RocketBody::Stated(body.into());
68        self
69    }
70
71    /// States a body this answer produces over time.
72    #[must_use]
73    pub fn with_chunks(mut self, chunks: Chunks) -> Self {
74        self.body = RocketBody::Produced(chunks);
75        self
76    }
77
78    /// Answers the same thing under a different status.
79    #[must_use]
80    pub fn with_status(mut self, status: HttpStatus) -> Self {
81        self.status = status;
82        self
83    }
84}
85
86/// Renders a semantic result as a JSON answer.
87pub struct RocketJsonOutput;
88
89impl<From> OutputAlg<From> for RocketJsonOutput
90where
91    From: Serialize,
92{
93    type Output = RocketAnswer;
94
95    fn output(from: From) -> Self::Output {
96        match serde_json::to_vec(&from) {
97            Ok(body) => RocketAnswer::content(HttpStatus::OK, "application/json", body),
98            Err(error) => RocketAnswer::content(HttpStatus::INTERNAL, "text/plain; charset=utf-8", error.to_string()),
99        }
100    }
101}
102
103macro_rules! rocket_text_outputs {
104    ($($output:ident => $content_type:literal, $meaning:literal),+ $(,)?) => {
105        $(
106            #[doc = concat!("Renders a semantic result as ", $meaning, ".")]
107            pub struct $output;
108
109            impl<From> OutputAlg<From> for $output
110            where
111                From: Display,
112            {
113                type Output = RocketAnswer;
114
115                fn output(from: From) -> Self::Output {
116                    RocketAnswer::content(HttpStatus::OK, $content_type, from.to_string())
117                }
118            }
119        )+
120    };
121}
122
123rocket_text_outputs! {
124    RocketTextOutput => "text/plain; charset=utf-8", "a plain-text answer",
125    RocketHtmlOutput => "text/html; charset=utf-8", "an HTML answer",
126}
127
128/// Renders a semantic result as a raw-byte answer.
129pub struct RocketBytesOutput;
130
131impl<From> OutputAlg<From> for RocketBytesOutput
132where
133    From: Into<Vec<u8>>,
134{
135    type Output = RocketAnswer;
136
137    fn output(from: From) -> Self::Output {
138        RocketAnswer::content(HttpStatus::OK, "application/octet-stream", from)
139    }
140}
141
142/// Renders a handler that returns nothing as an answer with no body.
143pub struct RocketEmptyOutput;
144
145impl OutputAlg<()> for RocketEmptyOutput {
146    type Output = RocketAnswer;
147
148    fn output((): ()) -> Self::Output {
149        RocketAnswer::new(HttpStatus::NO_CONTENT)
150    }
151}
152
153/// Renders a semantic location as a redirect.
154pub struct RocketRedirectOutput;
155
156impl<From> OutputAlg<From> for RocketRedirectOutput
157where
158    From: Display,
159{
160    type Output = RocketAnswer;
161
162    fn output(from: From) -> Self::Output {
163        RocketAnswer::new(HttpStatus::SEE_OTHER).with_header("location", &from.to_string())
164    }
165}
166
167/// Renders a semantic file result as a downloadable answer.
168pub struct RocketFileOutput;
169
170impl<File, Error> OutputAlg<(Result<File, Error>, String)> for RocketFileOutput
171where
172    File: Into<Vec<u8>>,
173    Error: HttpErrorAlg,
174{
175    type Output = RocketAnswer;
176
177    fn output((file, name): (Result<File, Error>, String)) -> Self::Output {
178        match file {
179            Ok(file) => {
180                let name = name.replace(['\r', '\n', '"'], "_");
181                RocketAnswer::content(HttpStatus::OK, "application/octet-stream", file)
182                    .with_header("content-disposition", &format!("attachment; filename=\"{name}\""))
183            }
184            Err(error) => RocketAnswer::new(error.http_status()),
185        }
186    }
187}
188
189/// Answers with a body produced over time.
190pub struct RocketStreamOutput;
191
192impl<From> OutputAlg<From> for RocketStreamOutput
193where
194    From: ChunksAlg + Send + 'static,
195    From::Chunk: Into<Vec<u8>> + Send,
196    From::Error: Display,
197{
198    type Output = RocketAnswer;
199
200    fn output(from: From) -> Self::Output {
201        let moving = from.moving().map_ok(Into::into).map_err(|error| IoError::other(error.to_string()));
202
203        RocketAnswer::new(HttpStatus::OK)
204            .with_header("content-type", "application/octet-stream")
205            .with_chunks(Box::pin(moving))
206    }
207}
208
209/// Answers with a header the handler stated, beside the body it stated.
210pub struct RocketHeaderOutput<Inner, Name>(PhantomData<fn(Inner, Name)>);
211
212impl<Inner, Name, Value, Rest> OutputAlg<(Value, Rest)> for RocketHeaderOutput<Inner, Name>
213where
214    Inner: OutputAlg<Rest, Output = RocketAnswer>,
215    Name: HeaderNameAlg,
216    Value: Display,
217{
218    type Output = RocketAnswer;
219
220    fn output((value, rest): (Value, Rest)) -> Self::Output {
221        Inner::output(rest).with_header(Name::HEADER_NAME, &value.to_string())
222    }
223}
224
225/// Answers with the status an endpoint declared, around the body it already states.
226pub struct RocketStatusOutput<Inner, const CODE: u16>(PhantomData<Inner>);
227
228impl<Inner, From, const CODE: u16> OutputAlg<From> for RocketStatusOutput<Inner, CODE>
229where
230    Inner: OutputAlg<From, Output = RocketAnswer>,
231{
232    type Output = RocketAnswer;
233
234    fn output(from: From) -> Self::Output {
235        Inner::output(from).with_status(HttpStatus::new(CODE))
236    }
237}
238
239/// Answers with what a failure means when the handler failed, and with the body it states otherwise.
240pub struct RocketResultOutput<Inner, Error>(PhantomData<fn(Inner, Error)>);
241
242impl<Inner, Error, Value> OutputAlg<Result<Value, Error>> for RocketResultOutput<Inner, Error>
243where
244    Inner: OutputAlg<Value, Output = RocketAnswer>,
245    Error: HttpErrorAlg,
246{
247    type Output = RocketAnswer;
248
249    fn output(from: Result<Value, Error>) -> Self::Output {
250        match from {
251            Ok(value) => Inner::output(value),
252            Err(error) => RocketAnswer::content(error.http_status(), "text/plain; charset=utf-8", error.http_message()),
253        }
254    }
255}
256
257macro_rules! rocket_outputs {
258    ($($alg:ident => $selected:ident, $output:ty),+ $(,)?) => {
259        $(
260            impl<Context> $alg for RocketHandlerImpl<Context> {
261                type $selected<From> = $output;
262            }
263        )+
264    };
265}
266
267rocket_outputs! {
268    JsonOutAlg     => Json, RocketJsonOutput,
269    FileOutAlg     => File, RocketFileOutput,
270    TextOutAlg     => Text, RocketTextOutput,
271    HtmlOutAlg     => Html, RocketHtmlOutput,
272    BytesOutAlg    => Bytes, RocketBytesOutput,
273    EmptyOutAlg    => Empty, RocketEmptyOutput,
274    RedirectOutAlg => Redirect, RocketRedirectOutput,
275    StreamOutAlg   => Stream, RocketStreamOutput,
276}
277
278impl<Context> HeaderOutAlg for RocketHandlerImpl<Context> {
279    type Header<Inner, Name> = RocketHeaderOutput<Inner, Name>;
280}
281
282impl<Context> StatusOutAlg for RocketHandlerImpl<Context> {
283    type Status<Inner, const CODE: u16> = RocketStatusOutput<Inner, CODE>;
284}
285
286impl<Context> ResultOutAlg for RocketHandlerImpl<Context> {
287    type Result<Inner, Error> = RocketResultOutput<Inner, Error>;
288}