use super::{field::IsoEncode, Response, R};
use frunk_core::coproduct::{CNil, Coproduct};
use headers::{Header, HeaderMapExt};
use http::{HeaderMap, Method, StatusCode, Uri};
use hyper::{
header::{CONTENT_TYPE, LOCATION, X_CONTENT_TYPE_OPTIONS},
Body,
};
use hyper_staticfile::{resolve_path, ResponseBuilder};
use serde::Serialize;
use std::{
borrow::Cow,
convert::{Infallible, TryFrom},
future::Future,
io,
path::PathBuf,
};
use thiserror::Error;
pub mod sse;
pub trait Reply: Sized + Send {
fn into_response(self) -> Response;
#[inline]
fn with_status(self, code: StatusCode) -> Response {
let mut resp = self.into_response();
*resp.status_mut() = code;
resp
}
#[inline]
fn with_header<H: Header>(self, header: H) -> Response {
let mut resp = self.into_response();
resp.headers_mut().typed_insert(header);
resp
}
}
impl Reply for Response {
#[inline]
fn into_response(self) -> Response {
self
}
#[inline]
fn with_status(mut self, code: StatusCode) -> Response {
*self.status_mut() = code;
self
}
#[inline]
fn with_header<H: Header>(mut self, header: H) -> Response {
self.headers_mut().typed_insert(header);
self
}
}
impl<R: Reply, E: Reply> Reply for Result<R, E> {
#[inline]
fn into_response(self) -> Response {
self.map_or_else(E::into_response, R::into_response)
}
}
macro_rules! uninhabited {
($t: ty) => {
impl Reply for $t {
#[inline]
fn into_response(self) -> Response {
match self {}
}
}
};
}
uninhabited! { Infallible }
uninhabited! { CNil }
impl<H: Reply, Tail: Reply> Reply for Coproduct<H, Tail> {
#[inline]
fn into_response(self) -> Response {
match self {
Coproduct::Inl(h) => h.into_response(),
Coproduct::Inr(t) => t.into_response(),
}
}
}
impl<T: Reply> Reply for Box<T> {
#[inline]
fn into_response(self) -> Response {
(*self).into_response()
}
}
impl<'a, T: ToOwned> Reply for Cow<'a, T>
where
&'a T: Reply,
T::Owned: Reply,
{
#[inline]
fn into_response(self) -> Response {
match self {
Cow::Borrowed(t) => t.into_response(),
Cow::Owned(t) => t.into_response(),
}
}
}
macro_rules! content_type {
($mime:literal $( $re_type:ty ),+ $(,)?) => {
$(impl Reply for $re_type {
#[inline]
fn into_response(self) -> Response {
hyper::Response::builder()
.header(CONTENT_TYPE, $mime)
.header(X_CONTENT_TYPE_OPTIONS, "nosniff")
.body(self.into())
.unwrap()
}
})+
};
}
content_type! { "text/plain; charset=utf-8"
String,
&'static str,
}
content_type! { "application/octet-stream"
Vec<u8>,
&'static [u8],
}
#[inline]
pub fn json<T: Serialize>(value: &T) -> Response {
let ser = serde_json::to_string(value).unwrap();
hyper::Response::builder()
.header(CONTENT_TYPE, "application/json")
.header(X_CONTENT_TYPE_OPTIONS, "nosniff")
.body(ser.into())
.unwrap()
}
#[inline]
pub fn jsonr<'a, T: IsoEncode<'a>>(value: &'a T) -> Response {
json(&value.as_repr())
}
pub async fn extract<T: Reply>(cx: R![T]) -> T {
cx.head
}
#[derive(Debug, Error)]
pub enum FsError {
#[error("io error: {}", .0)]
Io(#[from] io::Error),
#[error("http error: {}", .0)]
Http(#[from] http::Error),
}
impl Reply for FsError {
#[inline]
fn into_response(self) -> Response {
hyper::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body("internal server error".into())
.unwrap()
}
}
pub fn filesystem(base_path: &str) -> impl Fn(R![Method, Uri, HeaderMap]) -> FsFuture {
let root = PathBuf::from(base_path);
move |cx| fs_inner(root.clone(), cx.head, Ok(cx.tail.head), cx.tail.tail.head)
}
pub fn filesystem_path(
base_path: &str,
) -> impl Fn(R![Method, path: String, HeaderMap]) -> FsFuture {
let root = PathBuf::from(base_path);
move |cx| {
let uri = Uri::try_from(cx.tail.head.into_inner()).map_err(|e| e.into());
fs_inner(root.clone(), cx.head, uri, cx.tail.tail.head)
}
}
pub type FsFuture = impl Future<Output = Result<Response, FsError>>;
type UriRes = http::Result<Uri>;
async fn fs_inner(path: PathBuf, m: Method, u: UriRes, h: HeaderMap) -> Result<Response, FsError> {
let uri = u?;
ResponseBuilder::new()
.request_parts(&m, &uri, &h)
.build(resolve_path(path, uri.path()).await?)
.map_err(|e| e.into())
}
pub fn redirect<P: AsRef<str>>(permanent: bool, path: P) -> Response {
hyper::Response::builder()
.status(if permanent {
StatusCode::PERMANENT_REDIRECT
} else {
StatusCode::TEMPORARY_REDIRECT
})
.header(LOCATION, path.as_ref())
.body(Body::empty())
.unwrap()
}