use thiserror::Error;
use crate::core::ServiceError;
use crate::error::ApiError;
use crate::error::ErrorCategory;
#[derive(Debug, Error)]
pub enum SdForgeError {
#[error(transparent)]
Api(#[from] ApiError),
#[cfg(feature = "security")]
#[error(transparent)]
Auth(#[from] crate::security::AuthError),
#[cfg(feature = "security")]
#[error(transparent)]
Jwt(#[from] crate::security::JwtError),
#[cfg(feature = "security")]
#[error(transparent)]
AuthConfig(#[from] crate::security::AuthConfigError),
#[cfg(feature = "http")]
#[error(transparent)]
Config(#[from] crate::config::ConfigError),
#[error("Internal error: {0}")]
Internal(String),
}
impl SdForgeError {
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal(message.into())
}
pub fn category(&self) -> ErrorCategory {
match self {
SdForgeError::Api(err) => err.category(),
#[cfg(feature = "security")]
SdForgeError::Auth(_) | SdForgeError::Jwt(_) => ErrorCategory::AuthError,
#[cfg(feature = "security")]
SdForgeError::AuthConfig(_) => ErrorCategory::AuthError,
#[cfg(feature = "http")]
SdForgeError::Config(_) => ErrorCategory::ClientError,
SdForgeError::Internal(_) => ErrorCategory::ServerError,
}
}
pub fn sanitized_message(&self) -> String {
match self {
SdForgeError::Api(err) => err.sanitized_message(),
SdForgeError::Internal(msg) => msg.clone(),
#[cfg(any(feature = "http", feature = "security"))]
other => other.to_string(),
}
}
pub fn to_service_error(&self) -> ServiceError {
match self {
SdForgeError::Api(err) => err.to_service_error(),
#[cfg(feature = "security")]
SdForgeError::Auth(e) => ServiceError::with_details(
"AUTH_ERROR",
e.to_string(),
serde_json::json!({ "type": "auth" }),
401,
),
#[cfg(feature = "security")]
SdForgeError::Jwt(e) => ServiceError::with_details(
"JWT_ERROR",
e.to_string(),
serde_json::json!({ "type": "jwt" }),
401,
),
#[cfg(feature = "security")]
SdForgeError::AuthConfig(e) => ServiceError::with_details(
"AUTH_CONFIG_ERROR",
e.to_string(),
serde_json::json!({ "type": "auth_config" }),
500,
),
#[cfg(feature = "http")]
SdForgeError::Config(e) => ServiceError::with_details(
"CONFIG_ERROR",
e.to_string(),
serde_json::json!({ "type": "config" }),
400,
),
SdForgeError::Internal(msg) => ServiceError::with_details(
"INTERNAL_ERROR",
msg.clone(),
serde_json::json!({ "type": "internal" }),
500,
),
}
}
}
pub type SdForgeResult<T> = std::result::Result<T, SdForgeError>;
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ApiError;
use crate::error::ErrorCategory;
#[test]
fn test_internal_constructor_with_str() {
let err = SdForgeError::internal("something went wrong");
match err {
SdForgeError::Internal(msg) => {
assert_eq!(msg, "something went wrong");
}
_ => panic!("Expected Internal variant"),
}
}
#[test]
fn test_internal_constructor_with_string() {
let msg = String::from("dynamic error");
let err = SdForgeError::internal(msg);
match err {
SdForgeError::Internal(m) => assert_eq!(m, "dynamic error"),
_ => panic!("Expected Internal variant"),
}
}
#[test]
fn test_category_api_not_found() {
let err = SdForgeError::from(ApiError::not_found("User", None));
assert_eq!(err.category(), ErrorCategory::ClientError);
}
#[test]
fn test_category_api_auth() {
let err = SdForgeError::from(ApiError::authentication_failed("bad token"));
assert_eq!(err.category(), ErrorCategory::AuthError);
}
#[test]
fn test_category_api_rate_limit() {
let err = SdForgeError::from(ApiError::rate_limit_exceeded(100, 60));
assert_eq!(err.category(), ErrorCategory::RateLimitError);
}
#[test]
fn test_category_api_internal() {
let err = SdForgeError::from(ApiError::internal_error("boom", "err-1"));
assert_eq!(err.category(), ErrorCategory::ServerError);
}
#[test]
fn test_category_api_validation() {
let err = SdForgeError::from(ApiError::validation("email", "invalid"));
assert_eq!(err.category(), ErrorCategory::ValidationError);
}
#[test]
fn test_category_internal() {
let err = SdForgeError::internal("internal error");
assert_eq!(err.category(), ErrorCategory::ServerError);
}
#[cfg(feature = "http")]
#[test]
fn test_category_config() {
let config_err = crate::config::ConfigError::FileNotFound {
path: "/missing".to_string(),
};
let err = SdForgeError::from(config_err);
assert_eq!(err.category(), ErrorCategory::ClientError);
}
#[test]
fn test_sanitized_message_api_not_found() {
let err = SdForgeError::from(ApiError::not_found("User", None));
let msg = err.sanitized_message();
assert!(msg.contains("User"));
}
#[test]
fn test_sanitized_message_api_internal_sanitized() {
let err = SdForgeError::from(ApiError::internal_error("secret details", "err-1"));
let msg = err.sanitized_message();
assert!(!msg.contains("secret details"));
assert!(msg.contains("internal error"));
}
#[test]
fn test_sanitized_message_internal() {
let err = SdForgeError::internal("my internal message");
let msg = err.sanitized_message();
assert_eq!(msg, "my internal message");
}
#[cfg(feature = "http")]
#[test]
fn test_sanitized_message_config() {
let config_err = crate::config::ConfigError::FileNotFound {
path: "/missing".to_string(),
};
let err = SdForgeError::from(config_err);
let msg = err.sanitized_message();
assert!(msg.contains("File not found"));
}
#[test]
fn test_to_service_error_api_not_found() {
let err = SdForgeError::from(ApiError::not_found("User", Some("42".to_string())));
let service_err = err.to_service_error();
assert_eq!(service_err.code(), "NOT_FOUND");
assert_eq!(service_err.http_status(), 404);
}
#[test]
fn test_to_service_error_api_validation() {
let err = SdForgeError::from(ApiError::validation("email", "invalid format"));
let service_err = err.to_service_error();
assert_eq!(service_err.code(), "VALIDATION_ERROR");
assert_eq!(service_err.http_status(), 422);
}
#[test]
fn test_to_service_error_internal() {
let err = SdForgeError::internal("custom internal message");
let service_err = err.to_service_error();
assert_eq!(service_err.code(), "INTERNAL_ERROR");
assert_eq!(service_err.http_status(), 500);
assert_eq!(service_err.message(), "custom internal message");
}
#[cfg(feature = "http")]
#[test]
fn test_to_service_error_config() {
let config_err = crate::config::ConfigError::FileNotFound {
path: "/missing".to_string(),
};
let err = SdForgeError::from(config_err);
let service_err = err.to_service_error();
assert_eq!(service_err.code(), "CONFIG_ERROR");
assert_eq!(service_err.http_status(), 400);
}
#[test]
fn test_from_api_error() {
let api_err = ApiError::not_found("Resource", None);
let sdforge_err: SdForgeError = api_err.into();
match sdforge_err {
SdForgeError::Api(ApiError::NotFound { resource, .. }) => {
assert_eq!(resource, "Resource");
}
_ => panic!("Expected Api variant"),
}
}
#[cfg(feature = "http")]
#[test]
fn test_from_config_error() {
let config_err = crate::config::ConfigError::FileNotFound {
path: "/test".to_string(),
};
let sdforge_err: SdForgeError = config_err.into();
match sdforge_err {
SdForgeError::Config(_) => {}
_ => panic!("Expected Config variant"),
}
}
#[test]
fn test_debug_format_internal() {
let err = SdForgeError::internal("debug message");
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("Internal"));
assert!(debug_str.contains("debug message"));
}
#[test]
fn test_display_internal() {
let err = SdForgeError::internal("display message");
let s = err.to_string();
assert_eq!(s, "Internal error: display message");
}
#[test]
fn test_display_api() {
let err = SdForgeError::from(ApiError::not_found("User", None));
let s = err.to_string();
assert!(s.contains("Resource not found"));
assert!(s.contains("User"));
}
#[cfg(feature = "http")]
#[test]
fn test_display_config() {
let config_err = crate::config::ConfigError::FileNotFound {
path: "/missing".to_string(),
};
let err = SdForgeError::from(config_err);
let s = err.to_string();
assert!(s.contains("File not found"));
assert!(s.contains("/missing"));
}
#[test]
fn test_send_sync_bounds() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<SdForgeError>();
}
#[test]
fn test_error_chain_preserves_status_and_code() {
let api_err = ApiError::access_denied("read", Some("user-1".to_string()));
let sdforge_err: SdForgeError = api_err.into();
let service_err = sdforge_err.to_service_error();
assert_eq!(service_err.code(), "ACCESS_DENIED");
assert_eq!(service_err.http_status(), 403);
}
#[cfg(feature = "security")]
#[test]
fn test_to_service_error_auth() {
use crate::security::AuthError;
let auth_err = AuthError::InvalidToken;
let sdforge_err: SdForgeError = auth_err.into();
let service_err = sdforge_err.to_service_error();
assert_eq!(service_err.code(), "AUTH_ERROR");
assert_eq!(service_err.http_status(), 401);
}
#[cfg(feature = "security")]
#[test]
fn test_to_service_error_jwt() {
use crate::security::JwtError;
let jwt_err = JwtError::Expired;
let sdforge_err: SdForgeError = jwt_err.into();
let service_err = sdforge_err.to_service_error();
assert_eq!(service_err.code(), "JWT_ERROR");
assert_eq!(service_err.http_status(), 401);
}
#[cfg(feature = "security")]
#[test]
fn test_to_service_error_auth_config() {
use crate::security::AuthConfigError;
let config_err = AuthConfigError::SecretTooShort { length: 10 };
let sdforge_err: SdForgeError = config_err.into();
let service_err = sdforge_err.to_service_error();
assert_eq!(service_err.code(), "AUTH_CONFIG_ERROR");
assert_eq!(service_err.http_status(), 500);
}
#[cfg(feature = "security")]
#[test]
fn test_category_auth() {
use crate::security::AuthError;
let err: SdForgeError = AuthError::MissingAuth.into();
assert_eq!(err.category(), ErrorCategory::AuthError);
}
#[cfg(feature = "security")]
#[test]
fn test_category_jwt() {
use crate::security::JwtError;
let err: SdForgeError = JwtError::InvalidSignature.into();
assert_eq!(err.category(), ErrorCategory::AuthError);
}
#[cfg(feature = "security")]
#[test]
fn test_category_auth_config() {
use crate::security::AuthConfigError;
let err: SdForgeError = AuthConfigError::InvalidSecret("weak".to_string()).into();
assert_eq!(err.category(), ErrorCategory::AuthError);
}
#[cfg(feature = "security")]
#[test]
fn test_sanitized_message_auth() {
use crate::security::AuthError;
let err: SdForgeError = AuthError::InvalidToken.into();
let msg = err.sanitized_message();
assert!(msg.contains("Invalid or expired token"));
}
#[cfg(feature = "security")]
#[test]
fn test_display_auth() {
use crate::security::AuthError;
let err: SdForgeError = AuthError::MissingAuth.into();
let s = err.to_string();
assert!(s.contains("Missing or invalid authorization header"));
}
#[cfg(feature = "security")]
#[test]
fn test_from_auth_error() {
use crate::security::AuthError;
let auth_err = AuthError::InvalidToken;
let sdforge_err: SdForgeError = auth_err.into();
match sdforge_err {
SdForgeError::Auth(_) => {}
other => panic!("Expected Auth variant, got: {:?}", other),
}
}
#[cfg(feature = "security")]
#[test]
fn test_from_jwt_error() {
use crate::security::JwtError;
let jwt_err = JwtError::Expired;
let sdforge_err: SdForgeError = jwt_err.into();
match sdforge_err {
SdForgeError::Jwt(_) => {}
other => panic!("Expected Jwt variant, got: {:?}", other),
}
}
#[cfg(feature = "security")]
#[test]
fn test_from_auth_config_error() {
use crate::security::AuthConfigError;
let config_err = AuthConfigError::SecretTooShort { length: 5 };
let sdforge_err: SdForgeError = config_err.into();
match sdforge_err {
SdForgeError::AuthConfig(_) => {}
other => panic!("Expected AuthConfig variant, got: {:?}", other),
}
}
}