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    /// Compliance or risk analysis failed.
103    #[error("Compliance error: {0}")]
104    Compliance(String),
105
106    /// Token discovery operation failed.
107    #[error("Discovery error: {0}")]
108    Discovery(String),
109
110    /// Contract analysis failed.
111    #[error("Contract error: {0}")]
112    Contract(String),
113
114    /// Other unspecified error.
115    #[error("{0}")]
116    Other(String),
117}
118
119/// Configuration-specific error type.
120///
121/// Provides detailed context for configuration failures, including
122/// the file path and underlying cause.
123#[derive(Debug, Error)]
124pub enum ConfigError {
125    /// Configuration file was not found at the expected path.
126    #[error("Configuration file not found: {path}")]
127    NotFound {
128        /// The path where the config file was expected.
129        path: PathBuf,
130    },
131
132    /// Failed to read the configuration file.
133    #[error("Failed to read configuration file '{path}': {source}")]
134    Read {
135        /// The path to the configuration file.
136        path: PathBuf,
137        /// The underlying I/O error.
138        #[source]
139        source: std::io::Error,
140    },
141
142    /// Configuration file contains invalid YAML.
143    #[error("Failed to parse configuration: {source}")]
144    Parse {
145        /// The underlying YAML parsing error.
146        #[source]
147        source: serde_yaml::Error,
148    },
149
150    /// Configuration values failed validation.
151    #[error("Invalid configuration: {message}")]
152    Validation {
153        /// Description of the validation failure.
154        message: String,
155    },
156}
157
158/// A specialized [`Result`] type for Scope operations.
159///
160/// This type alias reduces boilerplate by defaulting the error type
161/// to [`ScopeError`].
162///
163/// # Examples
164///
165/// ```rust
166/// use scope::Result;
167///
168/// fn do_something() -> Result<String> {
169///     Ok("success".to_string())
170/// }
171/// ```
172pub type Result<T> = std::result::Result<T, ScopeError>;
173
174// ============================================================================
175// Unit Tests
176// ============================================================================
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_bca_error_display_invalid_address() {
184        let err = ScopeError::InvalidAddress("not-an-address".into());
185        assert_eq!(err.to_string(), "Invalid address format: not-an-address");
186    }
187
188    #[test]
189    fn test_bca_error_display_invalid_hash() {
190        let err = ScopeError::InvalidHash("bad-hash".into());
191        assert_eq!(err.to_string(), "Invalid transaction hash: bad-hash");
192    }
193
194    #[test]
195    fn test_bca_error_display_chain() {
196        let err = ScopeError::Chain("connection refused".into());
197        assert_eq!(err.to_string(), "Chain client error: connection refused");
198    }
199
200    #[test]
201    fn test_bca_error_display_export() {
202        let err = ScopeError::Export("failed to write CSV".into());
203        assert_eq!(err.to_string(), "Export failed: failed to write CSV");
204    }
205
206    #[test]
207    fn test_config_error_not_found() {
208        let err = ConfigError::NotFound {
209            path: PathBuf::from("/missing/config.yaml"),
210        };
211        assert_eq!(
212            err.to_string(),
213            "Configuration file not found: /missing/config.yaml"
214        );
215    }
216
217    #[test]
218    fn test_config_error_validation() {
219        let err = ConfigError::Validation {
220            message: "ethereum_rpc must be a valid URL".into(),
221        };
222        assert_eq!(
223            err.to_string(),
224            "Invalid configuration: ethereum_rpc must be a valid URL"
225        );
226    }
227
228    #[test]
229    fn test_bca_error_from_config_error() {
230        let config_err = ConfigError::NotFound {
231            path: PathBuf::from("/test"),
232        };
233        let bca_err: ScopeError = config_err.into();
234        assert!(matches!(bca_err, ScopeError::Config(_)));
235    }
236
237    #[test]
238    fn test_bca_error_from_io_error() {
239        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
240        let bca_err: ScopeError = io_err.into();
241        assert!(matches!(bca_err, ScopeError::IoError(_)));
242    }
243
244    #[test]
245    fn test_result_type_alias_ok() {
246        fn returns_ok() -> Result<i32> {
247            Ok(42)
248        }
249        assert_eq!(returns_ok().unwrap(), 42);
250    }
251
252    #[test]
253    fn test_result_type_alias_err() {
254        fn returns_err() -> Result<i32> {
255            Err(ScopeError::Chain("test error".into()))
256        }
257        assert!(returns_err().is_err());
258    }
259
260    #[test]
261    fn test_error_debug_impl() {
262        let err = ScopeError::InvalidAddress("0x123".into());
263        let debug_str = format!("{:?}", err);
264        assert!(debug_str.contains("InvalidAddress"));
265        assert!(debug_str.contains("0x123"));
266    }
267
268    #[test]
269    fn test_config_error_source_chain() {
270        use std::error::Error;
271
272        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
273        let config_err = ConfigError::Read {
274            path: PathBuf::from("/etc/scope/config.yaml"),
275            source: io_err,
276        };
277
278        // Verify the error chain works
279        assert!(config_err.source().is_some());
280    }
281}