use std::{collections::HashMap, fmt, sync::Arc};
use error_info::ErrorInfo;
use http::StatusCode;
use tracing_error::SpanTrace;
pub type Result<T, E = Box<Error>> = std::result::Result<T, E>;
#[derive(ErrorInfo)]
pub enum GenericErrorCode {
#[error(status = StatusCode::BAD_REQUEST, message = "The request is not well formed")]
BadRequest,
#[error(status = StatusCode::FORBIDDEN, message = "Forbidden access to the resource")]
Forbidden,
#[error(status = StatusCode::INTERNAL_SERVER_ERROR, message = "Internal server error")]
InternalServerError,
}
#[derive(Clone)]
pub struct Error {
pub(super) info: Arc<dyn ErrorInfo + Send + Sync + 'static>,
pub(super) reason: Option<String>,
pub(super) properties: Option<HashMap<String, serde_json::Value>>,
pub(super) unexpected: bool,
pub(super) source: Option<Arc<dyn fmt::Display + Send + Sync>>,
pub(super) context: SpanTrace,
}
struct ErrorInfoDebug {
status: StatusCode,
code: &'static str,
raw_message: &'static str,
}
impl fmt::Debug for ErrorInfoDebug {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ErrorInfo")
.field("status", &self.status)
.field("code", &self.code)
.field("raw_message", &self.raw_message)
.finish()
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Error")
.field(
"info",
&ErrorInfoDebug {
status: self.info.status(),
code: self.info.code(),
raw_message: self.info.raw_message(),
},
)
.field("reason", &self.reason)
.field("properties", &self.properties)
.field("source", &self.source.as_ref().map(|s| s.to_string()))
.field("context", &self.context)
.finish()
}
}
impl Error {
pub fn new(info: impl ErrorInfo + Send + Sync + 'static, unexpected: bool) -> Self {
Self {
info: Arc::new(info),
reason: None,
properties: None,
unexpected,
source: None,
context: SpanTrace::capture(),
}
}
pub fn internal(reason: impl Into<String>) -> Self {
Self::new(GenericErrorCode::InternalServerError, true).with_reason(reason)
}
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
pub fn with_source<S: fmt::Display + Send + Sync + 'static>(mut self, source: S) -> Self {
self.source = Some(Arc::new(source));
self
}
pub fn with_str_property(mut self, key: &str, value: impl Into<String>) -> Self {
self.properties
.get_or_insert_with(HashMap::new)
.insert(key.to_string(), serde_json::Value::String(value.into()));
self
}
pub fn with_property(mut self, key: &str, value: serde_json::Value) -> Self {
self.properties
.get_or_insert_with(HashMap::new)
.insert(key.to_string(), value);
self
}
pub fn boxed(self) -> Box<Self> {
Box::new(self)
}
pub fn info(&self) -> &dyn ErrorInfo {
self.info.as_ref()
}
pub fn is_unexpected(&self) -> bool {
self.unexpected
}
pub fn reason(&self) -> Option<&str> {
self.reason.as_deref()
}
pub fn properties(&self) -> Option<&HashMap<String, serde_json::Value>> {
self.properties.as_ref()
}
pub(super) fn reason_or_message(&self) -> String {
self.reason.clone().unwrap_or(self.info.message())
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = self.info.status();
write!(
f,
"[{} {}] {}: {}",
status.as_str(),
status.canonical_reason().unwrap_or("Unknown"),
self.info.code(),
self.reason_or_message()
)?;
if f.alternate() {
if let Some(source) = &self.source {
write!(f, "\nCaused by: {source}")?;
}
write!(f, "\n{}", self.context)
} else {
Ok(())
}
}
}
impl From<&str> for Error {
fn from(reason: &str) -> Self {
Self::internal(reason)
}
}
impl From<String> for Error {
fn from(reason: String) -> Self {
Self::internal(reason)
}
}
impl<T: ErrorInfo + Send + Sync + 'static> From<(T,)> for Error {
fn from((code,): (T,)) -> Self {
let status = code.status();
Self::new(code, status.is_server_error())
}
}
impl<T: ErrorInfo + Send + Sync + 'static, S: Into<String>> From<(T, S)> for Error {
fn from((code, reason): (T, S)) -> Self {
let status = code.status();
Self::new(code, status.is_server_error()).with_reason(reason)
}
}
impl From<&str> for Box<Error> {
fn from(reason: &str) -> Self {
Error::from(reason).boxed()
}
}
impl From<String> for Box<Error> {
fn from(reason: String) -> Self {
Error::from(reason).boxed()
}
}
impl<T: ErrorInfo + Send + Sync + 'static> From<(T,)> for Box<Error> {
fn from(t: (T,)) -> Self {
Error::from(t).boxed()
}
}
impl<T: ErrorInfo + Send + Sync + 'static, S: Into<String>> From<(T, S)> for Box<Error> {
fn from(t: (T, S)) -> Self {
Error::from(t).boxed()
}
}
pub trait MapToErr<T> {
fn map_to_internal_err(self, reason: impl Into<String>) -> Result<T>;
fn map_to_err(self, code: impl ErrorInfo + Send + Sync + 'static, reason: impl Into<String>) -> Result<T>;
}
impl<T, E: fmt::Display + Send + Sync + 'static> MapToErr<T> for Result<T, E> {
fn map_to_internal_err(self, reason: impl Into<String>) -> Result<T> {
self.map_err(|err| Error::internal(reason.into()).with_source(err).boxed())
}
fn map_to_err(self, code: impl ErrorInfo + Send + Sync + 'static, reason: impl Into<String>) -> Result<T> {
self.map_err(|err| {
let unexpected = code.status().is_server_error();
Error::new(code, unexpected)
.with_reason(reason.into())
.with_source(err)
.boxed()
})
}
}
pub trait ResultExt {
fn with_str_property(self, key: &str, value: impl Into<String>) -> Self
where
Self: Sized,
{
self.with_property(key, serde_json::Value::String(value.into()))
}
fn with_property(self, key: &str, value: serde_json::Value) -> Self;
}
impl<T> ResultExt for Result<T> {
fn with_property(self, key: &str, value: serde_json::Value) -> Self {
self.map_err(|err| err.with_property(key, value).boxed())
}
}