openzeppelin_monitor/models/config/
error.rs

1//! Configuration error types.
2//!
3//! This module defines the error types that can occur during configuration
4//! loading and validation.
5
6use crate::utils::logging::error::{ErrorContext, TraceableError};
7use std::collections::HashMap;
8use thiserror::Error as ThisError;
9use uuid::Uuid;
10
11/// Represents errors that can occur during configuration operations
12#[derive(ThisError, Debug)]
13pub enum ConfigError {
14	/// Errors related to validation failures
15	#[error("Validation error: {0}")]
16	ValidationError(ErrorContext),
17
18	/// Errors related to parsing failures
19	#[error("Parse error: {0}")]
20	ParseError(ErrorContext),
21
22	/// Errors related to file system errors
23	#[error("File error: {0}")]
24	FileError(ErrorContext),
25
26	/// Other errors that don't fit into the categories above
27	#[error(transparent)]
28	Other(#[from] anyhow::Error),
29}
30
31impl ConfigError {
32	// Validation error
33	pub fn validation_error(
34		msg: impl Into<String>,
35		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
36		metadata: Option<HashMap<String, String>>,
37	) -> Self {
38		// We explicitly do not use new_with_log here because we want to log the error
39		// at from the context of the repository
40		Self::ValidationError(ErrorContext::new(msg, source, metadata))
41	}
42
43	// Parse error
44	pub fn parse_error(
45		msg: impl Into<String>,
46		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
47		metadata: Option<HashMap<String, String>>,
48	) -> Self {
49		// We explicitly do not use new_with_log here because we want to log the error
50		// at from the context of the repository
51		Self::ParseError(ErrorContext::new(msg, source, metadata))
52	}
53
54	// File error
55	pub fn file_error(
56		msg: impl Into<String>,
57		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
58		metadata: Option<HashMap<String, String>>,
59	) -> Self {
60		// We explicitly do not use new_with_log here because we want to log the error
61		// at from the context of the repository
62		Self::FileError(ErrorContext::new(msg, source, metadata))
63	}
64}
65
66impl TraceableError for ConfigError {
67	fn trace_id(&self) -> String {
68		match self {
69			Self::ValidationError(ctx) => ctx.trace_id.clone(),
70			Self::ParseError(ctx) => ctx.trace_id.clone(),
71			Self::FileError(ctx) => ctx.trace_id.clone(),
72			Self::Other(_) => Uuid::new_v4().to_string(),
73		}
74	}
75}
76
77impl From<std::io::Error> for ConfigError {
78	fn from(err: std::io::Error) -> Self {
79		Self::file_error(err.to_string(), None, None)
80	}
81}
82
83impl From<serde_json::Error> for ConfigError {
84	fn from(err: serde_json::Error) -> Self {
85		Self::parse_error(err.to_string(), None, None)
86	}
87}
88
89#[cfg(test)]
90mod tests {
91	use super::*;
92	use std::io::{Error as IoError, ErrorKind};
93
94	#[test]
95	fn test_validation_error_formatting() {
96		let error = ConfigError::validation_error("test error", None, None);
97		assert_eq!(error.to_string(), "Validation error: test error");
98
99		let source_error = IoError::new(ErrorKind::NotFound, "test source");
100		let error = ConfigError::validation_error(
101			"test error",
102			Some(Box::new(source_error)),
103			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
104		);
105		assert_eq!(
106			error.to_string(),
107			"Validation error: test error [key1=value1]"
108		);
109	}
110
111	#[test]
112	fn test_parse_error_formatting() {
113		let error = ConfigError::parse_error("test error", None, None);
114		assert_eq!(error.to_string(), "Parse error: test error");
115
116		let source_error = IoError::new(ErrorKind::NotFound, "test source");
117		let error = ConfigError::parse_error(
118			"test error",
119			Some(Box::new(source_error)),
120			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
121		);
122		assert_eq!(error.to_string(), "Parse error: test error [key1=value1]");
123	}
124
125	#[test]
126	fn test_file_error_formatting() {
127		let error = ConfigError::file_error("test error", None, None);
128		assert_eq!(error.to_string(), "File error: test error");
129
130		let source_error = IoError::new(ErrorKind::NotFound, "test source");
131		let error = ConfigError::file_error(
132			"test error",
133			Some(Box::new(source_error)),
134			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
135		);
136
137		assert_eq!(error.to_string(), "File error: test error [key1=value1]");
138	}
139
140	#[test]
141	fn test_from_anyhow_error() {
142		let anyhow_error = anyhow::anyhow!("test anyhow error");
143		let config_error: ConfigError = anyhow_error.into();
144		assert!(matches!(config_error, ConfigError::Other(_)));
145		assert_eq!(config_error.to_string(), "test anyhow error");
146	}
147
148	#[test]
149	fn test_error_source_chain() {
150		let io_error = std::io::Error::new(std::io::ErrorKind::Other, "while reading config");
151
152		let outer_error =
153			ConfigError::file_error("Failed to initialize", Some(Box::new(io_error)), None);
154
155		// Just test the string representation instead of the source chain
156		assert!(outer_error.to_string().contains("Failed to initialize"));
157
158		// For ConfigError::FileError, we know the implementation details
159		if let ConfigError::FileError(ctx) = &outer_error {
160			// Check that the context has the right message
161			assert_eq!(ctx.message, "Failed to initialize");
162
163			// Check that the context has the source error
164			assert!(ctx.source.is_some());
165
166			if let Some(src) = &ctx.source {
167				assert_eq!(src.to_string(), "while reading config");
168			}
169		} else {
170			panic!("Expected FileError variant");
171		}
172	}
173
174	#[test]
175	fn test_io_error_conversion() {
176		let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
177		let config_error: ConfigError = io_error.into();
178		assert!(matches!(config_error, ConfigError::FileError(_)));
179	}
180
181	#[test]
182	fn test_serde_error_conversion() {
183		let json = "invalid json";
184		let serde_error = serde_json::from_str::<serde_json::Value>(json).unwrap_err();
185		let config_error: ConfigError = serde_error.into();
186		assert!(matches!(config_error, ConfigError::ParseError(_)));
187	}
188
189	#[test]
190	fn test_trace_id_propagation() {
191		// Create an error context with a known trace ID
192		let error_context = ErrorContext::new("Inner error", None, None);
193		let original_trace_id = error_context.trace_id.clone();
194
195		// Wrap it in a ConfigError
196		let config_error = ConfigError::FileError(error_context);
197
198		// Verify the trace ID is preserved
199		assert_eq!(config_error.trace_id(), original_trace_id);
200
201		// Test trace ID propagation through error chain
202		let source_error = IoError::new(ErrorKind::Other, "Source error");
203		let error_context = ErrorContext::new("Middle error", Some(Box::new(source_error)), None);
204		let original_trace_id = error_context.trace_id.clone();
205
206		let config_error = ConfigError::FileError(error_context);
207		assert_eq!(config_error.trace_id(), original_trace_id);
208
209		// Test Other variant
210		let anyhow_error = anyhow::anyhow!("Test anyhow error");
211		let config_error: ConfigError = anyhow_error.into();
212
213		// Other variant should generate a new UUID
214		assert!(!config_error.trace_id().is_empty());
215	}
216}