use std::fmt;
use std::convert::Infallible;
use chrono::{DateTime, Utc};
use futures::stream::{Stream, StreamExt};
use http_body_util::{BodyExt, Empty, Full, StreamBody};
use http_body_util::combinators::BoxBody;
use hyper::body::{Body, Bytes, Frame};
use hyper::StatusCode;
use hyper::http::response::Builder;
use crate::utils::date::{parse_http_date, format_http_date};
use crate::utils::json::JsonBuilder;
use super::request::Request;
type ResponseBody = BoxBody<Bytes, Infallible>;
pub struct Response(hyper::Response<ResponseBody>);
impl Response {
pub fn initial_validation(api: bool) -> Self {
Self::error(
api,
StatusCode::SERVICE_UNAVAILABLE,
"Initial validation ongoing. Please wait."
)
}
pub fn bad_request(api: bool, message: impl fmt::Display) -> Self {
Self::error(api, StatusCode::BAD_REQUEST, message)
}
pub fn not_found(api: bool) -> Self {
Self::error(api, StatusCode::NOT_FOUND, "resource not found")
}
pub fn not_modified(etag: &str, done: DateTime<Utc>) -> Self {
ResponseBuilder::new(
StatusCode::NOT_MODIFIED
).etag(etag).last_modified(done).empty()
}
pub fn method_not_allowed(api: bool) -> Self {
Self::error(
api, StatusCode::METHOD_NOT_ALLOWED,
"method not allowed"
)
}
pub fn unsupported_media_type(api: bool, message: impl fmt::Display) -> Self {
Self::error(api, StatusCode::UNSUPPORTED_MEDIA_TYPE, message)
}
pub fn internal_server_error(api: bool) -> Self {
Self::error(api, StatusCode::INTERNAL_SERVER_ERROR,
"internal server error")
}
pub fn error(
api: bool,
status: StatusCode,
message: impl fmt::Display
) -> Self {
if api {
ResponseBuilder::new(
status
).content_type(
ContentType::JSON
).body(
JsonBuilder::build(|json| {
json.member_str("error", message);
})
)
}
else {
ResponseBuilder::new(
status
).content_type(
ContentType::TEXT
).body(message.to_string())
}
}
#[allow(dead_code)]
pub fn moved_permanently(location: &str) -> Self {
ResponseBuilder::new(StatusCode::MOVED_PERMANENTLY)
.content_type(ContentType::TEXT)
.location(location)
.body(format!("Moved permanently to {location}"))
}
pub fn maybe_not_modified(
req: &Request,
etag: &str,
done: DateTime<Utc>,
) -> Option<Response> {
for value in req.headers().get_all("If-None-Match").iter() {
let value = match value.to_str() {
Ok(value) => value,
Err(_) => continue
};
let value = value.trim();
if value == "*" {
return Some(Self::not_modified(etag, done))
}
for tag in EtagsIter(value) {
if tag.trim() == etag {
return Some(Self::not_modified(etag, done))
}
}
}
if let Some(value) = req.headers().get("If-Modified-Since") {
if let Some(date) = parse_http_date(value.to_str().ok()?) {
if date >= done {
return Some(Self::not_modified(etag, done))
}
}
}
None
}
pub fn into_hyper(
self
) -> Result<hyper::Response<ResponseBody>, Infallible> {
Ok(self.0)
}
}
#[derive(Debug)]
pub struct ResponseBuilder {
builder: Builder,
}
impl ResponseBuilder {
pub fn new(status: StatusCode) -> Self {
ResponseBuilder {
builder: Builder::new().status(status).header(
"Access-Control-Allow-Origin", "*"
)
}
}
pub fn ok() -> Self {
Self::new(StatusCode::OK)
}
pub fn content_type(self, content_type: ContentType) -> Self {
ResponseBuilder {
builder: self.builder.header("Content-Type", content_type.0)
}
}
pub fn etag(self, etag: &str) -> Self {
ResponseBuilder {
builder: self.builder.header("ETag", etag)
}
}
pub fn last_modified(self, last_modified: DateTime<Utc>) -> Self {
ResponseBuilder {
builder: self.builder.header(
"Last-Modified",
format_http_date(last_modified)
)
}
}
#[allow(dead_code)]
pub fn location(self, location: &str) -> Self {
ResponseBuilder {
builder: self.builder.header(
"Location",
location
)
}
}
fn finalize<B>(self, body: B) -> Response
where
B: Body<Data = Bytes, Error = Infallible> + Send + Sync + 'static
{
Response(
self.builder.body(
body.boxed()
).expect("broken HTTP response builder")
)
}
pub fn body(self, body: impl Into<Bytes>) -> Response {
self.finalize(Full::new(body.into()))
}
pub fn empty(self) -> Response {
self.finalize(Empty::new())
}
pub fn stream<S>(self, body: S) -> Response
where
S: Stream<Item = Bytes> + Send + Sync + 'static
{
self.finalize(
StreamBody::new(body.map(|item| {
Ok(Frame::data(item))
}))
)
}
}
#[derive(Clone, Debug)]
pub struct ContentType(&'static [u8]);
impl ContentType {
pub const CSV: ContentType = ContentType(
b"text/csv;charset=utf-8;header=present"
);
pub const JSON: ContentType = ContentType(b"application/json");
pub const TEXT: ContentType = ContentType(b"text/plain;charset=utf-8");
pub const PROMETHEUS: ContentType = ContentType(
b"text/plain; version=0.0.4"
);
pub fn external(value: &'static [u8]) -> Self {
ContentType(value)
}
}
struct EtagsIter<'a>(&'a str);
impl<'a> Iterator for EtagsIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
self.0 = self.0.trim_start();
if self.0.is_empty() {
return None
}
let prefix_len = if self.0.starts_with('"') {
1
}
else if self.0.starts_with("W/\"") {
3
}
else {
return None
};
let end = match self.0[prefix_len..].find('"') {
Some(index) => index + prefix_len + 1,
None => return None
};
let res = &self.0[0..end];
self.0 = self.0[end..].trim_start();
if self.0.starts_with(',') {
self.0 = self.0[1..].trim_start();
}
Some(res)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn etags_iter() {
assert_eq!(
EtagsIter("\"foo\", \"bar\", \"ba,zz\"").collect::<Vec<_>>(),
["\"foo\"", "\"bar\"", "\"ba,zz\""]
);
assert_eq!(
EtagsIter("\"foo\", W/\"bar\" , \"ba,zz\", ").collect::<Vec<_>>(),
["\"foo\"", "W/\"bar\"", "\"ba,zz\""]
);
}
}