use crate::RocketHandlerImpl;
use alux_http::{
BytesOutAlg, ChunksAlg, ChunksExt, EmptyOutAlg, FileOutAlg, HeaderNameAlg, HeaderOutAlg, HtmlOutAlg, HttpErrorAlg,
HttpStatus, JsonOutAlg, OutputAlg, RedirectOutAlg, ResultOutAlg, StatusOutAlg, StreamOutAlg, TextOutAlg,
};
use core::fmt::{self, Debug, Display};
use core::marker::PhantomData;
use core::pin::Pin;
use futures::{Stream, TryStreamExt};
use serde::Serialize;
use std::io::Error as IoError;
#[derive(Debug)]
pub struct RocketAnswer {
pub(crate) status: HttpStatus,
pub(crate) headers: Vec<(String, String)>,
pub(crate) body: RocketBody,
}
pub enum RocketBody {
Stated(Vec<u8>),
Produced(Chunks),
}
impl Debug for RocketBody {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Stated(body) => formatter.debug_tuple("Stated").field(body).finish(),
Self::Produced(_) => formatter.write_str("Produced(..)"),
}
}
}
pub type Chunks = Pin<Box<dyn Stream<Item = Result<Vec<u8>, IoError>> + Send>>;
impl RocketAnswer {
pub fn new(status: HttpStatus) -> Self {
Self { status, headers: Vec::new(), body: RocketBody::Stated(Vec::new()) }
}
pub fn content(status: HttpStatus, content_type: &str, body: impl Into<Vec<u8>>) -> Self {
Self::new(status).with_header("content-type", content_type).with_body(body)
}
#[must_use]
pub fn with_header(mut self, name: &str, value: &str) -> Self {
self.headers.push((name.to_owned(), value.to_owned()));
self
}
#[must_use]
pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = RocketBody::Stated(body.into());
self
}
#[must_use]
pub fn with_chunks(mut self, chunks: Chunks) -> Self {
self.body = RocketBody::Produced(chunks);
self
}
#[must_use]
pub fn with_status(mut self, status: HttpStatus) -> Self {
self.status = status;
self
}
}
pub struct RocketJsonOutput;
impl<From> OutputAlg<From> for RocketJsonOutput
where
From: Serialize,
{
type Output = RocketAnswer;
fn output(from: From) -> Self::Output {
match serde_json::to_vec(&from) {
Ok(body) => RocketAnswer::content(HttpStatus::OK, "application/json", body),
Err(error) => RocketAnswer::content(HttpStatus::INTERNAL, "text/plain; charset=utf-8", error.to_string()),
}
}
}
macro_rules! rocket_text_outputs {
($($output:ident => $content_type:literal, $meaning:literal),+ $(,)?) => {
$(
#[doc = concat!("Renders a semantic result as ", $meaning, ".")]
pub struct $output;
impl<From> OutputAlg<From> for $output
where
From: Display,
{
type Output = RocketAnswer;
fn output(from: From) -> Self::Output {
RocketAnswer::content(HttpStatus::OK, $content_type, from.to_string())
}
}
)+
};
}
rocket_text_outputs! {
RocketTextOutput => "text/plain; charset=utf-8", "a plain-text answer",
RocketHtmlOutput => "text/html; charset=utf-8", "an HTML answer",
}
pub struct RocketBytesOutput;
impl<From> OutputAlg<From> for RocketBytesOutput
where
From: Into<Vec<u8>>,
{
type Output = RocketAnswer;
fn output(from: From) -> Self::Output {
RocketAnswer::content(HttpStatus::OK, "application/octet-stream", from)
}
}
pub struct RocketEmptyOutput;
impl OutputAlg<()> for RocketEmptyOutput {
type Output = RocketAnswer;
fn output((): ()) -> Self::Output {
RocketAnswer::new(HttpStatus::NO_CONTENT)
}
}
pub struct RocketRedirectOutput;
impl<From> OutputAlg<From> for RocketRedirectOutput
where
From: Display,
{
type Output = RocketAnswer;
fn output(from: From) -> Self::Output {
RocketAnswer::new(HttpStatus::SEE_OTHER).with_header("location", &from.to_string())
}
}
pub struct RocketFileOutput;
impl<File, Error> OutputAlg<(Result<File, Error>, String)> for RocketFileOutput
where
File: Into<Vec<u8>>,
Error: HttpErrorAlg,
{
type Output = RocketAnswer;
fn output((file, name): (Result<File, Error>, String)) -> Self::Output {
match file {
Ok(file) => {
let name = name.replace(['\r', '\n', '"'], "_");
RocketAnswer::content(HttpStatus::OK, "application/octet-stream", file)
.with_header("content-disposition", &format!("attachment; filename=\"{name}\""))
}
Err(error) => RocketAnswer::new(error.http_status()),
}
}
}
pub struct RocketStreamOutput;
impl<From> OutputAlg<From> for RocketStreamOutput
where
From: ChunksAlg + Send + 'static,
From::Chunk: Into<Vec<u8>> + Send,
From::Error: Display,
{
type Output = RocketAnswer;
fn output(from: From) -> Self::Output {
let moving = from.moving().map_ok(Into::into).map_err(|error| IoError::other(error.to_string()));
RocketAnswer::new(HttpStatus::OK)
.with_header("content-type", "application/octet-stream")
.with_chunks(Box::pin(moving))
}
}
pub struct RocketHeaderOutput<Inner, Name>(PhantomData<fn(Inner, Name)>);
impl<Inner, Name, Value, Rest> OutputAlg<(Value, Rest)> for RocketHeaderOutput<Inner, Name>
where
Inner: OutputAlg<Rest, Output = RocketAnswer>,
Name: HeaderNameAlg,
Value: Display,
{
type Output = RocketAnswer;
fn output((value, rest): (Value, Rest)) -> Self::Output {
Inner::output(rest).with_header(Name::HEADER_NAME, &value.to_string())
}
}
pub struct RocketStatusOutput<Inner, const CODE: u16>(PhantomData<Inner>);
impl<Inner, From, const CODE: u16> OutputAlg<From> for RocketStatusOutput<Inner, CODE>
where
Inner: OutputAlg<From, Output = RocketAnswer>,
{
type Output = RocketAnswer;
fn output(from: From) -> Self::Output {
Inner::output(from).with_status(HttpStatus::new(CODE))
}
}
pub struct RocketResultOutput<Inner, Error>(PhantomData<fn(Inner, Error)>);
impl<Inner, Error, Value> OutputAlg<Result<Value, Error>> for RocketResultOutput<Inner, Error>
where
Inner: OutputAlg<Value, Output = RocketAnswer>,
Error: HttpErrorAlg,
{
type Output = RocketAnswer;
fn output(from: Result<Value, Error>) -> Self::Output {
match from {
Ok(value) => Inner::output(value),
Err(error) => RocketAnswer::content(error.http_status(), "text/plain; charset=utf-8", error.http_message()),
}
}
}
macro_rules! rocket_outputs {
($($alg:ident => $selected:ident, $output:ty),+ $(,)?) => {
$(
impl<Context> $alg for RocketHandlerImpl<Context> {
type $selected<From> = $output;
}
)+
};
}
rocket_outputs! {
JsonOutAlg => Json, RocketJsonOutput,
FileOutAlg => File, RocketFileOutput,
TextOutAlg => Text, RocketTextOutput,
HtmlOutAlg => Html, RocketHtmlOutput,
BytesOutAlg => Bytes, RocketBytesOutput,
EmptyOutAlg => Empty, RocketEmptyOutput,
RedirectOutAlg => Redirect, RocketRedirectOutput,
StreamOutAlg => Stream, RocketStreamOutput,
}
impl<Context> HeaderOutAlg for RocketHandlerImpl<Context> {
type Header<Inner, Name> = RocketHeaderOutput<Inner, Name>;
}
impl<Context> StatusOutAlg for RocketHandlerImpl<Context> {
type Status<Inner, const CODE: u16> = RocketStatusOutput<Inner, CODE>;
}
impl<Context> ResultOutAlg for RocketHandlerImpl<Context> {
type Result<Inner, Error> = RocketResultOutput<Inner, Error>;
}