1use 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#[derive(Debug)]
20pub struct RocketAnswer {
21 pub(crate) status: HttpStatus,
22 pub(crate) headers: Vec<(String, String)>,
23 pub(crate) body: RocketBody,
24}
25
26pub enum RocketBody {
28 Stated(Vec<u8>),
30 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
43pub type Chunks = Pin<Box<dyn Stream<Item = Result<Vec<u8>, IoError>> + Send>>;
45
46impl RocketAnswer {
47 pub fn new(status: HttpStatus) -> Self {
49 Self { status, headers: Vec::new(), body: RocketBody::Stated(Vec::new()) }
50 }
51
52 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 #[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 #[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 #[must_use]
73 pub fn with_chunks(mut self, chunks: Chunks) -> Self {
74 self.body = RocketBody::Produced(chunks);
75 self
76 }
77
78 #[must_use]
80 pub fn with_status(mut self, status: HttpStatus) -> Self {
81 self.status = status;
82 self
83 }
84}
85
86pub 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
128pub 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
142pub 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
153pub 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
167pub 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
189pub 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
209pub 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
225pub 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
239pub 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}