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
97/// A type alias for `Result<T, TermError>`.
98///
99/// This is the standard `Result` type used throughout the Term library.
100///
101/// # Examples
102///
103/// ```rust,ignore
104/// use term_guard::error::Result;
105///
106/// fn validate_data() -> Result<()> {
107///     // validation logic here
108///     Ok(())
109/// }
110/// ```
111pub type Result<T> = std::result::Result<T, TermError>;
112
113impl TermError {
114    /// Creates a new validation failed error with the given message and check name.
115    pub fn validation_failed(check: impl Into<String>, message: impl Into<String>) -> Self {
116        Self::ValidationFailed {
117            message: message.into(),
118            check: check.into(),
119            source: None,
120        }
121    }
122
123    /// Creates a new validation failed error with a source error.
124    pub fn validation_failed_with_source(
125        check: impl Into<String>,
126        message: impl Into<String>,
127        source: Box<dyn std::error::Error + Send + Sync>,
128    ) -> Self {
129        Self::ValidationFailed {
130            message: message.into(),
131            check: check.into(),
132            source: Some(source),
133        }
134    }
135
136    /// Creates a new data source error.
137    pub fn data_source(source_type: impl Into<String>, message: impl Into<String>) -> Self {
138        Self::DataSource {
139            source_type: source_type.into(),
140            message: message.into(),
141            source: None,
142        }
143    }
144
145    /// Creates a new data source error with a source error.
146    pub fn data_source_with_source(
147        source_type: impl Into<String>,
148        message: impl Into<String>,
149        source: Box<dyn std::error::Error + Send + Sync>,
150    ) -> Self {
151        Self::DataSource {
152            source_type: source_type.into(),
153            message: message.into(),
154            source: Some(source),
155        }
156    }
157
158    /// Creates a new constraint evaluation error.
159    pub fn constraint_evaluation(
160        constraint: impl Into<String>,
161        message: impl Into<String>,
162    ) -> Self {
163        Self::ConstraintEvaluation {
164            constraint: constraint.into(),
165            message: message.into(),
166        }
167    }
168}
169
170/// Extension trait for adding context to errors.
171pub trait ErrorContext<T> {
172    /// Adds context to an error.
173    fn context(self, msg: &str) -> Result<T>;
174
175    /// Adds context with a lazy message.
176    fn with_context<F>(self, f: F) -> Result<T>
177    where
178        F: FnOnce() -> String;
179}
180
181impl<T, E> ErrorContext<T> for std::result::Result<T, E>
182where
183    E: Into<TermError>,
184{
185    fn context(self, msg: &str) -> Result<T> {
186        self.map_err(|e| {
187            let base_error = e.into();
188            match base_error {
189                TermError::Internal(inner) => TermError::Internal(format!("{msg}: {inner}")),
190                other => TermError::Internal(format!("{msg}: {other}")),
191            }
192        })
193    }
194
195    fn with_context<F>(self, f: F) -> Result<T>
196    where
197        F: FnOnce() -> String,
198    {
199        self.map_err(|e| {
200            let msg = f();
201            let base_error = e.into();
202            match base_error {
203                TermError::Internal(inner) => TermError::Internal(format!("{msg}: {inner}")),
204                other => TermError::Internal(format!("{msg}: {other}")),
205            }
206        })
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use std::error::Error;
214
215    #[test]
216    fn test_validation_failed_error() {
217        let err = TermError::validation_failed("completeness_check", "Too many null values");
218        assert_eq!(err.to_string(), "Validation failed: Too many null values");
219    }
220
221    #[test]
222    fn test_error_with_source() {
223        let source = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
224        let err = TermError::validation_failed_with_source(
225            "file_check",
226            "Could not read validation file",
227            Box::new(source),
228        );
229
230        // Check that source is preserved
231        assert!(err.source().is_some());
232    }
233
234    #[test]
235    fn test_data_source_error() {
236        let err = TermError::data_source("CSV", "Invalid file format");
237        assert_eq!(err.to_string(), "Data source error: Invalid file format");
238    }
239
240    #[test]
241    fn test_column_not_found() {
242        let err = TermError::ColumnNotFound {
243            column: "user_id".to_string(),
244        };
245        assert_eq!(err.to_string(), "Column 'user_id' not found in dataset");
246    }
247
248    #[test]
249    fn test_type_mismatch() {
250        let err = TermError::TypeMismatch {
251            expected: "Int64".to_string(),
252            found: "Utf8".to_string(),
253        };
254        assert_eq!(err.to_string(), "Type mismatch: expected Int64, found Utf8");
255    }
256
257    #[test]
258    fn test_error_context() {
259        fn failing_operation() -> Result<()> {
260            Err(TermError::Internal("Something went wrong".to_string()))
261        }
262
263        let result = failing_operation().context("During data validation");
264        assert!(result.is_err());
265        let err = result.unwrap_err();
266        assert!(err.to_string().contains("During data validation"));
267    }
268}