term_guard/
error.rs

1//! Error types for the Term data validation library.
2//!
3//! This module provides a comprehensive error handling strategy using `thiserror`
4//! for automatic error trait implementations. All errors in the Term library
5//! are represented by the `TermError` enum.
6
7use thiserror::Error;
8
9/// The main error type for the Term library.
10///
11/// This enum represents all possible errors that can occur during
12/// data validation operations.
13#[derive(Error, Debug)]
14pub enum TermError {
15    /// Error that occurs when a validation check fails.
16    #[error("Validation failed: {message}")]
17    ValidationFailed {
18        /// Human-readable error message
19        message: String,
20        /// Name of the check that failed
21        check: String,
22        /// Optional underlying error that caused the validation failure
23        #[source]
24        source: Option<Box<dyn std::error::Error + Send + Sync>>,
25    },
26
27    /// Error that occurs when a constraint evaluation fails.
28    #[error("Constraint evaluation failed for '{constraint}': {message}")]
29    ConstraintEvaluation {
30        /// Name of the constraint that failed
31        constraint: String,
32        /// Detailed error message
33        message: String,
34    },
35
36    /// Error from DataFusion operations.
37    #[error("DataFusion error: {0}")]
38    DataFusion(#[from] datafusion::error::DataFusionError),
39
40    /// Error from Arrow operations.
41    #[error("Arrow error: {0}")]
42    Arrow(#[from] arrow::error::ArrowError),
43
44    /// Error from data source operations.
45    #[error("Data source error: {message}")]
46    DataSource {
47        /// Type of data source (e.g., "CSV", "Parquet", "Database")
48        source_type: String,
49        /// Detailed error message
50        message: String,
51        /// Optional underlying error
52        #[source]
53        source: Option<Box<dyn std::error::Error + Send + Sync>>,
54    },
55
56    /// Error from I/O operations.
57    #[error("IO error: {0}")]
58    Io(#[from] std::io::Error),
59
60    /// Error when parsing or processing data.
61    #[error("Parse error: {0}")]
62    Parse(String),
63
64    /// Error related to configuration.
65    #[error("Configuration error: {0}")]
66    Configuration(String),
67
68    /// Error from serialization/deserialization operations.
69    #[error("Serialization error: {0}")]
70    Serialization(String),
71
72    /// Error from OpenTelemetry operations.
73    #[error("OpenTelemetry error: {0}")]
74    OpenTelemetry(String),
75
76    /// Error when a required column is not found in the dataset.
77    #[error("Column '{column}' not found in dataset")]
78    ColumnNotFound { column: String },
79
80    /// Error when data types don't match expected types.
81    #[error("Type mismatch: expected {expected}, found {found}")]
82    TypeMismatch { expected: String, found: String },
83
84    /// Error when an operation is not supported.
85    #[error("Operation not supported: {0}")]
86    NotSupported(String),
87
88    /// Generic internal error for unexpected conditions.
89    #[error("Internal error: {0}")]
90    Internal(String),
91
92    /// Security-related error.
93    #[error("Security error: {0}")]
94    SecurityError(String),
95
96    /// Error from repository operations.
97    #[error("Repository error ({operation} on {repository_type}): {message}")]
98    Repository {
99        /// Type of repository (e.g., "in_memory", "filesystem", "s3")
100        repository_type: String,
101        /// Operation that failed (e.g., "save", "load", "delete", "query")
102        operation: String,
103        /// Detailed error message
104        message: String,
105        /// Optional underlying error
106        #[source]
107        source: Option<Box<dyn std::error::Error + Send + Sync>>,
108    },
109
110    /// Error when a repository key is invalid or malformed.
111    #[error("Invalid repository key: {message}")]
112    InvalidRepositoryKey {
113        /// The invalid key that caused the error
114        key: String,
115        /// Detailed error message explaining why the key is invalid
116        message: String,
117    },
118
119    /// Error when repository query parameters are invalid.
120    #[error("Invalid repository query: {message}")]
121    InvalidRepositoryQuery {
122        /// Detailed error message describing the invalid query
123        message: String,
124        /// The query parameters that caused the error
125        query_info: String,
126    },
127
128    /// Error when a repository key collision is detected.
129    #[error("Repository key collision detected: {message}")]
130    RepositoryKeyCollision {
131        /// The key that caused the collision
132        key: String,
133        /// Detailed error message
134        message: String,
135    },
136
137    /// Error when repository validation fails.
138    #[error("Repository validation error: {message}")]
139    RepositoryValidation {
140        /// The field or component that failed validation
141        field: String,
142        /// Detailed error message
143        message: String,
144        /// The invalid value that caused the error
145        invalid_value: String,
146    },
147}
148
149/// A type alias for `Result<T, TermError>`.
150///
151/// This is the standard `Result` type used throughout the Term library.
152///
153/// # Examples
154///
155/// ```rust,ignore
156/// use term_guard::error::Result;
157///
158/// fn validate_data() -> Result<()> {
159///     // validation logic here
160///     Ok(())
161/// }
162/// ```
163pub type Result<T> = std::result::Result<T, TermError>;
164
165impl TermError {
166    /// Creates a new validation failed error with the given message and check name.
167    pub fn validation_failed(check: impl Into<String>, message: impl Into<String>) -> Self {
168        Self::ValidationFailed {
169            message: message.into(),
170            check: check.into(),
171            source: None,
172        }
173    }
174
175    /// Creates a new validation failed error with a source error.
176    pub fn validation_failed_with_source(
177        check: impl Into<String>,
178        message: impl Into<String>,
179        source: Box<dyn std::error::Error + Send + Sync>,
180    ) -> Self {
181        Self::ValidationFailed {
182            message: message.into(),
183            check: check.into(),
184            source: Some(source),
185        }
186    }
187
188    /// Creates a new data source error.
189    pub fn data_source(source_type: impl Into<String>, message: impl Into<String>) -> Self {
190        Self::DataSource {
191            source_type: source_type.into(),
192            message: message.into(),
193            source: None,
194        }
195    }
196
197    /// Creates a new data source error with a source error.
198    pub fn data_source_with_source(
199        source_type: impl Into<String>,
200        message: impl Into<String>,
201        source: Box<dyn std::error::Error + Send + Sync>,
202    ) -> Self {
203        Self::DataSource {
204            source_type: source_type.into(),
205            message: message.into(),
206            source: Some(source),
207        }
208    }
209
210    /// Creates a new repository error.
211    pub fn repository(
212        repository_type: impl Into<String>,
213        operation: impl Into<String>,
214        message: impl Into<String>,
215    ) -> Self {
216        Self::Repository {
217            repository_type: repository_type.into(),
218            operation: operation.into(),
219            message: message.into(),
220            source: None,
221        }
222    }
223
224    /// Creates a new repository error with a source error.
225    pub fn repository_with_source(
226        repository_type: impl Into<String>,
227        operation: impl Into<String>,
228        message: impl Into<String>,
229        source: Box<dyn std::error::Error + Send + Sync>,
230    ) -> Self {
231        Self::Repository {
232            repository_type: repository_type.into(),
233            operation: operation.into(),
234            message: message.into(),
235            source: Some(source),
236        }
237    }
238
239    /// Creates a new invalid repository key error.
240    pub fn invalid_repository_key(key: impl Into<String>, message: impl Into<String>) -> Self {
241        Self::InvalidRepositoryKey {
242            key: key.into(),
243            message: message.into(),
244        }
245    }
246
247    /// Creates a new invalid repository query error.
248    pub fn invalid_repository_query(
249        message: impl Into<String>,
250        query_info: impl Into<String>,
251    ) -> Self {
252        Self::InvalidRepositoryQuery {
253            message: message.into(),
254            query_info: query_info.into(),
255        }
256    }
257
258    /// Creates a new repository key collision error.
259    pub fn repository_key_collision(key: impl Into<String>, message: impl Into<String>) -> Self {
260        Self::RepositoryKeyCollision {
261            key: key.into(),
262            message: message.into(),
263        }
264    }
265
266    /// Creates a new repository validation error.
267    pub fn repository_validation(
268        field: impl Into<String>,
269        message: impl Into<String>,
270        invalid_value: impl Into<String>,
271    ) -> Self {
272        Self::RepositoryValidation {
273            field: field.into(),
274            message: message.into(),
275            invalid_value: invalid_value.into(),
276        }
277    }
278
279    /// Creates a new constraint evaluation error.
280    pub fn constraint_evaluation(
281        constraint: impl Into<String>,
282        message: impl Into<String>,
283    ) -> Self {
284        Self::ConstraintEvaluation {
285            constraint: constraint.into(),
286            message: message.into(),
287        }
288    }
289}
290
291/// Conversion from AnalyzerError to TermError.
292impl From<crate::analyzers::AnalyzerError> for TermError {
293    fn from(err: crate::analyzers::AnalyzerError) -> Self {
294        use crate::analyzers::AnalyzerError;
295
296        match err {
297            AnalyzerError::StateComputation(msg) => {
298                TermError::Internal(format!("Analyzer state computation failed: {msg}"))
299            }
300            AnalyzerError::MetricComputation(msg) => {
301                TermError::Internal(format!("Analyzer metric computation failed: {msg}"))
302            }
303            AnalyzerError::StateMerge(msg) => {
304                TermError::Internal(format!("Analyzer state merge failed: {msg}"))
305            }
306            AnalyzerError::QueryExecution(e) => TermError::DataFusion(e),
307            AnalyzerError::ArrowComputation(e) => TermError::Arrow(e),
308            AnalyzerError::InvalidConfiguration(msg) => TermError::Configuration(msg),
309            AnalyzerError::InvalidData(msg) => TermError::Parse(msg),
310            AnalyzerError::NoData => {
311                TermError::Internal("No data available for analysis".to_string())
312            }
313            AnalyzerError::Serialization(msg) => TermError::Serialization(msg),
314            AnalyzerError::Custom(msg) => TermError::Internal(msg),
315        }
316    }
317}
318
319/// Extension trait for adding context to errors.
320pub trait ErrorContext<T> {
321    /// Adds context to an error.
322    fn context(self, msg: &str) -> Result<T>;
323
324    /// Adds context with a lazy message.
325    fn with_context<F>(self, f: F) -> Result<T>
326    where
327        F: FnOnce() -> String;
328}
329
330impl<T, E> ErrorContext<T> for std::result::Result<T, E>
331where
332    E: Into<TermError>,
333{
334    fn context(self, msg: &str) -> Result<T> {
335        self.map_err(|e| {
336            let base_error = e.into();
337            match base_error {
338                TermError::Internal(inner) => TermError::Internal(format!("{msg}: {inner}")),
339                other => TermError::Internal(format!("{msg}: {other}")),
340            }
341        })
342    }
343
344    fn with_context<F>(self, f: F) -> Result<T>
345    where
346        F: FnOnce() -> String,
347    {
348        self.map_err(|e| {
349            let msg = f();
350            let base_error = e.into();
351            match base_error {
352                TermError::Internal(inner) => TermError::Internal(format!("{msg}: {inner}")),
353                other => TermError::Internal(format!("{msg}: {other}")),
354            }
355        })
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use std::error::Error;
363
364    #[test]
365    fn test_validation_failed_error() {
366        let err = TermError::validation_failed("completeness_check", "Too many null values");
367        assert_eq!(err.to_string(), "Validation failed: Too many null values");
368    }
369
370    #[test]
371    fn test_error_with_source() {
372        let source = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
373        let err = TermError::validation_failed_with_source(
374            "file_check",
375            "Could not read validation file",
376            Box::new(source),
377        );
378
379        // Check that source is preserved
380        assert!(err.source().is_some());
381    }
382
383    #[test]
384    fn test_data_source_error() {
385        let err = TermError::data_source("CSV", "Invalid file format");
386        assert_eq!(err.to_string(), "Data source error: Invalid file format");
387    }
388
389    #[test]
390    fn test_column_not_found() {
391        let err = TermError::ColumnNotFound {
392            column: "user_id".to_string(),
393        };
394        assert_eq!(err.to_string(), "Column 'user_id' not found in dataset");
395    }
396
397    #[test]
398    fn test_type_mismatch() {
399        let err = TermError::TypeMismatch {
400            expected: "Int64".to_string(),
401            found: "Utf8".to_string(),
402        };
403        assert_eq!(err.to_string(), "Type mismatch: expected Int64, found Utf8");
404    }
405
406    #[test]
407    fn test_error_context() {
408        fn failing_operation() -> Result<()> {
409            Err(TermError::Internal("Something went wrong".to_string()))
410        }
411
412        let result = failing_operation().context("During data validation");
413        assert!(result.is_err());
414        let err = result.unwrap_err();
415        assert!(err.to_string().contains("During data validation"));
416    }
417}