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>
647 lines
19 KiB
Rust
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");
|
|
}
|
|
}
|