use core::{
error::Error as CoreError,
fmt::{self, Display, Formatter}
};
#[cfg(feature = "axum")]
use axum::http::StatusCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppErrorKind {
NotFound,
Validation,
Conflict,
Unauthorized,
Forbidden,
NotImplemented,
Internal,
BadRequest,
TelegramAuth,
InvalidJwt,
Database,
Service,
Config,
Turnkey,
Timeout,
Network,
RateLimited,
DependencyUnavailable,
Serialization,
Deserialization,
ExternalApi,
Queue,
Cache
}
#[cfg(not(feature = "colored"))]
impl Display for AppErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[cfg(feature = "colored")]
impl Display for AppErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
use crate::colored::style;
let label = self.label();
let styled = if self.is_critical() {
style::error_kind_critical(label)
} else {
style::error_kind_warning(label)
};
f.write_str(&styled)
}
}
impl CoreError for AppErrorKind {}
impl AppErrorKind {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::NotFound => "Not found",
Self::Validation => "Validation error",
Self::Conflict => "Conflict",
Self::Unauthorized => "Unauthorized",
Self::Forbidden => "Forbidden",
Self::NotImplemented => "Not implemented",
Self::Internal => "Internal server error",
Self::BadRequest => "Bad request",
Self::TelegramAuth => "Telegram authentication error",
Self::InvalidJwt => "Invalid JWT",
Self::Database => "Database error",
Self::Service => "Service error",
Self::Config => "Configuration error",
Self::Turnkey => "Turnkey error",
Self::Timeout => "Operation timed out",
Self::Network => "Network error",
Self::RateLimited => "Rate limit exceeded",
Self::DependencyUnavailable => "External dependency unavailable",
Self::Serialization => "Serialization error",
Self::Deserialization => "Deserialization error",
Self::ExternalApi => "External API error",
Self::Queue => "Queue processing error",
Self::Cache => "Cache error"
}
}
pub fn http_status(&self) -> u16 {
match self {
AppErrorKind::NotFound => 404,
AppErrorKind::Validation => 422,
AppErrorKind::Conflict => 409,
AppErrorKind::Unauthorized | AppErrorKind::InvalidJwt | AppErrorKind::TelegramAuth => {
401
}
AppErrorKind::Forbidden => 403,
AppErrorKind::NotImplemented => 501,
AppErrorKind::BadRequest => 400,
AppErrorKind::RateLimited => 429,
AppErrorKind::Timeout => 504,
AppErrorKind::Network | AppErrorKind::DependencyUnavailable => 503,
AppErrorKind::Serialization
| AppErrorKind::Deserialization
| AppErrorKind::ExternalApi
| AppErrorKind::Queue
| AppErrorKind::Cache
| AppErrorKind::Database
| AppErrorKind::Service
| AppErrorKind::Config
| AppErrorKind::Turnkey
| AppErrorKind::Internal => 500
}
}
#[cfg(feature = "axum")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
pub fn status_code(&self) -> StatusCode {
StatusCode::from_u16(self.http_status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
#[cfg(feature = "colored")]
pub(crate) fn is_critical(&self) -> bool {
self.http_status() >= 500
}
}
#[cfg(test)]
mod tests {
use super::AppErrorKind::*;
#[test]
fn http_status_is_stable() {
assert_eq!(NotFound.http_status(), 404);
assert_eq!(Validation.http_status(), 422);
assert_eq!(Unauthorized.http_status(), 401);
assert_eq!(Forbidden.http_status(), 403);
assert_eq!(Conflict.http_status(), 409);
assert_eq!(BadRequest.http_status(), 400);
assert_eq!(RateLimited.http_status(), 429);
assert_eq!(Timeout.http_status(), 504);
assert_eq!(DependencyUnavailable.http_status(), 503);
assert_eq!(Internal.http_status(), 500);
}
#[test]
#[cfg(feature = "colored")]
fn is_critical_identifies_server_errors() {
assert!(Internal.is_critical());
assert!(Database.is_critical());
assert!(Service.is_critical());
assert!(Config.is_critical());
assert!(Timeout.is_critical());
assert!(Network.is_critical());
assert!(DependencyUnavailable.is_critical());
assert!(Serialization.is_critical());
assert!(Deserialization.is_critical());
assert!(ExternalApi.is_critical());
assert!(Queue.is_critical());
assert!(Cache.is_critical());
assert!(Turnkey.is_critical());
assert!(NotImplemented.is_critical());
}
#[test]
#[cfg(feature = "colored")]
fn is_critical_excludes_client_errors() {
assert!(!NotFound.is_critical());
assert!(!Validation.is_critical());
assert!(!Conflict.is_critical());
assert!(!Unauthorized.is_critical());
assert!(!Forbidden.is_critical());
assert!(!BadRequest.is_critical());
assert!(!TelegramAuth.is_critical());
assert!(!InvalidJwt.is_critical());
assert!(!RateLimited.is_critical());
}
#[test]
fn display_shows_label() {
assert_eq!(NotFound.to_string(), "Not found");
assert_eq!(Internal.to_string(), "Internal server error");
assert_eq!(BadRequest.to_string(), "Bad request");
}
#[test]
#[cfg(feature = "colored")]
fn display_colored_contains_label() {
let output = Internal.to_string();
assert!(output.contains("Internal server error"));
let output = BadRequest.to_string();
assert!(output.contains("Bad request"));
}
}