Skip to main content

scope/
error.rs

1//! # Error Handling Module
2//!
3//! This module defines the error types used throughout the Scope application.
4//! It provides a unified error type [`ScopeError`] that wraps all possible
5//! error conditions, along with a convenient [`Result`] type alias.
6//!
7//! ## Error Hierarchy
8//!
9//! ```text
10//! ScopeError
11//! ├── Config     - Configuration loading/parsing errors
12//! ├── Chain      - Blockchain client errors
13//! ├── Request    - HTTP/API request failures
14//! ├── InvalidAddress - Malformed blockchain address
15//! ├── InvalidHash    - Malformed transaction hash
16//! ├── Io         - File system operations
17//! └── Export     - Data export failures
18//! ```
19//!
20//! ## Usage
21//!
22//! ```rust
23//! use scope::{ScopeError, Result};
24//!
25//! fn validate_address(addr: &str) -> Result<()> {
26//!     if !addr.starts_with("0x") || addr.len() != 42 {
27//!         return Err(ScopeError::InvalidAddress(addr.to_string()));
28//!     }
29//!     Ok(())
30//! }
31//! ```
32
33use std::path::PathBuf;
34use thiserror::Error;
35
36/// The primary error type for the Scope application.
37///
38/// This enum encompasses all error conditions that can occur during
39/// blockchain analysis operations. Each variant provides context-specific
40/// information to aid in debugging and user-friendly error messages.
41#[derive(Debug, Error)]
42pub enum ScopeError {
43    /// Configuration file could not be loaded or parsed.
44    ///
45    /// This includes missing files, invalid YAML syntax, or schema violations.
46    #[error("Configuration error: {0}")]
47    Config(#[from] ConfigError),
48
49    /// Blockchain client encountered an error.
50    ///
51    /// This covers RPC failures, unsupported chain operations, or protocol errors.
52    #[error("Chain client error: {0}")]
53    Chain(String),
54
55    /// HTTP request to an external API failed.
56    ///
57    /// Wraps reqwest errors for network failures, timeouts, or HTTP error responses.
58    #[error("API request failed: {0}")]
59    Request(#[from] reqwest::Error),
60
61    /// The provided blockchain address is malformed.
62    ///
63    /// Addresses must be valid for their respective chain (e.g., 0x-prefixed
64    /// 40-character hex string for Ethereum).
65    #[error("Invalid address format: {0}")]
66    InvalidAddress(String),
67
68    /// The provided transaction hash is malformed.
69    ///
70    /// Transaction hashes must be valid for their respective chain.
71    #[error("Invalid transaction hash: {0}")]
72    InvalidHash(String),
73
74    /// File system operation failed.
75    #[error("I/O error: {0}")]
76    IoError(#[from] std::io::Error),
77
78    /// File system operation failed (string variant).
79    #[error("I/O error: {0}")]
80    Io(String),
81
82    /// Data export operation failed.
83    #[error("Export failed: {0}")]
84    Export(String),
85
86    /// JSON serialization/deserialization error.
87    #[error("JSON error: {0}")]
88    Json(#[from] serde_json::Error),
89
90    /// Network operation failed.
91    #[error("Network error: {0}")]
92    Network(String),
93
94    /// External API returned an error.
95    #[error("API error: {0}")]
96    Api(String),
97
98    /// Resource not found.
99    #[error("Not found: {0}")]
100    NotFound(String),
101
102    /// Other unspecified error.
103    #[error("{0}")]
104    Other(String),
105}
106
107/// Configuration-specific error type.
108///
109/// Provides detailed context for configuration failures, including
110/// the file path and underlying cause.
111#[derive(Debug, Error)]
112pub enum ConfigError {
113    /// Configuration file was not found at the expected path.
114    #[error("Configuration file not found: {path}")]
115    NotFound {
116        /// The path where the config file was expected.
117        path: PathBuf,
118    },
119
120    /// Failed to read the configuration file.
121    #[error("Failed to read configuration file '{path}': {source}")]
122    Read {
123        /// The path to the configuration file.
124        path: PathBuf,
125        /// The underlying I/O error.
126        #[source]
127        source: std::io::Error,
128    },
129
130    /// Configuration file contains invalid YAML.
131    #[error("Failed to parse configuration: {source}")]
132    Parse {
133        /// The underlying YAML parsing error.
134        #[source]
135        source: serde_yaml::Error,
136    },
137
138    /// Configuration values failed validation.
139    #[error("Invalid configuration: {message}")]
140    Validation {
141        /// Description of the validation failure.
142        message: String,
143    },
144}
145
146/// A specialized [`Result`] type for Scope operations.
147///
148/// This type alias reduces boilerplate by defaulting the error type
149/// to [`ScopeError`].
150///
151/// # Examples
152///
153/// ```rust
154/// use scope::Result;
155///
156/// fn do_something() -> Result<String> {
157///     Ok("success".to_string())
158/// }
159/// ```
160pub type Result<T> = std::result::Result<T, ScopeError>;
161
162// ============================================================================
163// Unit Tests
164// ============================================================================
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_bca_error_display_invalid_address() {
172        let err = ScopeError::InvalidAddress("not-an-address".into());
173        assert_eq!(err.to_string(), "Invalid address format: not-an-address");
174    }
175
176    #[test]
177    fn test_bca_error_display_invalid_hash() {
178        let err = ScopeError::InvalidHash("bad-hash".into());
179        assert_eq!(err.to_string(), "Invalid transaction hash: bad-hash");
180    }
181
182    #[test]
183    fn test_bca_error_display_chain() {
184        let err = ScopeError::Chain("connection refused".into());
185        assert_eq!(err.to_string(), "Chain client error: connection refused");
186    }
187
188    #[test]
189    fn test_bca_error_display_export() {
190        let err = ScopeError::Export("failed to write CSV".into());
191        assert_eq!(err.to_string(), "Export failed: failed to write CSV");
192    }
193
194    #[test]
195    fn test_config_error_not_found() {
196        let err = ConfigError::NotFound {
197            path: PathBuf::from("/missing/config.yaml"),
198        };
199        assert_eq!(
200            err.to_string(),
201            "Configuration file not found: /missing/config.yaml"
202        );
203    }
204
205    #[test]
206    fn test_config_error_validation() {
207        let err = ConfigError::Validation {
208            message: "ethereum_rpc must be a valid URL".into(),
209        };
210        assert_eq!(
211            err.to_string(),
212            "Invalid configuration: ethereum_rpc must be a valid URL"
213        );
214    }
215
216    #[test]
217    fn test_bca_error_from_config_error() {
218        let config_err = ConfigError::NotFound {
219            path: PathBuf::from("/test"),
220        };
221        let bca_err: ScopeError = config_err.into();
222        assert!(matches!(bca_err, ScopeError::Config(_)));
223    }
224
225    #[test]
226    fn test_bca_error_from_io_error() {
227        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
228        let bca_err: ScopeError = io_err.into();
229        assert!(matches!(bca_err, ScopeError::IoError(_)));
230    }
231
232    #[test]
233    fn test_result_type_alias_ok() {
234        fn returns_ok() -> Result<i32> {
235            Ok(42)
236        }
237        assert_eq!(returns_ok().unwrap(), 42);
238    }
239
240    #[test]
241    fn test_result_type_alias_err() {
242        fn returns_err() -> Result<i32> {
243            Err(ScopeError::Chain("test error".into()))
244        }
245        assert!(returns_err().is_err());
246    }
247
248    #[test]
249    fn test_error_debug_impl() {
250        let err = ScopeError::InvalidAddress("0x123".into());
251        let debug_str = format!("{:?}", err);
252        assert!(debug_str.contains("InvalidAddress"));
253        assert!(debug_str.contains("0x123"));
254    }
255
256    #[test]
257    fn test_config_error_source_chain() {
258        use std::error::Error;
259
260        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
261        let config_err = ConfigError::Read {
262            path: PathBuf::from("/etc/bca/config.yaml"),
263            source: io_err,
264        };
265
266        // Verify the error chain works
267        assert!(config_err.source().is_some());
268    }
269}