Files
foxhunt/crates/data/src/error.rs
jgrusewski 61950e4c05 refactor: consolidate ErrorSeverity to common crate
Three duplicate ErrorSeverity enums (data/error.rs, data/validation.rs,
database/error.rs) with variants Low/Medium/High/Critical now use the
canonical definition in common::error::ErrorSeverity.

Extended common's ErrorSeverity with Low, Medium, High variants (alongside
existing Debug, Info, Warn, Error, Critical) and added PartialOrd/Ord derives
so both severity models coexist in a single type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:39:54 +01:00

647 lines
19 KiB
Rust

//! Error types for the data module
pub use common::error::ErrorSeverity;
use thiserror::Error;
/// Result type alias for data module operations
pub type Result<T> = std::result::Result<T, DataError>;
/// Data module error types
#[derive(Error, Debug)]
pub enum DataError {
/// Network connectivity errors
#[error("Network error: {message}")]
Network {
/// Error message
message: String,
},
/// `FIX` protocol errors
#[error("FIX protocol error: {message}")]
FixProtocol {
/// Error message
message: String,
},
/// Authentication errors
#[error("Authentication error: {message}")]
Authentication {
/// Error message
message: String,
},
/// Configuration errors
#[error("Configuration error in field '{field}': {message}")]
Configuration {
/// Field name with configuration error
field: String,
/// Error message
message: String,
},
/// Message parsing errors
#[error("Message parsing error: {message}")]
MessageParsing {
/// Error message
message: String,
},
/// Session management errors
#[error("Session error: {message}")]
Session {
/// Error message
message: String,
},
/// Order management errors
#[error("Order error: {message}")]
Order {
/// Error message
message: String,
},
/// Timeout errors
#[error("Operation timed out: {message}")]
Timeout {
/// Error message
message: String,
},
/// Parse errors
#[error("Parse error: {message}")]
Parse {
/// Error message
message: String,
},
/// Validation errors (consolidated)
#[error("Validation error in field '{field}': {message}")]
Validation {
/// Field name with validation error
field: String,
/// Error message
message: String,
},
/// Simple validation error
#[error("Validation error: {0}")]
ValidationSimple(String),
/// Serialization errors (consolidated)
#[error("Serialization error: {message}")]
Serialization {
/// Error message
message: String,
},
/// Compression errors
#[error("Compression error: {0}")]
Compression(String),
/// Storage errors
#[error("Storage error: {0}")]
Storage(String),
/// Dataset not found
#[error("Not found: {0}")]
NotFound(String),
/// Broker-specific errors
#[error("Broker error: {message}")]
Broker {
/// Error message
message: String,
},
/// Connection errors
#[error("Connection error: {0}")]
Connection(String),
/// Subscription errors
#[error("Subscription error: {message}")]
Subscription {
/// Error message
message: String,
},
/// API errors
#[error("API error: {message} (status: {status:?})")]
Api {
/// Error message
message: String,
/// HTTP status code if available
status: Option<String>,
},
/// Invalid parameter errors
#[error("Invalid parameter '{field}': {message}")]
InvalidParameter {
/// Parameter name
field: String,
/// Error message
message: String,
},
/// Unsupported operation errors
#[error("Unsupported operation: {0}")]
Unsupported(String),
/// Not implemented errors
#[error("Not implemented: {0}")]
NotImplemented(String),
/// Initialization errors
#[error("Initialization error: {0}")]
Initialization(String),
/// Invalid format errors
#[error("Invalid format: {0}")]
InvalidFormat(String),
/// Conversion errors
#[error("Conversion error: {0}")]
Conversion(String),
/// Rate limiting errors
#[error("Rate limit exceeded")]
RateLimit,
// External error types with automatic From trait generation
/// I/O errors
#[error(transparent)]
Io(#[from] std::io::Error),
/// JSON serialization/deserialization errors
#[error(transparent)]
Json(#[from] serde_json::Error),
/// HTTP client errors
#[error(transparent)]
Http(#[from] reqwest::Error),
/// WebSocket errors
#[error(transparent)]
WebSocket(#[from] tungstenite::Error),
/// Time parsing errors
#[error(transparent)]
Time(#[from] chrono::ParseError),
/// URL parsing errors
#[error(transparent)]
Url(#[from] url::ParseError),
/// Bincode serialization errors
#[error("Bincode error: {0}")]
Bincode(#[from] bincode::Error),
/// `Parquet` file errors
#[error("Parquet error: {0}")]
Parquet(#[from] parquet::errors::ParquetError),
/// `Arrow` data errors
#[error("Arrow error: {0}")]
Arrow(#[from] arrow::error::ArrowError),
/// `Redis` cache errors
#[cfg(feature = "redis-cache")]
#[error("Redis error: {0}")]
Redis(#[from] redis::RedisError),
/// Generic errors with transparent forwarding
#[error(transparent)]
Generic(#[from] anyhow::Error),
/// Trading engine errors
#[error("Trading engine error: {0}")]
TradingEngine(#[from] common::CommonTypeError),
/// Configuration module errors
#[error("Config error: {0}")]
ConfigError(#[from] config::error::ConfigError),
}
// Display implementation is now automatically generated by thiserror
// Error trait and From trait implementations are now automatically generated by thiserror
impl DataError {
/// Create a network error
pub fn network<S: Into<String>>(message: S) -> Self {
Self::Network {
message: message.into(),
}
}
/// Create a `FIX` protocol error
pub fn fix_protocol<S: Into<String>>(message: S) -> Self {
Self::FixProtocol {
message: message.into(),
}
}
/// Create an authentication error
pub fn authentication<S: Into<String>>(message: S) -> Self {
Self::Authentication {
message: message.into(),
}
}
/// Create a configuration error
pub fn configuration<F: Into<String>, M: Into<String>>(field: F, message: M) -> Self {
Self::Configuration {
field: field.into(),
message: message.into(),
}
}
/// Create a message parsing error
pub fn message_parsing<S: Into<String>>(message: S) -> Self {
Self::MessageParsing {
message: message.into(),
}
}
/// Create a session error
pub fn session<S: Into<String>>(message: S) -> Self {
Self::Session {
message: message.into(),
}
}
/// Create an order error
pub fn order<S: Into<String>>(message: S) -> Self {
Self::Order {
message: message.into(),
}
}
/// Create a timeout error
pub fn timeout<S: Into<String>>(message: S) -> Self {
Self::Timeout {
message: message.into(),
}
}
/// Create a broker error
pub fn broker<S: Into<String>>(message: S) -> Self {
Self::Broker {
message: message.into(),
}
}
/// Create a parse error
pub fn parse<S: Into<String>>(message: S) -> Self {
Self::Parse {
message: message.into(),
}
}
/// Create a validation error
pub fn validation<F: Into<String>, M: Into<String>>(field: F, message: M) -> Self {
Self::Validation {
field: field.into(),
message: message.into(),
}
}
/// Create a serialization error
pub fn serialization<S: Into<String>>(message: S) -> Self {
Self::Serialization {
message: message.into(),
}
}
/// Create an internal error
pub fn internal<S: Into<String>>(message: S) -> Self {
Self::Broker {
message: format!("Internal error: {}", message.into()),
}
}
/// Create a compression error
pub fn compression<S: Into<String>>(message: S) -> Self {
Self::Compression(message.into())
}
/// Create a storage error
pub fn storage<S: Into<String>>(message: S) -> Self {
Self::Storage(message.into())
}
/// Create an initialization error
pub fn initialization<S: Into<String>>(message: S) -> Self {
Self::Initialization(message.into())
}
/// Create a simple validation error
pub fn validation_simple<S: Into<String>>(message: S) -> Self {
Self::ValidationSimple(message.into())
}
/// Create a subscription error
pub fn subscription<S: Into<String>>(message: S) -> Self {
Self::Subscription {
message: message.into(),
}
}
/// Create an API error
pub fn api<S: Into<String>, T: Into<String>>(message: S, status: Option<T>) -> Self {
Self::Api {
message: message.into(),
status: status.map(|s| s.into()),
}
}
/// Check if error is retryable
pub fn is_retryable(&self) -> bool {
match self {
Self::Network { .. } => true,
Self::Timeout { .. } => true,
Self::Connection(_) => true,
Self::RateLimit => true,
Self::Io(_) => true,
Self::Http(_) => true,
Self::WebSocket(_) => true,
Self::Session { .. } => true,
Self::Storage(_) => true, // Storage errors may be transient
#[cfg(feature = "redis-cache")]
Self::Redis(_) => true,
_ => false,
}
}
/// Get error severity level
pub fn severity(&self) -> ErrorSeverity {
match self {
Self::Authentication { .. } => ErrorSeverity::Critical,
Self::Configuration { .. } => ErrorSeverity::Critical,
Self::FixProtocol { .. } => ErrorSeverity::High,
Self::Order { .. } => ErrorSeverity::High,
Self::Network { .. } => ErrorSeverity::Medium,
Self::Session { .. } => ErrorSeverity::Medium,
Self::Timeout { .. } => ErrorSeverity::Medium,
Self::NotFound(_) => ErrorSeverity::Medium,
Self::MessageParsing { .. } => ErrorSeverity::Low,
Self::Broker { .. } => ErrorSeverity::Medium,
_ => ErrorSeverity::Low,
}
}
/// Get error category for monitoring
pub fn category(&self) -> &'static str {
match self {
Self::Network { .. } => "NETWORK",
Self::FixProtocol { .. } => "FIX_PROTOCOL",
Self::Authentication { .. } => "AUTHENTICATION",
Self::Configuration { .. } => "CONFIGURATION",
Self::MessageParsing { .. } => "MESSAGE_PARSING",
Self::Session { .. } => "SESSION",
Self::Order { .. } => "ORDER",
Self::Timeout { .. } => "TIMEOUT",
Self::Parse { .. } => "PARSING",
Self::Validation { .. } => "VALIDATION",
Self::ValidationSimple(_) => "VALIDATION",
Self::Serialization { .. } => "SERIALIZATION",
Self::Compression(_) => "COMPRESSION",
Self::Storage(_) => "STORAGE",
Self::NotFound(_) => "NOT_FOUND",
Self::Broker { .. } => "BROKER",
Self::Connection(_) => "CONNECTION",
Self::Subscription { .. } => "SUBSCRIPTION",
Self::Api { .. } => "API",
Self::InvalidParameter { .. } => "INVALID_PARAMETER",
Self::Unsupported(_) => "UNSUPPORTED",
Self::NotImplemented(_) => "NOT_IMPLEMENTED",
Self::Initialization(_) => "INITIALIZATION",
Self::InvalidFormat(_) => "INVALID_FORMAT",
Self::Conversion(_) => "CONVERSION",
Self::RateLimit => "RATE_LIMIT",
Self::Io(_) => "IO",
Self::Json(_) => "JSON",
Self::Http(_) => "HTTP",
Self::WebSocket(_) => "WEBSOCKET",
Self::Time(_) => "TIME",
Self::Url(_) => "URL",
Self::Bincode(_) => "BINCODE",
Self::Parquet(_) => "PARQUET",
Self::Arrow(_) => "ARROW",
#[cfg(feature = "redis-cache")]
Self::Redis(_) => "REDIS",
Self::Generic(_) => "GENERIC",
Self::TradingEngine(_) => "TRADING_ENGINE",
Self::ConfigError(_) => "CONFIG",
}
}
}
// ErrorSeverity is imported from common::error
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let error = DataError::network("Connection failed");
assert!(matches!(error, DataError::Network { .. }));
assert!(error.is_retryable());
assert_eq!(error.severity(), ErrorSeverity::Medium);
assert_eq!(error.category(), "NETWORK");
}
#[test]
fn test_error_severity() {
assert_eq!(
DataError::authentication("Invalid credentials").severity(),
ErrorSeverity::Critical
);
assert_eq!(
DataError::timeout("Request timeout").severity(),
ErrorSeverity::Medium
);
}
#[test]
fn test_retryable_errors() {
assert!(DataError::network("test").is_retryable());
assert!(DataError::timeout("test").is_retryable());
assert!(!DataError::authentication("test").is_retryable());
assert!(!DataError::configuration("field", "test").is_retryable());
}
#[test]
fn test_automatic_from_conversions() {
// Test that thiserror automatically converts external errors
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
let data_error: DataError = io_error.into();
assert!(matches!(data_error, DataError::Io(_)));
assert!(data_error.is_retryable());
assert_eq!(data_error.category(), "IO");
let json_error = serde_json::from_str::<i32>("invalid").unwrap_err();
let data_error: DataError = json_error.into();
assert!(matches!(data_error, DataError::Json(_)));
assert_eq!(data_error.category(), "JSON");
}
#[test]
fn test_new_error_variants() {
let compression_error = DataError::compression("Failed to compress");
assert!(matches!(compression_error, DataError::Compression(_)));
assert_eq!(compression_error.category(), "COMPRESSION");
let storage_error = DataError::storage("Storage failed");
assert!(matches!(storage_error, DataError::Storage(_)));
assert_eq!(storage_error.category(), "STORAGE");
let rate_limit_error = DataError::RateLimit;
assert!(matches!(rate_limit_error, DataError::RateLimit));
assert!(rate_limit_error.is_retryable());
assert_eq!(rate_limit_error.category(), "RATE_LIMIT");
}
#[test]
fn test_consolidated_variants() {
// Test that consolidated variants work properly
let validation_error = DataError::validation("field", "invalid");
assert!(matches!(validation_error, DataError::Validation { .. }));
assert_eq!(validation_error.category(), "VALIDATION");
let simple_validation_error = DataError::validation_simple("invalid");
assert!(matches!(
simple_validation_error,
DataError::ValidationSimple(_)
));
assert_eq!(simple_validation_error.category(), "VALIDATION");
let api_error = DataError::api("API failed", Some("404"));
assert!(matches!(api_error, DataError::Api { .. }));
assert_eq!(api_error.category(), "API");
}
#[test]
fn test_error_categories() {
assert_eq!(DataError::network("test").category(), "NETWORK");
assert_eq!(DataError::timeout("test").category(), "TIMEOUT");
assert_eq!(DataError::parse("test").category(), "PARSING");
assert_eq!(DataError::RateLimit.category(), "RATE_LIMIT");
assert_eq!(
DataError::authentication("test").category(),
"AUTHENTICATION"
);
assert_eq!(
DataError::NotFound("test".to_string()).category(),
"NOT_FOUND"
);
}
#[test]
fn test_error_display() {
let error = DataError::network("Connection refused");
let display = format!("{}", error);
assert!(display.contains("Network"));
let error = DataError::validation("price", "must be positive");
let display = format!("{}", error);
assert!(display.contains("price"));
assert!(display.contains("must be positive"));
}
#[test]
fn test_non_retryable_errors() {
assert!(!DataError::parse("test").is_retryable());
assert!(!DataError::validation("field", "test").is_retryable());
assert!(!DataError::ValidationSimple("test".to_string()).is_retryable());
assert!(!DataError::NotFound("test".to_string()).is_retryable());
}
#[test]
fn test_severity_levels() {
assert_eq!(DataError::network("test").severity(), ErrorSeverity::Medium);
assert_eq!(DataError::timeout("test").severity(), ErrorSeverity::Medium);
assert_eq!(
DataError::authentication("test").severity(),
ErrorSeverity::Critical
);
assert_eq!(DataError::RateLimit.severity(), ErrorSeverity::Low);
assert_eq!(DataError::parse("test").severity(), ErrorSeverity::Low);
}
#[test]
fn test_severity_display() {
assert_eq!(format!("{}", ErrorSeverity::Low), "LOW");
assert_eq!(format!("{}", ErrorSeverity::Medium), "MEDIUM");
assert_eq!(format!("{}", ErrorSeverity::High), "HIGH");
assert_eq!(format!("{}", ErrorSeverity::Critical), "CRITICAL");
}
#[test]
fn test_error_with_context() {
let error = DataError::network("Connection failed");
let error_string = format!("{}", error);
assert!(error_string.contains("Network"));
}
#[test]
fn test_not_found_error() {
let error = DataError::NotFound("dataset123".to_string());
assert!(!error.is_retryable());
assert_eq!(error.severity(), ErrorSeverity::Medium);
assert_eq!(error.category(), "NOT_FOUND");
}
#[test]
fn test_compression_error() {
let error = DataError::compression("ZSTD compression failed");
assert!(matches!(error, DataError::Compression(_)));
assert!(!error.is_retryable());
assert_eq!(error.category(), "COMPRESSION");
}
#[test]
fn test_storage_error() {
let error = DataError::storage("Disk full");
assert!(matches!(error, DataError::Storage(_)));
assert!(error.is_retryable());
assert_eq!(error.category(), "STORAGE");
}
#[test]
fn test_timeout_error() {
let error = DataError::timeout("Request timeout after 30s");
assert!(error.is_retryable());
assert_eq!(error.severity(), ErrorSeverity::Medium);
}
#[test]
fn test_configuration_error() {
let error = DataError::configuration("api_key", "Missing required field");
assert!(!error.is_retryable());
assert_eq!(error.severity(), ErrorSeverity::Critical);
}
#[test]
fn test_api_error_with_code() {
let error = DataError::api("Unauthorized", Some("401"));
assert!(matches!(error, DataError::Api { .. }));
assert_eq!(error.category(), "API");
}
#[test]
fn test_api_error_without_code() {
let error = DataError::api("Server error", None::<String>);
assert!(matches!(error, DataError::Api { .. }));
assert_eq!(error.category(), "API");
}
#[test]
fn test_websocket_error() {
let ws_error = tungstenite::Error::ConnectionClosed;
let error = DataError::WebSocket(ws_error);
assert!(error.is_retryable());
assert_eq!(error.category(), "WEBSOCKET");
}
}