1use thiserror::Error;
8
9#[derive(Error, Debug)]
14pub enum TermError {
15 #[error("Validation failed: {message}")]
17 ValidationFailed {
18 message: String,
20 check: String,
22 #[source]
24 source: Option<Box<dyn std::error::Error + Send + Sync>>,
25 },
26
27 #[error("Constraint evaluation failed for '{constraint}': {message}")]
29 ConstraintEvaluation {
30 constraint: String,
32 message: String,
34 },
35
36 #[error("DataFusion error: {0}")]
38 DataFusion(#[from] datafusion::error::DataFusionError),
39
40 #[error("Arrow error: {0}")]
42 Arrow(#[from] arrow::error::ArrowError),
43
44 #[error("Data source error: {message}")]
46 DataSource {
47 source_type: String,
49 message: String,
51 #[source]
53 source: Option<Box<dyn std::error::Error + Send + Sync>>,
54 },
55
56 #[error("IO error: {0}")]
58 Io(#[from] std::io::Error),
59
60 #[error("Parse error: {0}")]
62 Parse(String),
63
64 #[error("Configuration error: {0}")]
66 Configuration(String),
67
68 #[error("Serialization error: {0}")]
70 Serialization(String),
71
72 #[error("OpenTelemetry error: {0}")]
74 OpenTelemetry(String),
75
76 #[error("Column '{column}' not found in dataset")]
78 ColumnNotFound { column: String },
79
80 #[error("Type mismatch: expected {expected}, found {found}")]
82 TypeMismatch { expected: String, found: String },
83
84 #[error("Operation not supported: {0}")]
86 NotSupported(String),
87
88 #[error("Internal error: {0}")]
90 Internal(String),
91
92 #[error("Security error: {0}")]
94 SecurityError(String),
95}
96
97pub type Result<T> = std::result::Result<T, TermError>;
112
113impl TermError {
114 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 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 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 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 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
170pub trait ErrorContext<T> {
172 fn context(self, msg: &str) -> Result<T>;
174
175 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 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}