use std::{error::Error as StdError, fmt};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum HumanLayerPresentation {
#[default]
Normal,
Transparent,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HumanErrorCause {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HumanErrorReport {
pub code: String,
pub message: String,
pub hint: Option<String>,
pub causes: Vec<HumanErrorCause>,
}
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())
}
fn human_layer_presentation(&self) -> HumanLayerPresentation {
HumanLayerPresentation::Normal
}
fn hint(&self) -> Option<String> {
None
}
}
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(nullable = true))]
pub hint: Option<String>,
#[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(skip, default)]
#[cfg_attr(feature = "openapi", schema(ignore))]
pub human_layer_presentation: HumanLayerPresentation,
#[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(),
hint: meta.hint(),
retryable: meta.retryable(),
internal: meta.internal(),
http_status_code: Some(meta.http_status_code()),
source: None,
human_layer_presentation: meta.human_layer_presentation(),
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(),
hint: generic.hint(),
retryable: generic.retryable(),
internal: generic.internal(),
http_status_code: Some(generic.http_status_code()),
source,
human_layer_presentation: HumanLayerPresentation::Normal,
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(),
hint: err.hint.clone(),
retryable: err.retryable,
internal: err.internal,
source: err.source,
human_layer_presentation: err.human_layer_presentation,
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(),
hint: self.hint.clone(),
retryable: self.retryable,
internal: self.internal,
source: self.source,
human_layer_presentation: self.human_layer_presentation,
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,
hint: self.hint,
retryable: self.retryable,
internal: self.internal,
source: self.source,
human_layer_presentation: self.human_layer_presentation,
error: None,
http_status_code: self.http_status_code,
}
}
pub fn human_report(&self) -> HumanErrorReport {
let mut layers = Vec::new();
collect_human_layers(
&self.code,
&self.message,
self.hint.as_deref(),
self.human_layer_presentation,
self.source.as_deref(),
&mut layers,
);
let headline_index = layers
.iter()
.position(|layer| layer.presentation == HumanLayerPresentation::Normal)
.unwrap_or(0);
let headline = &layers[headline_index];
let mut causes = Vec::new();
for (index, layer) in layers.iter().enumerate() {
if index == headline_index || layer.presentation == HumanLayerPresentation::Transparent
{
continue;
}
if causes.iter().any(|cause: &HumanErrorCause| {
cause.code == layer.code && cause.message == layer.message
}) {
continue;
}
causes.push(HumanErrorCause {
code: layer.code.clone(),
message: layer.message.clone(),
});
}
HumanErrorReport {
code: headline.code.clone(),
message: headline.message.clone(),
hint: headline.hint.clone().or_else(|| {
layers
.iter()
.enumerate()
.find(|(index, layer)| {
*index != headline_index
&& layer.presentation == HumanLayerPresentation::Normal
&& layer.hint.is_some()
})
.and_then(|(_, layer)| layer.hint.clone())
}),
causes,
}
}
}
#[derive(Debug, Clone)]
struct HumanLayer {
code: String,
message: String,
hint: Option<String>,
presentation: HumanLayerPresentation,
}
fn collect_human_layers(
code: &str,
message: &str,
hint: Option<&str>,
presentation: HumanLayerPresentation,
source: Option<&AlienError<GenericError>>,
layers: &mut Vec<HumanLayer>,
) {
layers.push(HumanLayer {
code: code.to_string(),
message: message.to_string(),
hint: hint.map(ToOwned::to_owned),
presentation,
});
if let Some(source) = source {
collect_human_layers(
&source.code,
&source.message,
source.hint.as_deref(),
source.human_layer_presentation,
source.source.as_deref(),
layers,
);
}
}
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()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Serialize)]
enum TestError {
Normal,
Transparent,
Hint,
}
impl AlienErrorData for TestError {
fn code(&self) -> &'static str {
match self {
Self::Normal => "NORMAL",
Self::Transparent => "WRAPPER",
Self::Hint => "HINT",
}
}
fn retryable(&self) -> bool {
false
}
fn internal(&self) -> bool {
false
}
fn message(&self) -> String {
match self {
Self::Normal => "Inner failure".to_string(),
Self::Transparent => "Wrapper failure".to_string(),
Self::Hint => "Action required".to_string(),
}
}
fn human_layer_presentation(&self) -> HumanLayerPresentation {
match self {
Self::Normal => HumanLayerPresentation::Normal,
Self::Transparent => HumanLayerPresentation::Transparent,
Self::Hint => HumanLayerPresentation::Normal,
}
}
fn hint(&self) -> Option<String> {
match self {
Self::Hint => Some("Run the setup command first.".to_string()),
_ => None,
}
}
}
#[test]
fn human_report_skips_transparent_wrappers() {
let err = Err::<(), _>(AlienError::new(TestError::Normal))
.context(TestError::Transparent)
.unwrap_err();
let report = err.human_report();
assert_eq!(report.code, "NORMAL");
assert_eq!(report.message, "Inner failure");
assert!(report.causes.is_empty());
}
#[test]
fn human_report_keeps_distinct_non_transparent_causes() {
let source = AlienError::new(GenericError {
message: "Socket closed".to_string(),
});
let err = Err::<(), _>(source).context(TestError::Normal).unwrap_err();
let report = err.human_report();
assert_eq!(report.code, "NORMAL");
assert_eq!(report.message, "Inner failure");
assert_eq!(report.causes.len(), 1);
assert_eq!(report.causes[0].code, "GENERIC_ERROR");
assert_eq!(report.causes[0].message, "Socket closed");
}
#[test]
fn human_report_uses_headline_hint_when_present() {
let err = AlienError::new(TestError::Hint);
let report = err.human_report();
assert_eq!(report.code, "HINT");
assert_eq!(report.message, "Action required");
assert_eq!(report.hint.as_deref(), Some("Run the setup command first."));
}
#[test]
fn human_report_uses_first_visible_hint_from_causes() {
let err = Err::<(), _>(AlienError::new(TestError::Hint))
.context(TestError::Transparent)
.unwrap_err();
let report = err.human_report();
assert_eq!(report.code, "HINT");
assert_eq!(report.hint.as_deref(), Some("Run the setup command first."));
}
}