use core::fmt::Debug;
use std::error::Error;
#[cfg(feature = "serde_errors")]
use std::fmt::Display;
use http::{header::CONTENT_TYPE, Response, StatusCode};
use snafu::{whatever, Backtrace, Snafu};
pub type HttpResult<A> = std::result::Result<A, HttpWhatever>;
#[cfg(feature = "serde_errors")]
use serde::{ser::Error as SerError,de::Error as DeserError};
#[macro_export]
macro_rules! http_err {
($s:expr,$d:expr,$e:expr) => {
format!("{}:{}:{}", $s, $d, $e)
};
($d:expr,$e:expr) => {
format!("500:{}:{}", $d, $e)
};
($e:expr) => {
format!("500:unknown:{}", $e)
};
}
#[derive(Debug, Snafu)]
#[snafu(whatever)]
#[snafu(display("{}", self.display()))]
pub struct HttpWhatever {
#[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, Some)))]
#[snafu(provide(false))]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
message: String,
backtrace: Backtrace,
}
impl HttpWhatever {
pub fn parts(&self) -> (&str, &str, StatusCode) {
let parts: Vec<&str> = self.message.splitn(3, ':').collect::<Vec<&str>>();
let mut idx = parts.len();
let message = if idx == 0 {
"<unknown>"
} else {
idx -= 1;
parts[idx]
};
let domain = if idx == 0 {
"Internal"
} else {
idx -= 1;
parts[idx]
};
let status_code = if idx == 0 {
StatusCode::INTERNAL_SERVER_ERROR
} else {
StatusCode::from_bytes(parts[idx - 1].as_bytes())
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
};
(message, domain, status_code)
}
fn display(&self) -> String {
let parts = self.parts();
format!(
"{}: (Domain: {}, HTTP status: {})",
parts.0, parts.1, parts.2
)
}
pub fn details(&self) -> String {
let mut s = self.to_string();
let mut source = self.source();
while let Some(e) = source {
s.push_str(&format!("\n[{e}]"));
source = e.source();
}
s
}
pub fn as_http_response<B>(&self) -> Response<B>
where
B: Default,
{
let parts = self.parts();
Response::builder()
.status(parts.2)
.body(B::default())
.expect("Response::build should succeed")
}
pub fn as_http_string_response<B>(&self) -> Response<B>
where
B: From<String>,
{
let parts = self.parts();
let body_str = format!("{} (application domain: {})", parts.0, parts.1);
let body: B = body_str.into();
Response::builder()
.status(parts.2)
.header(CONTENT_TYPE, "text/plain")
.body(body)
.expect("Response::build should succeed")
}
pub fn as_http_json_response<B>(&self) -> Response<B>
where
B: From<String>,
{
let parts = self.parts();
let body_str = format!("{{\"message\":\"{}\",\"domain\":\"{}\"}}", parts.0, parts.1);
let body: B = body_str.into();
Response::builder()
.status(parts.2)
.header(CONTENT_TYPE, "application/json")
.body(body)
.expect("Response::build should succeed")
}
pub fn new(message: impl std::fmt::Display) -> Self {
let err_gen = |message| -> HttpResult<()> { whatever!("{message}") };
err_gen(message).unwrap_err()
}
}
#[cfg(feature = "serde_errors")]
impl SerError for HttpWhatever {
fn custom<T>(msg:T) -> Self where T:Display {
HttpWhatever::new(msg)
}
}
#[cfg(feature = "serde_errors")]
impl DeserError for HttpWhatever {
fn custom<T>(msg:T) -> Self where T:Display {
HttpWhatever::new(msg)
}
}
pub mod prelude {
pub use crate::http_err;
pub use crate::HttpResult;
pub use crate::HttpWhatever;
pub use snafu::{ensure, OptionExt as _, ResultExt as _};
pub use snafu::{ensure_whatever, whatever};
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use http::{header::CONTENT_TYPE, Response, StatusCode};
use std::num::ParseIntError;
fn parse_usize(strint: &str) -> Result<usize, ParseIntError> {
strint.parse()
}
#[test]
fn basic_test() {
let result: HttpWhatever = parse_usize("certainly not a usize")
.whatever_context("400:Input:That was NOT a usize!")
.unwrap_err();
let parts = result.parts();
assert_eq!(parts.0, "That was NOT a usize!");
assert_eq!(parts.1, "Input");
assert_eq!(parts.2, StatusCode::BAD_REQUEST);
}
#[test]
fn basic_details() {
let result: HttpWhatever = parse_usize("certainly not a usize")
.whatever_context("400:Input:That was NOT a usize!")
.unwrap_err();
assert_eq!(result.details(), "That was NOT a usize!: (Domain: Input, HTTP status: 400 Bad Request)\n[invalid digit found in string]");
}
#[test]
fn test_macro() {
let result: HttpWhatever = parse_usize("certainly not a usize")
.whatever_context(http_err!(400, "Input", "That was NOT a usize!"))
.unwrap_err();
let parts = result.parts();
assert_eq!(parts.0, "That was NOT a usize!");
assert_eq!(parts.1, "Input");
assert_eq!(parts.2, StatusCode::BAD_REQUEST);
}
#[test]
fn test_new() {
let result: HttpWhatever =
HttpWhatever::new(&http_err!(403, "Input", "That was NOT a usize!"));
let parts = result.parts();
assert_eq!(parts.0, "That was NOT a usize!");
assert_eq!(parts.1, "Input");
assert_eq!(parts.2, StatusCode::FORBIDDEN);
}
#[test]
fn test_response() {
let result: HttpWhatever =
HttpWhatever::new(&http_err!(403, "Input", "That was NOT a usize!"));
let http1: Response<String> = result.as_http_response();
let http2: Response<String> = result.as_http_string_response();
let http3: Response<String> = result.as_http_json_response();
assert_eq!(http1.body(), "");
assert_eq!(http1.status(), StatusCode::FORBIDDEN);
assert_eq!(
http2.body(),
"That was NOT a usize! (application domain: Input)"
);
assert_eq!(
http2.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(),
"text/plain"
);
assert_eq!(
http3.body(),
"{\"message\":\"That was NOT a usize!\",\"domain\":\"Input\"}"
);
assert_eq!(
http3.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(),
"application/json"
);
}
}