use thiserror::Error;
#[derive(Error, Debug)]
pub enum DomainError {
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Unauthorized")]
Unauthorized,
#[error("Forbidden: {0}")]
Forbidden(String),
#[error("Invalid user id in token")]
InvalidTokenUserId,
#[error("User not found")]
UserNotFound,
#[error("User with id {0} not found")]
UserNotFoundById(i64),
#[error("Not found: {0}")]
NotFound(String),
#[error("Config not initialized")]
ConfigNotInitialized,
#[error("Provider not configured")]
ProviderNotConfigured,
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Duplicate entry: {0}")]
DuplicateEntry(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("External service error: {0}")]
ExternalService(String),
#[error("Failed to issue token: {0}")]
TokenIssueFailed(String),
#[error("Internal error: {0}")]
Internal(String),
#[error("Invalid tenant name: {0}")]
TenantInvalid(String),
#[error("Tenant not found: {0}")]
TenantNotFound(String),
#[error("Tenant mismatch: JWT tenant does not match URL tenant")]
TenantMismatch,
#[error("Upgrade required: feature '{feature}' requires database version {required_version} (current: {current_version})")]
UpgradeRequired {
current_version: i32,
required_version: i32,
feature: String,
},
}
impl DomainError {
pub fn code(&self) -> &'static str {
match self {
DomainError::InvalidCredentials => "INVALID_CREDENTIALS",
DomainError::Unauthorized => "UNAUTHORIZED",
DomainError::Forbidden(_) => "FORBIDDEN",
DomainError::InvalidTokenUserId => "BAD_REQUEST",
DomainError::UserNotFound => "NOT_FOUND",
DomainError::UserNotFoundById(_) => "NOT_FOUND",
DomainError::NotFound(_) => "NOT_FOUND",
DomainError::ConfigNotInitialized => "SERVICE_UNAVAILABLE",
DomainError::ProviderNotConfigured => "SERVICE_UNAVAILABLE",
DomainError::InvalidInput(_) => "BAD_REQUEST",
DomainError::DuplicateEntry(_) => "CONFLICT",
DomainError::DatabaseError(_) => "INTERNAL",
DomainError::ExternalService(_) => "BAD_GATEWAY",
DomainError::TokenIssueFailed(_) => "INTERNAL",
DomainError::Internal(_) => "INTERNAL",
DomainError::TenantInvalid(_) => "BAD_REQUEST",
DomainError::TenantNotFound(_) => "NOT_FOUND",
DomainError::TenantMismatch => "FORBIDDEN",
DomainError::UpgradeRequired { .. } => "UPGRADE_REQUIRED",
}
}
pub fn is_not_found(&self) -> bool {
matches!(
self,
DomainError::UserNotFound
| DomainError::UserNotFoundById(_)
| DomainError::NotFound(_)
| DomainError::TenantNotFound(_)
)
}
pub fn is_auth_error(&self) -> bool {
matches!(
self,
DomainError::InvalidCredentials
| DomainError::Unauthorized
| DomainError::Forbidden(_)
| DomainError::TenantMismatch
)
}
pub fn is_validation_error(&self) -> bool {
matches!(
self,
DomainError::InvalidInput(_)
| DomainError::DuplicateEntry(_)
| DomainError::TenantInvalid(_)
)
}
}
pub type DomainResult<T> = Result<T, DomainError>;
impl From<sea_orm::DbErr> for DomainError {
fn from(err: sea_orm::DbErr) -> Self {
let msg = err.to_string();
if msg.contains("Duplicate entry") || msg.contains("1062") {
DomainError::DuplicateEntry(msg)
} else {
DomainError::DatabaseError(msg)
}
}
}
impl From<anyhow::Error> for DomainError {
fn from(err: anyhow::Error) -> Self {
match err.downcast::<DomainError>() {
Ok(domain_err) => domain_err,
Err(err) => DomainError::Internal(err.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_codes() {
assert_eq!(DomainError::InvalidCredentials.code(), "INVALID_CREDENTIALS");
assert_eq!(DomainError::Unauthorized.code(), "UNAUTHORIZED");
assert_eq!(DomainError::NotFound("test".into()).code(), "NOT_FOUND");
assert_eq!(DomainError::InvalidInput("test".into()).code(), "BAD_REQUEST");
}
#[test]
fn test_is_not_found() {
assert!(DomainError::UserNotFound.is_not_found());
assert!(DomainError::NotFound("test".into()).is_not_found());
assert!(!DomainError::InvalidCredentials.is_not_found());
}
#[test]
fn test_is_auth_error() {
assert!(DomainError::InvalidCredentials.is_auth_error());
assert!(DomainError::Unauthorized.is_auth_error());
assert!(DomainError::Forbidden("test".into()).is_auth_error());
assert!(!DomainError::NotFound("test".into()).is_auth_error());
}
}