Skip to main content

alux_http_actix/
output.rs

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