use bytes::Bytes;
use http::header::{self, HeaderMap, HeaderValue};
#[cfg(feature = "server-warp")]
use http::response::Builder;
pub use http::StatusCode;
#[cfg(feature = "server-warp")]
use hyper::Body;
use std::fmt::Debug;
#[cfg(feature = "server-warp")]
use warp::{
reject::{self, Reject, Rejection},
reply::{Reply, Response},
};
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "server-warp")]
use warp::reject;
#[test]
fn new_status_contains_correct_code() {
assert_eq!(
Status::new(StatusCode::IM_A_TEAPOT).code(),
&StatusCode::IM_A_TEAPOT
);
}
#[test]
fn new_status_contains_correct_specified_message() {
assert_eq!(
Status::with_message(StatusCode::IM_A_TEAPOT, String::from("foobar")).message(),
Some("foobar")
);
}
#[test]
fn new_status_does_not_contain_message() {
assert_eq!(Status::new(StatusCode::IM_A_TEAPOT).message(), None);
}
#[test]
fn new_status_has_empty_headers() {
assert!(Status::new(StatusCode::IM_A_TEAPOT).headers().is_empty());
}
#[test]
fn server_error_does_not_contain_error_message() {
let server_msg = "foobar";
let status =
Status::with_message(StatusCode::INTERNAL_SERVER_ERROR, String::from(server_msg));
let client_msg = status.to_string();
assert!(!client_msg.contains(server_msg));
}
#[test]
#[cfg(feature = "server-warp")]
fn status_rejection_is_a_status() {
assert!(Status::<Empty>::rejection_is_status(&reject::custom(Status::new(
StatusCode::IM_A_TEAPOT
))));
}
#[test]
#[cfg(feature = "server-warp")]
fn non_status_rejection_is_not_status() {
assert!(!Status::<Empty>::rejection_is_status(&reject::not_found()));
}
#[test]
#[cfg(feature = "server-warp")]
fn rejection_from_status() {
let data = vec![0u8, 1u8, 2u8, 3u8, 4u8];
let status = Status::with_data(StatusCode::IM_A_TEAPOT, data.clone());
let rej = reject::Rejection::from(status.clone());
let rej_status = rej.find::<Status<Vec<u8>>>().unwrap();
assert_eq!(&status, rej_status);
}
}
#[derive(Debug)]
pub enum Error {
#[cfg(feature = "server-warp")]
NotStatus(Rejection),
}
pub trait StatusInnerData: Clone + Debug + Send + Sync + 'static {}
impl<T: Clone + Debug + Send + Sync + 'static> StatusInnerData for T {}
pub trait StatusData: Into<Bytes> + StatusInnerData {}
impl<T: Into<Bytes> + StatusInnerData> StatusData for T {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Empty;
impl Into<Bytes> for Empty {
fn into(self) -> Bytes {
Bytes::new()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Status<T = Empty>
where
T: StatusData,
{
c: StatusCode,
data: T,
data_bytes: Bytes,
h: HeaderMap<HeaderValue>,
}
impl Status {
pub fn new(code: StatusCode) -> Status<Empty> {
Status {
c: code,
data: Empty{},
data_bytes: Bytes::new(),
h: HeaderMap::new(),
}
}
pub fn with_message(code: StatusCode, msg: String) -> Status<String> {
let mut status = Status::with_data(code, msg);
status.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_str(mime::TEXT_PLAIN_UTF_8.as_ref()).unwrap(),
);
status
}
pub fn with_data<T: StatusData>(code: StatusCode, data: T) -> Status<T> {
Status {
c: code,
data: data.clone(),
data_bytes: data.into(),
h: HeaderMap::new(),
}
}
}
impl<T: StatusData> Status<T> {
pub fn code(&self) -> &StatusCode {
&self.c
}
fn data_as_message(&self) -> Option<&str> {
std::str::from_utf8(self.data_bytes.as_ref()).ok()
}
pub fn message(&self) -> Option<&str> {
if self.h.contains_key(header::CONTENT_TYPE) {
match self.h.get(header::CONTENT_TYPE).unwrap().to_str() {
Err(_) => None,
Ok(content_type) => match content_type.parse::<mime::Mime>().ok() {
None => None,
Some(mime_type) => match mime_type.type_() {
mime::TEXT => self.data_as_message(),
_ => {
if mime_type == mime::APPLICATION_JSON {
self.data_as_message()
} else {
None
}
}
},
},
}
} else {
None
}
}
pub fn data(&self) -> &T {
&self.data
}
pub fn bytes(&self) -> &[u8] {
&self.data_bytes
}
pub fn headers(&self) -> &HeaderMap<HeaderValue> {
&self.h
}
pub fn headers_mut(&mut self) -> &mut HeaderMap<HeaderValue> {
&mut self.h
}
#[cfg(feature = "server-warp")]
pub fn rejection_is_status(err: &Rejection) -> bool {
err.find::<Self>().is_some()
}
#[cfg(feature = "server-warp")]
pub fn recover(err: Rejection) -> std::result::Result<Self, Error> {
err.find::<Self>()
.map(|stat| stat.clone())
.ok_or(Error::NotStatus(err))
}
}
#[cfg(feature = "server-warp")]
impl <T: StatusData> From<Status<T>> for Rejection {
fn from(status: Status<T>) -> Self {
reject::custom(status)
}
}
impl<T: StatusData> std::fmt::Display for Status<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.message() {
None => write!(f, "{}", self.code()),
Some(msg) => {
if self.code().as_u16() < 500 {
write!(f, "{}\n{}", self.code(), msg)
} else {
write!(f, "{}", self.code())
}
}
}
}
}
impl<T: StatusData> std::error::Error for Status<T> {}
impl<T: StatusData> From<Status<T>> for Result<Status<T>, Status<T>> {
fn from(s: Status<T>) -> Result<Status<T>, Status<T>> {
if s.code().as_u16() < 400 {
Ok(s)
} else {
Err(s)
}
}
}
#[cfg(feature = "server-warp")]
impl<T: StatusData> From<Status<T>> for Response {
fn from(s: Status<T>) -> Response {
let mut build = Builder::new().status(s.code());
for (key, val) in s.headers().iter() {
build = build.header(key, val)
}
build.body(Body::from(s.data_bytes)).unwrap()
}
}
#[cfg(feature = "server-warp")]
impl<T: StatusData> Reject for Status<T> {}
#[cfg(feature = "server-warp")]
impl<T: StatusData> Reply for Status<T> {
fn into_response(self) -> Response {
self.into()
}
}