use std::{error::Error as StdError, fmt};
use serde::{Deserialize, Serialize};
pub trait AlienErrorData {
fn code(&self) -> &'static str;
fn retryable(&self) -> bool;
fn internal(&self) -> bool;
fn message(&self) -> String;
fn http_status_code(&self) -> u16 {
500
}
fn context(&self) -> Option<serde_json::Value> {
None
}
fn retryable_inherit(&self) -> Option<bool> {
Some(self.retryable())
}
fn internal_inherit(&self) -> Option<bool> {
Some(self.internal())
}
fn http_status_code_inherit(&self) -> Option<u16> {
Some(self.http_status_code())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Default)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GenericError {
pub message: String,
}
impl std::fmt::Display for GenericError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl StdError for GenericError {}
impl AlienErrorData for GenericError {
fn code(&self) -> &'static str {
"GENERIC_ERROR"
}
fn retryable(&self) -> bool {
false
}
fn internal(&self) -> bool {
false
}
fn message(&self) -> String {
self.message.clone()
}
fn http_status_code(&self) -> u16 {
500
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct AlienError<T = GenericError>
where
T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
{
#[cfg_attr(feature = "openapi", schema(example = "NOT_FOUND", max_length = 128))]
pub code: String,
#[cfg_attr(
feature = "openapi",
schema(example = "Item not found.", max_length = 16384)
)]
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(nullable = true))]
pub context: Option<serde_json::Value>,
#[cfg_attr(feature = "openapi", schema(default = false))]
pub retryable: bool,
pub internal: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(minimum = 100, maximum = 599))]
pub http_status_code: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Option<serde_json::Value>))]
pub source: Option<Box<AlienError<GenericError>>>,
#[serde(
rename = "_error_for_pattern_matching",
skip_serializing_if = "Option::is_none"
)]
#[cfg_attr(feature = "openapi", schema(ignore))]
pub error: Option<T>,
}
impl<T> AlienError<T>
where
T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
{
pub fn new(meta: T) -> Self {
AlienError {
code: meta.code().to_string(),
message: meta.message(),
context: meta.context(),
retryable: meta.retryable(),
internal: meta.internal(),
http_status_code: Some(meta.http_status_code()),
source: None,
error: Some(meta),
}
}
}
impl AlienError<GenericError> {
pub fn from_std(err: &(dyn StdError + 'static)) -> Self {
let generic = GenericError {
message: err.to_string(),
};
let source = err.source().map(|src| Box::new(Self::from_std(src)));
AlienError {
code: generic.code().to_string(),
message: generic.message(),
context: generic.context(),
retryable: generic.retryable(),
internal: generic.internal(),
http_status_code: Some(generic.http_status_code()),
source,
error: Some(generic),
}
}
}
impl<T> fmt::Display for AlienError<T>
where
T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.code, self.message)?;
fn recurse(
e: &AlienError<GenericError>,
indent: &str,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
writeln!(f, "{}├─▶ {}: {}", indent, e.code, e.message)?;
if let Some(ref src) = e.source {
recurse(src, &format!("{}│ ", indent), f)?;
}
Ok(())
}
if let Some(ref src) = self.source {
writeln!(f)?;
recurse(src, "", f)?;
}
Ok(())
}
}
impl<T> StdError for AlienError<T>
where
T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
{
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source
.as_ref()
.map(|e| e.as_ref() as &(dyn StdError + 'static))
}
}
pub trait Context<T, E> {
fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
self,
meta: M,
) -> std::result::Result<T, AlienError<M>>;
}
impl<T, E> Context<T, E> for std::result::Result<T, AlienError<E>>
where
E: AlienErrorData + Clone + std::fmt::Debug + Serialize + 'static + Send + Sync,
{
fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
self,
meta: M,
) -> std::result::Result<T, AlienError<M>> {
self.map_err(|err| {
let mut new_err = AlienError::new(meta.clone());
if meta.retryable_inherit().is_none() {
new_err.retryable = err.retryable;
}
if meta.internal_inherit().is_none() {
new_err.internal = err.internal;
}
if meta.http_status_code_inherit().is_none() {
new_err.http_status_code = err.http_status_code;
}
let generic_err = AlienError {
code: err.code.clone(),
message: err.message.clone(),
context: err.context.clone(),
retryable: err.retryable,
internal: err.internal,
source: err.source,
error: None,
http_status_code: err.http_status_code,
};
new_err.source = Some(Box::new(generic_err));
new_err
})
}
}
pub trait ContextError<E> {
fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
self,
meta: M,
) -> AlienError<M>;
}
impl<E> ContextError<E> for AlienError<E>
where
E: AlienErrorData + Clone + std::fmt::Debug + Serialize + 'static + Send + Sync,
{
fn context<M: AlienErrorData + Clone + std::fmt::Debug + Serialize>(
self,
meta: M,
) -> AlienError<M> {
let mut new_err = AlienError::new(meta.clone());
if meta.retryable_inherit().is_none() {
new_err.retryable = self.retryable;
}
if meta.internal_inherit().is_none() {
new_err.internal = self.internal;
}
if meta.http_status_code_inherit().is_none() {
new_err.http_status_code = self.http_status_code;
}
let generic_err = AlienError {
code: self.code.clone(),
message: self.message.clone(),
context: self.context.clone(),
retryable: self.retryable,
internal: self.internal,
source: self.source,
error: None,
http_status_code: self.http_status_code,
};
new_err.source = Some(Box::new(generic_err));
new_err
}
}
pub trait IntoAlienError<T> {
fn into_alien_error(self) -> std::result::Result<T, AlienError<GenericError>>;
}
impl<T, E> IntoAlienError<T> for std::result::Result<T, E>
where
E: StdError + 'static,
{
fn into_alien_error(self) -> std::result::Result<T, AlienError<GenericError>> {
self.map_err(|err| AlienError::from_std(&err as &dyn StdError))
}
}
pub trait IntoAlienErrorDirect {
fn into_alien_error(self) -> AlienError<GenericError>;
}
impl<E> IntoAlienErrorDirect for E
where
E: StdError + 'static,
{
fn into_alien_error(self) -> AlienError<GenericError> {
AlienError::from_std(&self as &dyn StdError)
}
}
pub type Result<T, E = GenericError> = std::result::Result<T, AlienError<E>>;
impl<T> AlienError<T>
where
T: AlienErrorData + Clone + std::fmt::Debug + Serialize,
{
pub fn into_generic(self) -> AlienError<GenericError> {
AlienError {
code: self.code,
message: self.message,
context: self.context,
retryable: self.retryable,
internal: self.internal,
source: self.source,
error: None,
http_status_code: self.http_status_code,
}
}
}
pub use alien_error_derive::AlienErrorData;
#[cfg(feature = "anyhow")]
impl From<anyhow::Error> for AlienError<GenericError> {
fn from(err: anyhow::Error) -> AlienError<GenericError> {
AlienError::new(GenericError {
message: err.to_string(),
})
}
}
#[cfg(feature = "anyhow")]
pub trait IntoAnyhow<T> {
fn into_anyhow(self) -> anyhow::Result<T>;
}
#[cfg(feature = "anyhow")]
impl<T, E> IntoAnyhow<T> for std::result::Result<T, AlienError<E>>
where
E: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
{
fn into_anyhow(self) -> anyhow::Result<T> {
self.map_err(|err| anyhow::Error::new(err))
}
}
#[cfg(feature = "axum")]
impl<T> axum::response::IntoResponse for AlienError<T>
where
T: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
{
fn into_response(self) -> axum::response::Response {
self.into_external_response()
}
}
#[cfg(feature = "axum")]
impl<T> AlienError<T>
where
T: AlienErrorData + Clone + std::fmt::Debug + Serialize + Send + Sync + 'static,
{
pub fn into_internal_response(self) -> axum::response::Response {
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json};
let response_error = self.into_generic();
let status_code = response_error
.http_status_code
.and_then(|code| StatusCode::from_u16(code).ok())
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
(status_code, Json(response_error)).into_response()
}
pub fn into_external_response(self) -> axum::response::Response {
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json};
let response_error = if self.internal {
AlienError::new(GenericError {
message: "Internal server error".to_string(),
})
} else {
self.into_generic()
};
let status_code = response_error
.http_status_code
.and_then(|code| StatusCode::from_u16(code).ok())
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
(status_code, Json(response_error)).into_response()
}
}