use std::fmt;
use axum::http::HeaderValue;
#[derive(Debug)]
pub enum Error {
NotFound(String),
BadRequest(String),
Unauthorized,
Forbidden(String),
Validation(Vec<ValidationError>),
Redirect(String),
Io(String),
Database(String),
Cache(String),
Storage(String),
Mail(String),
Job(String),
Serialization(String),
Config(String),
Other(String),
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ValidationError {
pub field: String,
pub message: String,
}
impl Error {
#[must_use]
pub fn context(mut self, ctx: impl Into<String>) -> Self {
match &mut self {
Error::NotFound(s)
| Error::BadRequest(s)
| Error::Redirect(s)
| Error::Io(s)
| Error::Database(s)
| Error::Cache(s)
| Error::Storage(s)
| Error::Mail(s)
| Error::Job(s)
| Error::Serialization(s)
| Error::Config(s)
| Error::Other(s) => {
if s.is_empty() {
*s = ctx.into();
} else {
*s = format!("{}: {}", *s, ctx.into());
}
}
Error::Forbidden(s) => {
if s.is_empty() {
*s = ctx.into();
} else {
*s = format!("{}: {}", *s, ctx.into());
}
}
Error::Unauthorized | Error::Validation(_) => {}
}
self
}
#[must_use]
pub fn status(&self) -> u16 {
match self {
Error::NotFound(_) => 404,
Error::BadRequest(_) => 400,
Error::Unauthorized => 401,
Error::Forbidden(_) => 403,
Error::Validation(_) => 422,
Error::Redirect(_) => 400,
Error::Io(_)
| Error::Database(_)
| Error::Cache(_)
| Error::Storage(_)
| Error::Mail(_)
| Error::Job(_)
| Error::Serialization(_)
| Error::Config(_)
| Error::Other(_) => 500,
}
}
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Error::NotFound(_) => "not_found",
Error::BadRequest(_) => "bad_request",
Error::Unauthorized => "unauthorized",
Error::Forbidden(_) => "forbidden",
Error::Validation(_) => "validation_failed",
Error::Redirect(_) => "invalid_redirect",
Error::Io(_) => "io_error",
Error::Database(_) => "database_error",
Error::Cache(_) => "cache_error",
Error::Storage(_) => "storage_error",
Error::Mail(_) => "mail_error",
Error::Job(_) => "job_error",
Error::Serialization(_) => "serialization_error",
Error::Config(_) => "config_error",
Error::Other(_) => "internal_error",
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::NotFound(s) => write!(f, "not found: {s}"),
Error::BadRequest(s) => write!(f, "bad request: {s}"),
Error::Unauthorized => write!(f, "unauthorized"),
Error::Forbidden(s) => write!(f, "forbidden: {s}"),
Error::Validation(errs) => {
write!(f, "validation failed: ")?;
for (i, e) in errs.iter().enumerate() {
if i > 0 {
write!(f, "; ")?;
}
write!(f, "{}: {}", e.field, e.message)?;
}
Ok(())
}
Error::Redirect(s) => write!(f, "invalid redirect: {s}"),
Error::Io(s) => write!(f, "io error: {s}"),
Error::Database(s) => write!(f, "database error: {s}"),
Error::Cache(s) => write!(f, "cache error: {s}"),
Error::Storage(s) => write!(f, "storage error: {s}"),
Error::Mail(s) => write!(f, "mail error: {s}"),
Error::Job(s) => write!(f, "job error: {s}"),
Error::Serialization(s) => write!(f, "serialization error: {s}"),
Error::Config(s) => write!(f, "config error: {s}"),
Error::Other(s) => write!(f, "internal error: {s}"),
}
}
}
impl std::error::Error for Error {}
#[must_use]
pub fn not_found(what: impl Into<String>) -> Error {
Error::NotFound(what.into())
}
#[must_use]
pub fn bad_request(what: impl Into<String>) -> Error {
Error::BadRequest(what.into())
}
#[must_use]
pub fn forbidden(what: impl Into<String>) -> Error {
Error::Forbidden(what.into())
}
impl axum::response::IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
let status = axum::http::StatusCode::from_u16(self.status())
.unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
let is_production = matches!(
std::env::var("APP_ENV")
.ok()
.map(|v| v.to_ascii_lowercase())
.as_deref(),
Some("production") | Some("prod")
);
if is_production {
return (
status,
[(
axum::http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
)],
serde_json::json!({
"type": format!("urn:arcature:problem:{}", self.code()),
"title": self.code(),
"status": self.status(),
})
.to_string(),
)
.into_response();
}
let body = match &self {
Error::Validation(errs) => serde_json::json!({
"type": format!("urn:arcature:problem:{}", self.code()),
"title": "validation_failed",
"status": self.status(),
"errors": errs,
}),
other => serde_json::json!({
"type": format!("urn:arcature:problem:{}", other.code()),
"title": other.code(),
"status": other.status(),
"detail": other.to_string(),
}),
};
(
status,
[(
axum::http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
)],
body.to_string(),
)
.into_response()
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e.to_string())
}
}
#[cfg(feature = "database")]
impl From<sea_orm::DbErr> for Error {
fn from(e: sea_orm::DbErr) -> Self {
match e {
sea_orm::DbErr::RecordNotFound(msg) => Error::NotFound(msg),
other => Error::Database(other.to_string()),
}
}
}
#[cfg(feature = "database")]
impl From<sqlx::Error> for Error {
fn from(e: sqlx::Error) -> Self {
match e {
sqlx::Error::RowNotFound => Error::NotFound("row not found".to_string()),
other => Error::Database(other.to_string()),
}
}
}
#[cfg(feature = "cache")]
impl From<redis::RedisError> for Error {
fn from(e: redis::RedisError) -> Self {
Error::Cache(e.to_string())
}
}
#[cfg(feature = "cache")]
impl From<crate::cache::CacheError> for Error {
fn from(e: crate::cache::CacheError) -> Self {
Error::Cache(e.to_string())
}
}
#[cfg(feature = "cache")]
impl From<crate::cache::CacheConnectError> for Error {
fn from(e: crate::cache::CacheConnectError) -> Self {
Error::Cache(e.to_string())
}
}
#[cfg(feature = "mail")]
impl From<lettre::error::Error> for Error {
fn from(e: lettre::error::Error) -> Self {
Error::Mail(e.to_string())
}
}
#[cfg(feature = "mail")]
impl From<crate::mail::MailSendError> for Error {
fn from(e: crate::mail::MailSendError) -> Self {
Error::Mail(e.to_string())
}
}
#[cfg(feature = "mail")]
impl From<crate::mail::MailConfigError> for Error {
fn from(e: crate::mail::MailConfigError) -> Self {
Error::Mail(e.to_string())
}
}
#[cfg(any(feature = "storage-fs", feature = "storage-s3"))]
impl From<opendal::Error> for Error {
fn from(e: opendal::Error) -> Self {
Error::Storage(e.to_string())
}
}
#[cfg(any(feature = "storage-fs", feature = "storage-s3"))]
impl From<crate::storage::StorageError> for Error {
fn from(e: crate::storage::StorageError) -> Self {
Error::Storage(e.to_string())
}
}
#[cfg(any(feature = "storage-fs", feature = "storage-s3"))]
impl From<crate::storage::StorageConnectError> for Error {
fn from(e: crate::storage::StorageConnectError) -> Self {
Error::Storage(e.to_string())
}
}
#[cfg(any(
feature = "inertia",
feature = "api",
feature = "events",
feature = "jobs"
))]
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Serialization(e.to_string())
}
}
#[cfg(feature = "storage")]
impl From<bytes::BufMut> for Error {
fn from(_: bytes::BufMut) -> Self {
unreachable!("BufMut is an enum trait object; only used for documentation")
}
}
pub type Result<T> = std::result::Result<T, Error>;