#![doc = include_str!("../README.md")]
use anyhow::{format_err, Error};
use axum::{
response::{IntoResponse, Response},
Json,
};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
pub fn trace_error(message: Option<String>, error: Error) -> Vec<String> {
let mut reasons = Vec::new();
if let Some(head) = message {
reasons.push(head);
}
reasons.push(error.to_string());
error
.chain()
.skip(1)
.for_each(|x| reasons.push(x.to_string()));
reasons
}
#[derive(Clone, Default, Deserialize, Serialize)]
pub struct Failure {
pub errors: Vec<String>,
}
pub trait ErrorResponder: Default + Serialize {
fn new() -> Self {
Self::default()
}
fn fail(due_to: Error, status: Option<StatusCode>) -> (StatusCode, Json<Self>);
fn fail_because(
because: impl Display,
due_to: Error,
status: Option<StatusCode>,
) -> (StatusCode, Json<Self>);
fn fail_directly(because: impl Display, status: Option<StatusCode>) -> (StatusCode, Json<Self>);
fn crash(due_to: Option<Error>) -> (StatusCode, Json<Self>);
}
impl ErrorResponder for Failure {
fn fail(due_to: Error, status: Option<StatusCode>) -> (StatusCode, Json<Self>) {
(
if let Some(status) = status {
status
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(Failure {
errors: trace_error(None, due_to),
}),
)
}
fn fail_because(
because: impl Display,
due_to: Error,
status: Option<StatusCode>,
) -> (StatusCode, Json<Self>) {
(
if let Some(status) = status {
status
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(Failure {
errors: trace_error(Some(because.to_string()), due_to),
}),
)
}
fn fail_directly(because: impl Display, status: Option<StatusCode>) -> (StatusCode, Json<Self>) {
(
if let Some(status) = status {
status
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(Failure {
errors: vec![because.to_string()],
}),
)
}
fn crash(due_to: Option<Error>) -> (StatusCode, Json<Self>) {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(Failure {
errors: if let Some(error) = due_to {
trace_error(None, error)
} else {
vec!["Server crashed due to unknown reasons.".to_owned()]
},
}),
)
}
}
pub trait Fallible<T> {
fn has_not_failed(&self) -> bool;
fn get_inner(self) -> T;
fn get_error(self) -> Error;
}
impl<T, E> Fallible<T> for Result<T, E>
where
E: Debug + Display + Send + Sync + 'static,
{
fn has_not_failed(&self) -> bool {
self.is_ok()
}
fn get_inner(self) -> T {
self.unwrap()
}
fn get_error(self) -> Error {
format_err!(self.err().unwrap())
}
}
impl<T> Fallible<T> for Option<T> {
fn has_not_failed(&self) -> bool {
self.is_some()
}
fn get_inner(self) -> T {
self.unwrap()
}
fn get_error(self) -> Error {
Error::msg("`Option` has `None` value.")
}
}
pub trait Recoil<T>: Fallible<T> {
fn recoil<R>(
self,
context: Option<impl Display + Send + Sync + 'static>,
status: Option<StatusCode>,
) -> Result<T, Response>
where
R: ErrorResponder,
Self: Sized,
{
if self.has_not_failed() {
Ok(self.get_inner())
} else {
Err((if let Some(message) = context {
R::fail_because(message.to_string(), self.get_error(), status)
} else {
R::fail(self.get_error(), status)
})
.into_response())
}
}
}
impl<T, E> Recoil<T> for Result<T, E> where E: Debug + Display + Send + Sync + 'static {}
impl<T> Recoil<T> for Option<T> {}
#[cfg(test)]
mod tests {
use super::*;
use axum::response::IntoResponse;
use http_body::Body;
use serde_json::to_string as json_dumps;
mod example {
use anyhow::{bail, Context, Result};
fn always_err() -> Result<()> {
bail!("Never depend on me.");
}
pub fn dependent() -> Result<()> {
always_err()?;
Ok(())
}
pub fn with_context() -> Result<()> {
always_err().context("Was giving someone else a chance.")
}
}
#[test]
fn tracing() {
assert_eq!(
trace_error(None, example::dependent().err().unwrap()).len(),
1
);
{
let message = "I can't trust anyone.";
let errors = trace_error(
Some(message.to_owned()),
example::dependent().err().unwrap(),
);
assert_eq!(errors.len(), 2);
assert_eq!(errors.get(0).unwrap(), message);
}
{
let errors = trace_error(None, example::with_context().err().unwrap());
assert_eq!(errors.len(), 2);
assert_eq!(errors.get(0).unwrap(), "Was giving someone else a chance.");
}
}
async fn response_generic(
response: (StatusCode, Json<Failure>),
expected_status: StatusCode,
expected_errors: usize,
) {
let body = response.1 .0.clone();
assert_eq!(body.errors.len(), expected_errors);
let mut res = response.into_response();
assert_eq!(res.status(), expected_status);
assert_eq!(
String::from_utf8(res.body_mut().data().await.unwrap().unwrap().to_vec()).unwrap(),
json_dumps(&body).unwrap()
);
}
#[tokio::test]
async fn response_fail() {
response_generic(
Failure::fail(example::with_context().err().unwrap(), None),
StatusCode::INTERNAL_SERVER_ERROR,
2,
)
.await;
}
#[tokio::test]
async fn response_fail_with_code() {
response_generic(
Failure::fail(
example::with_context().err().unwrap(),
Some(StatusCode::IM_A_TEAPOT),
),
StatusCode::IM_A_TEAPOT,
2,
)
.await;
}
#[tokio::test]
async fn response_fail_because() {
response_generic(
Failure::fail_because(
"I missing working alone.".to_owned(),
example::with_context().err().unwrap(),
None,
),
StatusCode::INTERNAL_SERVER_ERROR,
3,
)
.await;
}
#[tokio::test]
async fn response_fail_because_with_code() {
response_generic(
Failure::fail_because(
"I missing working alone.".to_owned(),
example::with_context().err().unwrap(),
Some(StatusCode::IM_A_TEAPOT),
),
StatusCode::IM_A_TEAPOT,
3,
)
.await;
}
#[tokio::test]
async fn response_fail_directly() {
response_generic(
Failure::fail_directly("I missing working alone.".to_owned(), None),
StatusCode::INTERNAL_SERVER_ERROR,
1,
)
.await;
}
#[tokio::test]
async fn response_fail_directly_with_code() {
response_generic(
Failure::fail_directly(
"I missing working alone.".to_owned(),
Some(StatusCode::IM_A_TEAPOT),
),
StatusCode::IM_A_TEAPOT,
1,
)
.await;
}
#[tokio::test]
async fn response_crash() {
response_generic(
Failure::crash(Some(example::with_context().err().unwrap())),
StatusCode::INTERNAL_SERVER_ERROR,
2,
)
.await;
}
#[tokio::test]
async fn response_crash_blind() {
response_generic(Failure::crash(None), StatusCode::INTERNAL_SERVER_ERROR, 1).await;
}
#[test]
#[should_panic(expected = "called `Result::unwrap()` on an `Err` value: Never depend on me.")]
fn fallible() {
assert!(!example::dependent().has_not_failed());
example::dependent().get_error();
example::dependent().get_inner();
}
#[tokio::test]
async fn recoil() {
let result =
example::dependent().recoil::<Failure>(Some("Nobody is coming to help us."), None);
assert!(result.is_err());
let mut res = result.err().unwrap();
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(
String::from_utf8(res.body_mut().data().await.unwrap().unwrap().to_vec()).unwrap(),
json_dumps(
&Failure::fail_because(
"Nobody is coming to help us.".to_owned(),
example::dependent().err().unwrap(),
None
)
.1
.0
)
.unwrap()
);
}
}