use std::{borrow::Cow, collections::HashMap, panic::Location};
use backtrace::Backtrace;
use eyre::EyreContext;
use parking_lot::Once;
use serde::ser::SerializeMap;
mod macros;
#[cfg(feature = "actix")]
mod actix;
#[cfg(feature = "axum")]
mod axum;
mod commons;
mod custom;
pub(crate) use self::custom::*;
mod ext;
pub mod http;
pub mod reporter;
use self::reporter::Report;
#[cfg(feature = "sql")]
pub mod sql;
pub mod prelude {
#[cfg(feature = "actix")]
pub use super::actix::*;
#[cfg(feature = "axum")]
pub use super::axum::*;
#[cfg(feature = "sql")]
pub use super::sql::*;
pub use super::{commons::*, custom::*, ext::*, http, Problem, Result};
}
pub(crate) fn blank_type_uri() -> custom::Uri {
custom::Uri::from_static("about:blank")
}
pub type CowStr = Cow<'static, str>;
pub type Result<T, E = Problem> = std::result::Result<T, E>;
fn install() {
static HOOK_INSTALLED: Once = Once::new();
HOOK_INSTALLED.call_once(|| {
eyre::set_hook(Box::new(crate::reporter::capture_handler))
.expect("Failed to set error hook, maybe install was already called?");
})
}
#[derive(Default)]
pub struct Problem {
inner: Box<ProblemInner>,
}
#[derive(Debug)]
struct ProblemInner {
r#type: Uri,
title: CowStr,
status: StatusCode,
details: CowStr,
cause: eyre::Report,
extensions: Extensions,
}
impl Default for ProblemInner {
fn default() -> Self {
Self {
r#type: blank_type_uri(),
title: Cow::Borrowed(""),
status: StatusCode::default(),
details: Cow::Borrowed(""),
cause: eyre::Report::msg(""),
extensions: Extensions::default(),
}
}
}
impl ProblemInner {
fn report(&self) -> &Report {
self.cause
.handler()
.downcast_ref::<Report>()
.expect("Problem used without installation")
}
}
impl serde::Serialize for Problem {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(None)?;
map.serialize_entry(&"status", &self.status().as_u16())?;
if !matches!(self.type_().scheme_str(), None | Some("about")) {
map.serialize_entry(&"type", &format_args!("{}", self.type_()))?;
}
map.serialize_entry(&"title", &self.title())?;
map.serialize_entry(&"detail", &self.details())?;
for (k, v) in &self.extensions().inner {
map.serialize_entry(k, v)?;
}
map.end()
}
}
impl std::fmt::Debug for Problem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.inner.report().debug(self.cause(), f)
}
}
impl std::fmt::Display for Problem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use eyre::EyreHandler;
writeln!(
f,
"{} - {}: {}",
self.status(),
self.title(),
self.details()
)?;
self.inner.report().display(&*self.inner.cause, f)?;
Ok(())
}
}
impl std::error::Error for Problem {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.cause())
}
}
impl Problem {
pub(crate) fn report_as_error(&self) {
if let Some(reporter) = self::reporter::global_reporter() {
if reporter.should_report_error(self) {
reporter.report_error(self);
}
}
}
}
impl Problem {
#[track_caller]
pub fn custom(status: StatusCode, r#type: Uri) -> Self {
let mut problem = Self::from_status(status);
problem.inner.r#type = r#type;
problem
}
#[track_caller]
pub fn from_status(status: StatusCode) -> Self {
install();
let title = status.canonical_reason().unwrap();
Self {
inner: Box::new(ProblemInner {
title: title.into(),
cause: eyre::Report::msg(title),
status,
..ProblemInner::default()
}),
}
}
#[must_use]
pub fn with_title(mut self, title: impl Into<CowStr>) -> Self {
self.inner.title = title.into();
self
}
#[must_use]
pub fn with_detail(mut self, detail: impl Into<CowStr>) -> Self {
self.inner.details = detail.into();
self
}
#[must_use]
#[track_caller]
pub fn with_cause<E>(mut self, cause: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
self.inner.cause = eyre::Report::new(cause);
self
}
#[must_use]
pub fn with_extension<E, V>(mut self, extension: E, value: V) -> Self
where
E: Into<CowStr>,
V: serde::Serialize,
{
let extension = extension.into();
match extension.as_ref() {
"type" | "status" | "details" | "cause" | "" => {
panic!("Invalid extension received: {}", extension)
}
_ => self.inner.extensions.insert(extension, value),
}
self
}
}
impl Problem {
pub const fn type_(&self) -> &Uri {
&self.inner.r#type
}
pub fn title(&self) -> &str {
&self.inner.title
}
pub const fn status(&self) -> StatusCode {
self.inner.status
}
pub fn details(&self) -> &str {
&self.inner.details
}
pub const fn extensions(&self) -> &Extensions {
&self.inner.extensions
}
pub fn extensions_mut(&mut self) -> &mut Extensions {
&mut self.inner.extensions
}
pub fn cause(&self) -> &(dyn std::error::Error + 'static) {
&*self.inner.cause
}
}
impl Problem {
#[must_use]
pub fn report(&self) -> &Report {
self.inner.report()
}
pub fn backtrace(&self) -> Backtrace {
(*self.inner.report().backtrace()).clone()
}
pub fn location(&self) -> &'static Location<'static> {
self.inner.report().location()
}
pub fn is<E>(&self) -> bool
where
E: std::error::Error + Send + Sync + 'static,
{
self.inner.cause.is::<E>()
}
pub fn downcast<E>(mut self) -> Result<E, Self>
where
E: std::error::Error + Send + Sync + 'static,
{
match self.inner.cause.downcast() {
Ok(err) => Ok(err),
Err(cause) => {
self.inner.cause = cause;
Err(self)
}
}
}
pub fn downcast_ref<E>(&self) -> Option<&E>
where
E: std::error::Error + Send + Sync + 'static,
{
self.inner.cause.downcast_ref()
}
pub fn isolate<E>(self) -> Result<Self, Self>
where
E: std::error::Error + Send + Sync + 'static,
{
if self.is::<E>() {
Err(self)
} else {
Ok(self)
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize)]
#[serde(transparent)]
pub struct Extensions {
inner: HashMap<CowStr, serde_json::Value>,
}
impl Extensions {
pub fn insert<K, V>(&mut self, key: K, value: V)
where
K: Into<CowStr>,
V: serde::Serialize,
{
self.inner.insert(key.into(), serde_json::json!(value));
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl<'e> IntoIterator for &'e Extensions {
type IntoIter = ExtensionsIter<'e>;
type Item = (&'e str, &'e serde_json::Value);
fn into_iter(self) -> Self::IntoIter {
ExtensionsIter(self.inner.iter().map(|(k, v)| (&**k, v)))
}
}
use std::{collections::hash_map::Iter, iter::Map};
#[doc(hidden)]
#[allow(clippy::type_complexity)]
pub struct ExtensionsIter<'e>(
Map<
Iter<'e, Cow<'e, str>, serde_json::Value>,
for<'a> fn((&'a Cow<'a, str>, &'a serde_json::Value)) -> (&'a str, &'a serde_json::Value),
>,
);
impl<'e> Iterator for ExtensionsIter<'e> {
type Item = (&'e str, &'e serde_json::Value);
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use serde_json::json;
use super::*;
#[test]
fn test_extensions() {
let mut ext = Extensions::default();
assert!(ext.is_empty());
assert_eq!(ext.len(), 0);
assert!(ext.into_iter().next().is_none());
ext.insert("bla", "bla");
assert_eq!(ext.len(), 1);
assert!(!ext.is_empty());
assert_eq!(ext.into_iter().next(), Some(("bla", &json!("bla"))));
assert_eq!(json!(ext), json!({ "bla": "bla" }));
}
#[test]
fn test_problem_with_extensions_good() {
let mut error = http::failed_precondition();
for (key, value) in [
("bla", json!("bla")),
("foo", json!(1)),
("bar", json!(1.2)),
("baz", json!([1.2])),
] {
error = error.with_extension(key, value);
}
assert_eq!(error.extensions().len(), 4);
}
macro_rules! test_invalid_extension {
($test_fn: ident, $ext: literal) => {
#[test]
#[should_panic = concat!("Invalid extension received: ", $ext)]
fn $test_fn() {
let _res = http::failed_precondition().with_extension($ext, json!(1));
}
};
}
test_invalid_extension!(test_problem_with_extension_type, "type");
test_invalid_extension!(test_problem_with_extension_status, "status");
test_invalid_extension!(test_problem_with_extension_details, "details");
test_invalid_extension!(test_problem_with_extension_cause, "cause");
test_invalid_extension!(test_problem_with_extension_empty, "");
#[test]
fn test_problem_getter_type_() {
assert_eq!(http::failed_precondition().type_(), "about:blank");
}
#[test]
fn test_problem_getter_report() {
let err = http::failed_precondition();
let report = err.report();
assert_eq!(err.location(), report.location());
}
#[test]
fn test_problem_error_handling() {
let err = http::failed_precondition();
assert!(err.is::<http::PreconditionFailed>());
assert!(err.downcast_ref::<http::PreconditionFailed>().is_some());
assert!(err.isolate::<http::PreconditionFailed>().is_err());
let err = http::failed_precondition();
assert!(!err.is::<http::NotFound>());
assert!(err.downcast_ref::<http::NotFound>().is_none());
assert!(err.isolate::<http::NotFound>().is_ok());
let err = http::failed_precondition();
assert!(err.downcast::<http::PreconditionFailed>().is_ok());
let err = http::failed_precondition();
assert!(err.downcast::<http::NotFound>().is_err());
}
#[test]
fn test_problem_source() {
let err = http::failed_precondition();
let source = err.source().unwrap() as *const dyn Error as *const ();
let cause = err.cause() as *const dyn Error as *const ();
assert!(core::ptr::eq(source, cause));
}
#[test]
fn test_problem_serialize_no_type() {
let err = http::failed_precondition()
.with_detail("Failed a precondition")
.with_extension("foo", "bar");
assert_eq!(
json!(err),
json!({
"detail": "Failed a precondition",
"foo": "bar",
"status": 412,
"title": "Precondition Failed",
})
);
}
#[test]
fn test_problem_serialize_type() {
let err = Problem::custom(
StatusCode::PRECONDITION_FAILED,
Uri::from_static("https://my.beautiful.error"),
)
.with_detail("Failed a precondition")
.with_extension("foo", "bar");
assert_eq!(
json!(err),
json!({
"detail": "Failed a precondition",
"foo": "bar",
"status": 412,
"title": "Precondition Failed",
"type": "https://my.beautiful.error/",
})
);
}
}