use connectrpc::ErrorCode;
use http::StatusCode;
pub type ServiceResult<T> = Result<T, ServiceError>;
#[derive(Debug, Clone)]
pub enum ServiceError {
InvalidArgument(String),
NotFound(String),
AlreadyExists(String),
PermissionDenied(String),
Unauthenticated(String),
Internal(String),
Unavailable(String),
Unimplemented(String),
DeadlineExceeded(String),
Database(String),
Configuration(String),
Serialization(String),
}
impl ServiceError {
pub fn code(&self) -> ErrorCode {
match self {
ServiceError::InvalidArgument(_) => ErrorCode::InvalidArgument,
ServiceError::NotFound(_) => ErrorCode::NotFound,
ServiceError::AlreadyExists(_) => ErrorCode::AlreadyExists,
ServiceError::PermissionDenied(_) => ErrorCode::PermissionDenied,
ServiceError::Unauthenticated(_) => ErrorCode::Unauthenticated,
ServiceError::Internal(_) => ErrorCode::Internal,
ServiceError::Unavailable(_) => ErrorCode::Unavailable,
ServiceError::Unimplemented(_) => ErrorCode::Unimplemented,
ServiceError::DeadlineExceeded(_) => ErrorCode::DeadlineExceeded,
ServiceError::Database(_) => ErrorCode::Internal,
ServiceError::Configuration(_) => ErrorCode::Internal,
ServiceError::Serialization(_) => ErrorCode::Internal,
}
}
pub fn http_status(&self) -> StatusCode {
match self.code() {
ErrorCode::InvalidArgument => StatusCode::BAD_REQUEST,
ErrorCode::NotFound => StatusCode::NOT_FOUND,
ErrorCode::AlreadyExists => StatusCode::CONFLICT,
ErrorCode::PermissionDenied => StatusCode::FORBIDDEN,
ErrorCode::Unauthenticated => StatusCode::UNAUTHORIZED,
ErrorCode::Internal => StatusCode::INTERNAL_SERVER_ERROR,
ErrorCode::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
ErrorCode::Unimplemented => StatusCode::NOT_IMPLEMENTED,
ErrorCode::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub fn message(&self) -> &str {
match self {
ServiceError::InvalidArgument(msg) => msg,
ServiceError::NotFound(msg) => msg,
ServiceError::AlreadyExists(msg) => msg,
ServiceError::PermissionDenied(msg) => msg,
ServiceError::Unauthenticated(msg) => msg,
ServiceError::Internal(msg) => msg,
ServiceError::Unavailable(msg) => msg,
ServiceError::Unimplemented(msg) => msg,
ServiceError::DeadlineExceeded(msg) => msg,
ServiceError::Database(msg) => msg,
ServiceError::Configuration(msg) => msg,
ServiceError::Serialization(msg) => msg,
}
}
}
impl std::fmt::Display for ServiceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ServiceError::InvalidArgument(msg) => write!(f, "Invalid argument: {}", msg),
ServiceError::NotFound(msg) => write!(f, "Not found: {}", msg),
ServiceError::AlreadyExists(msg) => write!(f, "Already exists: {}", msg),
ServiceError::PermissionDenied(msg) => write!(f, "Permission denied: {}", msg),
ServiceError::Unauthenticated(msg) => write!(f, "Unauthenticated: {}", msg),
ServiceError::Internal(msg) => write!(f, "Internal error: {}", msg),
ServiceError::Unavailable(msg) => write!(f, "Service unavailable: {}", msg),
ServiceError::Unimplemented(msg) => write!(f, "Unimplemented: {}", msg),
ServiceError::DeadlineExceeded(msg) => write!(f, "Deadline exceeded: {}", msg),
ServiceError::Database(msg) => write!(f, "Database error: {}", msg),
ServiceError::Configuration(msg) => write!(f, "Configuration error: {}", msg),
ServiceError::Serialization(msg) => write!(f, "Serialization error: {}", msg),
}
}
}
impl std::error::Error for ServiceError {}
impl From<ServiceError> for connectrpc::ConnectError {
fn from(err: ServiceError) -> Self {
connectrpc::ConnectError::new(err.code(), err.message())
}
}
impl From<connectrpc::ConnectError> for ServiceError {
fn from(err: connectrpc::ConnectError) -> Self {
let msg = err.message.unwrap_or_default();
match err.code {
ErrorCode::InvalidArgument => ServiceError::InvalidArgument(msg),
ErrorCode::NotFound => ServiceError::NotFound(msg),
ErrorCode::AlreadyExists => ServiceError::AlreadyExists(msg),
ErrorCode::PermissionDenied => ServiceError::PermissionDenied(msg),
ErrorCode::Unauthenticated => ServiceError::Unauthenticated(msg),
ErrorCode::Internal => ServiceError::Internal(msg),
ErrorCode::Unavailable => ServiceError::Unavailable(msg),
ErrorCode::Unimplemented => ServiceError::Unimplemented(msg),
ErrorCode::DeadlineExceeded => ServiceError::DeadlineExceeded(msg),
_ => ServiceError::Internal(msg),
}
}
}
impl<E: std::error::Error + Send + Sync + 'static> From<Box<E>> for ServiceError {
fn from(err: Box<E>) -> Self {
ServiceError::Internal(err.to_string())
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for ServiceError {
fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
ServiceError::Internal(err.to_string())
}
}
impl From<anyhow::Error> for ServiceError {
fn from(err: anyhow::Error) -> Self {
ServiceError::Internal(err.to_string())
}
}
impl From<serde_json::Error> for ServiceError {
fn from(err: serde_json::Error) -> Self {
ServiceError::Serialization(err.to_string())
}
}
impl From<crate::config::ConfigError> for ServiceError {
fn from(err: crate::config::ConfigError) -> Self {
ServiceError::Configuration(err.to_string())
}
}
#[cfg(feature = "sqlx")]
impl From<sqlx::Error> for ServiceError {
fn from(err: sqlx::Error) -> Self {
ServiceError::Database(err.to_string())
}
}
#[cfg(feature = "nats")]
impl<T> From<async_nats::error::Error<T>> for ServiceError
where
T: std::clone::Clone + std::fmt::Debug + std::fmt::Display + PartialEq,
{
fn from(err: async_nats::error::Error<T>) -> Self {
ServiceError::Internal(format!("NATS error: {}", err))
}
}
#[cfg(test)]
mod tests {
use super::*;
use connectrpc::ConnectError;
#[test]
fn test_error_from_connect_error() {
use ErrorCode::*;
let test_cases = vec![
(
InvalidArgument,
ServiceError::InvalidArgument("test".into()),
),
(NotFound, ServiceError::NotFound("test".into())),
(AlreadyExists, ServiceError::AlreadyExists("test".into())),
(
PermissionDenied,
ServiceError::PermissionDenied("test".into()),
),
(
Unauthenticated,
ServiceError::Unauthenticated("test".into()),
),
(Internal, ServiceError::Internal("test".into())),
(Unavailable, ServiceError::Unavailable("test".into())),
(Unimplemented, ServiceError::Unimplemented("test".into())),
(
DeadlineExceeded,
ServiceError::DeadlineExceeded("test".into()),
),
];
for (code, expected_service_err) in test_cases {
let connect_err = ConnectError::new(code, "test");
let service_err: ServiceError = connect_err.into();
match (&service_err, &expected_service_err) {
(ServiceError::InvalidArgument(_), ServiceError::InvalidArgument(_)) => {}
(ServiceError::NotFound(_), ServiceError::NotFound(_)) => {}
(ServiceError::AlreadyExists(_), ServiceError::AlreadyExists(_)) => {}
(ServiceError::PermissionDenied(_), ServiceError::PermissionDenied(_)) => {}
(ServiceError::Unauthenticated(_), ServiceError::Unauthenticated(_)) => {}
(ServiceError::Internal(_), ServiceError::Internal(_)) => {}
(ServiceError::Unavailable(_), ServiceError::Unavailable(_)) => {}
(ServiceError::Unimplemented(_), ServiceError::Unimplemented(_)) => {}
(ServiceError::DeadlineExceeded(_), ServiceError::DeadlineExceeded(_)) => {}
_ => panic!(
"Mismatch for code {:?}: got {:?}, expected {:?}",
code, service_err, expected_service_err
),
}
}
}
#[test]
fn test_error_display() {
let test_cases = vec![
(
ServiceError::InvalidArgument("bad input".into()),
"Invalid argument: bad input",
),
(
ServiceError::NotFound("user 123".into()),
"Not found: user 123",
),
(
ServiceError::AlreadyExists("email@test.com".into()),
"Already exists: email@test.com",
),
(
ServiceError::PermissionDenied("read:admin".into()),
"Permission denied: read:admin",
),
(
ServiceError::Unauthenticated("token expired".into()),
"Unauthenticated: token expired",
),
(
ServiceError::Internal("server crash".into()),
"Internal error: server crash",
),
(
ServiceError::Unavailable("maintenance".into()),
"Service unavailable: maintenance",
),
(
ServiceError::Unimplemented("feature X".into()),
"Unimplemented: feature X",
),
(
ServiceError::DeadlineExceeded("30s timeout".into()),
"Deadline exceeded: 30s timeout",
),
(
ServiceError::Database("connection failed".into()),
"Database error: connection failed",
),
(
ServiceError::Configuration("missing env var".into()),
"Configuration error: missing env var",
),
(
ServiceError::Serialization("json parse failed".into()),
"Serialization error: json parse failed",
),
];
for (err, expected) in test_cases {
assert_eq!(err.to_string(), expected);
}
}
#[test]
fn test_error_code_mapping() {
use ErrorCode::*;
assert_eq!(
ServiceError::InvalidArgument("".into()).code(),
InvalidArgument
);
assert_eq!(ServiceError::NotFound("".into()).code(), NotFound);
assert_eq!(ServiceError::AlreadyExists("".into()).code(), AlreadyExists);
assert_eq!(
ServiceError::PermissionDenied("".into()).code(),
PermissionDenied
);
assert_eq!(
ServiceError::Unauthenticated("".into()).code(),
Unauthenticated
);
assert_eq!(ServiceError::Internal("".into()).code(), Internal);
assert_eq!(ServiceError::Unavailable("".into()).code(), Unavailable);
assert_eq!(ServiceError::Unimplemented("".into()).code(), Unimplemented);
assert_eq!(
ServiceError::DeadlineExceeded("".into()).code(),
DeadlineExceeded
);
assert_eq!(ServiceError::Database("".into()).code(), Internal);
assert_eq!(ServiceError::Configuration("".into()).code(), Internal);
assert_eq!(ServiceError::Serialization("".into()).code(), Internal);
}
#[test]
fn test_error_http_status() {
assert_eq!(
ServiceError::InvalidArgument("".into()).http_status(),
http::StatusCode::BAD_REQUEST
);
assert_eq!(
ServiceError::NotFound("".into()).http_status(),
http::StatusCode::NOT_FOUND
);
assert_eq!(
ServiceError::AlreadyExists("".into()).http_status(),
http::StatusCode::CONFLICT
);
assert_eq!(
ServiceError::PermissionDenied("".into()).http_status(),
http::StatusCode::FORBIDDEN
);
assert_eq!(
ServiceError::Unauthenticated("".into()).http_status(),
http::StatusCode::UNAUTHORIZED
);
assert_eq!(
ServiceError::Internal("".into()).http_status(),
http::StatusCode::INTERNAL_SERVER_ERROR
);
assert_eq!(
ServiceError::Unavailable("".into()).http_status(),
http::StatusCode::SERVICE_UNAVAILABLE
);
assert_eq!(
ServiceError::Unimplemented("".into()).http_status(),
http::StatusCode::NOT_IMPLEMENTED
);
assert_eq!(
ServiceError::DeadlineExceeded("".into()).http_status(),
http::StatusCode::GATEWAY_TIMEOUT
);
}
#[test]
fn test_error_message() {
let msg = "test message";
let err = ServiceError::Internal(msg.into());
assert_eq!(err.message(), msg);
}
#[test]
fn test_error_from_box_error() {
let err: Box<dyn std::error::Error + Send + Sync> =
Box::new(std::io::Error::other("IO error"));
let service_err: ServiceError = err.into();
match service_err {
ServiceError::Internal(msg) => assert!(msg.contains("IO error")),
_ => panic!("Expected Internal variant"),
}
}
#[test]
fn test_error_from_anyhow() {
let err = anyhow::anyhow!("Anyhow error");
let service_err: ServiceError = err.into();
match service_err {
ServiceError::Internal(msg) => assert!(msg.contains("Anyhow error")),
_ => panic!("Expected Internal variant"),
}
}
#[test]
fn test_error_from_serde_json() {
let err: serde_json::Error =
serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
let service_err: ServiceError = err.into();
match service_err {
ServiceError::Serialization(_) => {}
_ => panic!("Expected Serialization variant"),
}
}
#[test]
fn test_error_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<ServiceError>();
assert_sync::<ServiceError>();
}
#[test]
fn test_error_clone() {
let err = ServiceError::Internal("original".into());
let cloned = err.clone();
match (err, cloned) {
(ServiceError::Internal(msg1), ServiceError::Internal(msg2)) => {
assert_eq!(msg1, msg2);
}
_ => panic!("Clone produced different variant"),
}
}
}