use thiserror::Error;
#[derive(Error, Debug)]
pub enum FinanceError {
#[error("Authentication failed: {context}")]
AuthenticationFailed {
context: String,
},
#[error("Symbol not found: {}", symbol.as_deref().unwrap_or("unknown"))]
SymbolNotFound {
symbol: Option<String>,
context: String,
},
#[error("Rate limited (retry after {retry_after:?}s)")]
RateLimited {
retry_after: Option<u64>,
},
#[error("HTTP request failed: {0}")]
HttpError(#[from] reqwest::Error),
#[error("JSON parse error: {0}")]
JsonParseError(#[from] serde_json::Error),
#[error("Response structure error in '{field}': {context}")]
ResponseStructureError {
field: String,
context: String,
},
#[error("Invalid parameter '{param}': {reason}")]
InvalidParameter {
param: String,
reason: String,
},
#[error("Request timeout after {timeout_ms}ms")]
Timeout {
timeout_ms: u64,
},
#[error("Server error {status}: {context}")]
ServerError {
status: u16,
context: String,
},
#[error("Unexpected response: {0}")]
UnexpectedResponse(String),
#[error("Internal error: {0}")]
InternalError(String),
#[error("API error: {0}")]
ApiError(String),
#[error("Runtime error: {0}")]
RuntimeError(#[from] std::io::Error),
#[cfg(feature = "indicators")]
#[error("Indicator calculation error: {0}")]
IndicatorError(#[from] crate::indicators::IndicatorError),
#[error("External API error from '{api}': HTTP {status}")]
ExternalApiError {
api: String,
status: u16,
},
#[error("Macro data error from '{provider}': {context}")]
MacroDataError {
provider: String,
context: String,
},
#[error("Feed parse error for '{url}': {context}")]
FeedParseError {
url: String,
context: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
Auth,
RateLimit,
Timeout,
Server,
NotFound,
Validation,
Parsing,
Other,
}
pub type Error = FinanceError;
pub type Result<T> = std::result::Result<T, FinanceError>;
impl FinanceError {
pub fn is_retriable(&self) -> bool {
matches!(
self,
FinanceError::Timeout { .. }
| FinanceError::RateLimited { .. }
| FinanceError::HttpError(_)
| FinanceError::AuthenticationFailed { .. }
| FinanceError::ServerError { .. }
)
}
pub fn is_auth_error(&self) -> bool {
matches!(self, FinanceError::AuthenticationFailed { .. })
}
pub fn is_not_found(&self) -> bool {
matches!(self, FinanceError::SymbolNotFound { .. })
}
pub fn retry_after_secs(&self) -> Option<u64> {
match self {
Self::RateLimited { retry_after } => *retry_after,
Self::Timeout { .. } => Some(2),
Self::ServerError { status, .. } if *status >= 500 => Some(5),
Self::AuthenticationFailed { .. } => Some(1),
_ => None,
}
}
pub fn category(&self) -> ErrorCategory {
match self {
Self::AuthenticationFailed { .. } => ErrorCategory::Auth,
Self::RateLimited { .. } => ErrorCategory::RateLimit,
Self::Timeout { .. } => ErrorCategory::Timeout,
Self::ServerError { .. } => ErrorCategory::Server,
Self::SymbolNotFound { .. } => ErrorCategory::NotFound,
Self::InvalidParameter { .. } => ErrorCategory::Validation,
Self::JsonParseError(_) | Self::ResponseStructureError { .. } => ErrorCategory::Parsing,
_ => ErrorCategory::Other,
}
}
pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
if let Self::SymbolNotFound {
symbol: ref mut s, ..
} = self
{
*s = Some(symbol.into());
}
self
}
pub fn with_context(mut self, context: impl Into<String>) -> Self {
match self {
Self::AuthenticationFailed {
context: ref mut c, ..
} => {
*c = context.into();
}
Self::SymbolNotFound {
context: ref mut c, ..
} => {
*c = context.into();
}
Self::ResponseStructureError {
context: ref mut c, ..
} => {
*c = context.into();
}
Self::ServerError {
context: ref mut c, ..
} => {
*c = context.into();
}
_ => {}
}
self
}
}
impl FinanceError {
#[deprecated(since = "2.0.0", note = "Use ResponseStructureError instead")]
pub fn parse_error(msg: impl Into<String>) -> Self {
let msg = msg.into();
Self::ResponseStructureError {
field: "unknown".to_string(),
context: msg,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_is_retriable() {
assert!(FinanceError::Timeout { timeout_ms: 5000 }.is_retriable());
assert!(FinanceError::RateLimited { retry_after: None }.is_retriable());
assert!(
FinanceError::AuthenticationFailed {
context: "test".to_string()
}
.is_retriable()
);
assert!(
FinanceError::ServerError {
status: 500,
context: "test".to_string()
}
.is_retriable()
);
assert!(
!FinanceError::SymbolNotFound {
symbol: Some("AAPL".to_string()),
context: "test".to_string()
}
.is_retriable()
);
assert!(
!FinanceError::InvalidParameter {
param: "test".to_string(),
reason: "invalid".to_string()
}
.is_retriable()
);
}
#[test]
fn test_error_is_auth_error() {
assert!(
FinanceError::AuthenticationFailed {
context: "test".to_string()
}
.is_auth_error()
);
assert!(!FinanceError::Timeout { timeout_ms: 5000 }.is_auth_error());
}
#[test]
fn test_error_is_not_found() {
assert!(
FinanceError::SymbolNotFound {
symbol: Some("AAPL".to_string()),
context: "test".to_string()
}
.is_not_found()
);
assert!(!FinanceError::Timeout { timeout_ms: 5000 }.is_not_found());
}
#[test]
fn test_retry_after_secs() {
assert_eq!(
FinanceError::RateLimited {
retry_after: Some(10)
}
.retry_after_secs(),
Some(10)
);
assert_eq!(
FinanceError::Timeout { timeout_ms: 5000 }.retry_after_secs(),
Some(2)
);
assert_eq!(
FinanceError::ServerError {
status: 503,
context: "test".to_string()
}
.retry_after_secs(),
Some(5)
);
assert_eq!(
FinanceError::SymbolNotFound {
symbol: None,
context: "test".to_string()
}
.retry_after_secs(),
None
);
}
#[test]
fn test_error_category() {
assert_eq!(
FinanceError::AuthenticationFailed {
context: "test".to_string()
}
.category(),
ErrorCategory::Auth
);
assert_eq!(
FinanceError::RateLimited { retry_after: None }.category(),
ErrorCategory::RateLimit
);
assert_eq!(
FinanceError::Timeout { timeout_ms: 5000 }.category(),
ErrorCategory::Timeout
);
assert_eq!(
FinanceError::SymbolNotFound {
symbol: None,
context: "test".to_string()
}
.category(),
ErrorCategory::NotFound
);
}
#[test]
fn test_with_symbol() {
let error = FinanceError::SymbolNotFound {
symbol: None,
context: "test".to_string(),
}
.with_symbol("AAPL");
if let FinanceError::SymbolNotFound { symbol, .. } = error {
assert_eq!(symbol, Some("AAPL".to_string()));
} else {
panic!("Expected SymbolNotFound");
}
}
#[test]
fn test_with_context() {
let error = FinanceError::AuthenticationFailed {
context: "old".to_string(),
}
.with_context("new context");
if let FinanceError::AuthenticationFailed { context } = error {
assert_eq!(context, "new context");
} else {
panic!("Expected AuthenticationFailed");
}
}
}