use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ScopeError {
#[error("Configuration error: {0}")]
Config(#[from] ConfigError),
#[error("Chain client error: {0}")]
Chain(String),
#[error("Venue error ({venue}): {message}")]
Venue {
venue: String,
message: String,
},
#[error("API request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("Invalid address format: {0}")]
InvalidAddress(String),
#[error("Invalid transaction hash: {0}")]
InvalidHash(String),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Export failed: {0}")]
Export(String),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Network error: {0}")]
Network(String),
#[error("API error: {0}")]
Api(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("{0}")]
InvalidInput(String),
#[error("Compliance error: {0}")]
Compliance(String),
#[error("Discovery error: {0}")]
Discovery(String),
#[error("Contract error: {0}")]
Contract(String),
#[error("{0}")]
Other(String),
}
impl ScopeError {
pub fn io_context(e: std::io::Error, context: impl std::fmt::Display) -> Self {
let kind = e.kind();
let wrapped = IoContext {
context: context.to_string(),
source: e,
};
Self::IoError(std::io::Error::new(kind, wrapped))
}
}
#[derive(Debug)]
struct IoContext {
context: String,
source: std::io::Error,
}
impl std::fmt::Display for IoContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.context, self.source)
}
}
impl std::error::Error for IoContext {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Configuration file not found: {path}")]
NotFound {
path: PathBuf,
},
#[error("Failed to read configuration file '{path}': {source}")]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to parse configuration: {source}")]
Parse {
#[source]
source: serde_yaml::Error,
},
#[error("Invalid configuration: {message}")]
Validation {
message: String,
},
}
pub type Result<T> = std::result::Result<T, ScopeError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bca_error_display_invalid_address() {
let err = ScopeError::InvalidAddress("not-an-address".into());
assert_eq!(err.to_string(), "Invalid address format: not-an-address");
}
#[test]
fn test_bca_error_display_invalid_hash() {
let err = ScopeError::InvalidHash("bad-hash".into());
assert_eq!(err.to_string(), "Invalid transaction hash: bad-hash");
}
#[test]
fn test_venue_error_display_names_the_venue() {
let err = ScopeError::Venue {
venue: "bybit".into(),
message: "API error: HTTP 400".into(),
};
assert_eq!(err.to_string(), "Venue error (bybit): API error: HTTP 400");
}
#[test]
fn test_bca_error_display_chain() {
let err = ScopeError::Chain("connection refused".into());
assert_eq!(err.to_string(), "Chain client error: connection refused");
}
#[test]
fn test_bca_error_display_export() {
let err = ScopeError::Export("failed to write CSV".into());
assert_eq!(err.to_string(), "Export failed: failed to write CSV");
}
#[test]
fn test_config_error_not_found() {
let err = ConfigError::NotFound {
path: PathBuf::from("/missing/config.yaml"),
};
assert_eq!(
err.to_string(),
"Configuration file not found: /missing/config.yaml"
);
}
#[test]
fn test_config_error_validation() {
let err = ConfigError::Validation {
message: "ethereum_rpc must be a valid URL".into(),
};
assert_eq!(
err.to_string(),
"Invalid configuration: ethereum_rpc must be a valid URL"
);
}
#[test]
fn test_bca_error_from_config_error() {
let config_err = ConfigError::NotFound {
path: PathBuf::from("/test"),
};
let bca_err: ScopeError = config_err.into();
assert!(matches!(bca_err, ScopeError::Config(_)));
}
#[test]
fn test_bca_error_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let bca_err: ScopeError = io_err.into();
assert!(matches!(bca_err, ScopeError::IoError(_)));
}
#[test]
fn test_io_error_with_context_keeps_its_kind_and_the_old_text() {
let denied = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
let err = ScopeError::io_context(denied, "Failed to write token aliases");
let source = std::error::Error::source(&err).and_then(std::error::Error::source);
assert_eq!(
source.map(|s| s.to_string()).as_deref(),
Some("denied"),
"the source stays"
);
assert_eq!(
err.to_string(),
"I/O error: Failed to write token aliases: denied"
);
assert!(
matches!(&err, ScopeError::IoError(e) if e.kind() == std::io::ErrorKind::PermissionDenied)
);
}
#[test]
fn test_save_report_failure_names_the_path_and_keeps_the_kind() {
let dir = std::env::temp_dir().join("scope-no-such-dir-for-report-test");
let err = crate::display::report::save_report("x", dir.join("r.md")).unwrap_err();
assert!(
err.to_string()
.starts_with("I/O error: Failed to write report to ")
);
assert!(matches!(&err, ScopeError::IoError(e) if e.kind() == std::io::ErrorKind::NotFound));
}
#[test]
fn test_result_type_alias_ok() {
fn returns_ok() -> Result<i32> {
Ok(42)
}
assert_eq!(returns_ok().unwrap(), 42);
}
#[test]
fn test_result_type_alias_err() {
fn returns_err() -> Result<i32> {
Err(ScopeError::Chain("test error".into()))
}
assert!(returns_err().is_err());
}
#[test]
fn test_error_debug_impl() {
let err = ScopeError::InvalidAddress("0x123".into());
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("InvalidAddress"));
assert!(debug_str.contains("0x123"));
}
#[test]
fn test_config_error_source_chain() {
use std::error::Error;
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
let config_err = ConfigError::Read {
path: PathBuf::from("/etc/scope/config.yaml"),
source: io_err,
};
assert!(config_err.source().is_some());
}
}