use thiserror::Error;
#[derive(Error, Debug)]
pub enum TermError {
#[error("Validation failed: {message}")]
ValidationFailed {
message: String,
check: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Constraint evaluation failed for '{constraint}': {message}")]
ConstraintEvaluation {
constraint: String,
message: String,
},
#[error("DataFusion error: {0}")]
DataFusion(#[from] datafusion::error::DataFusionError),
#[error("Arrow error: {0}")]
Arrow(#[from] arrow::error::ArrowError),
#[error("Data source error: {message}")]
DataSource {
source_type: String,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
Parse(String),
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("OpenTelemetry error: {0}")]
OpenTelemetry(String),
#[error("Column '{column}' not found in dataset")]
ColumnNotFound { column: String },
#[error("Type mismatch: expected {expected}, found {found}")]
TypeMismatch { expected: String, found: String },
#[error("Operation not supported: {0}")]
NotSupported(String),
#[error("Internal error: {0}")]
Internal(String),
#[error("Security error: {0}")]
SecurityError(String),
#[error("Repository error ({operation} on {repository_type}): {message}")]
Repository {
repository_type: String,
operation: String,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Invalid repository key: {message}")]
InvalidRepositoryKey {
key: String,
message: String,
},
#[error("Invalid repository query: {message}")]
InvalidRepositoryQuery {
message: String,
query_info: String,
},
#[error("Repository key collision detected: {message}")]
RepositoryKeyCollision {
key: String,
message: String,
},
#[error("Repository validation error: {message}")]
RepositoryValidation {
field: String,
message: String,
invalid_value: String,
},
}
pub type Result<T> = std::result::Result<T, TermError>;
impl TermError {
pub fn validation_failed(check: impl Into<String>, message: impl Into<String>) -> Self {
Self::ValidationFailed {
message: message.into(),
check: check.into(),
source: None,
}
}
pub fn validation_failed_with_source(
check: impl Into<String>,
message: impl Into<String>,
source: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self::ValidationFailed {
message: message.into(),
check: check.into(),
source: Some(source),
}
}
pub fn data_source(source_type: impl Into<String>, message: impl Into<String>) -> Self {
Self::DataSource {
source_type: source_type.into(),
message: message.into(),
source: None,
}
}
pub fn data_source_with_source(
source_type: impl Into<String>,
message: impl Into<String>,
source: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self::DataSource {
source_type: source_type.into(),
message: message.into(),
source: Some(source),
}
}
pub fn repository(
repository_type: impl Into<String>,
operation: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::Repository {
repository_type: repository_type.into(),
operation: operation.into(),
message: message.into(),
source: None,
}
}
pub fn repository_with_source(
repository_type: impl Into<String>,
operation: impl Into<String>,
message: impl Into<String>,
source: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self::Repository {
repository_type: repository_type.into(),
operation: operation.into(),
message: message.into(),
source: Some(source),
}
}
pub fn invalid_repository_key(key: impl Into<String>, message: impl Into<String>) -> Self {
Self::InvalidRepositoryKey {
key: key.into(),
message: message.into(),
}
}
pub fn invalid_repository_query(
message: impl Into<String>,
query_info: impl Into<String>,
) -> Self {
Self::InvalidRepositoryQuery {
message: message.into(),
query_info: query_info.into(),
}
}
pub fn repository_key_collision(key: impl Into<String>, message: impl Into<String>) -> Self {
Self::RepositoryKeyCollision {
key: key.into(),
message: message.into(),
}
}
pub fn repository_validation(
field: impl Into<String>,
message: impl Into<String>,
invalid_value: impl Into<String>,
) -> Self {
Self::RepositoryValidation {
field: field.into(),
message: message.into(),
invalid_value: invalid_value.into(),
}
}
pub fn constraint_evaluation(
constraint: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::ConstraintEvaluation {
constraint: constraint.into(),
message: message.into(),
}
}
}
impl From<crate::analyzers::AnalyzerError> for TermError {
fn from(err: crate::analyzers::AnalyzerError) -> Self {
use crate::analyzers::AnalyzerError;
match err {
AnalyzerError::StateComputation(msg) => {
TermError::Internal(format!("Analyzer state computation failed: {msg}"))
}
AnalyzerError::MetricComputation(msg) => {
TermError::Internal(format!("Analyzer metric computation failed: {msg}"))
}
AnalyzerError::StateMerge(msg) => {
TermError::Internal(format!("Analyzer state merge failed: {msg}"))
}
AnalyzerError::QueryExecution(e) => TermError::DataFusion(e),
AnalyzerError::ArrowComputation(e) => TermError::Arrow(e),
AnalyzerError::InvalidConfiguration(msg) => TermError::Configuration(msg),
AnalyzerError::InvalidData(msg) => TermError::Parse(msg),
AnalyzerError::NoData => {
TermError::Internal("No data available for analysis".to_string())
}
AnalyzerError::Serialization(msg) => TermError::Serialization(msg),
AnalyzerError::Custom(msg) => TermError::Internal(msg),
}
}
}
pub trait ErrorContext<T> {
fn context(self, msg: &str) -> Result<T>;
fn with_context<F>(self, f: F) -> Result<T>
where
F: FnOnce() -> String;
}
impl<T, E> ErrorContext<T> for std::result::Result<T, E>
where
E: Into<TermError>,
{
fn context(self, msg: &str) -> Result<T> {
self.map_err(|e| {
let base_error = e.into();
match base_error {
TermError::Internal(inner) => TermError::Internal(format!("{msg}: {inner}")),
other => TermError::Internal(format!("{msg}: {other}")),
}
})
}
fn with_context<F>(self, f: F) -> Result<T>
where
F: FnOnce() -> String,
{
self.map_err(|e| {
let msg = f();
let base_error = e.into();
match base_error {
TermError::Internal(inner) => TermError::Internal(format!("{msg}: {inner}")),
other => TermError::Internal(format!("{msg}: {other}")),
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn test_validation_failed_error() {
let err = TermError::validation_failed("completeness_check", "Too many null values");
assert_eq!(err.to_string(), "Validation failed: Too many null values");
}
#[test]
fn test_error_with_source() {
let source = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
let err = TermError::validation_failed_with_source(
"file_check",
"Could not read validation file",
Box::new(source),
);
assert!(err.source().is_some());
}
#[test]
fn test_data_source_error() {
let err = TermError::data_source("CSV", "Invalid file format");
assert_eq!(err.to_string(), "Data source error: Invalid file format");
}
#[test]
fn test_column_not_found() {
let err = TermError::ColumnNotFound {
column: "user_id".to_string(),
};
assert_eq!(err.to_string(), "Column 'user_id' not found in dataset");
}
#[test]
fn test_type_mismatch() {
let err = TermError::TypeMismatch {
expected: "Int64".to_string(),
found: "Utf8".to_string(),
};
assert_eq!(err.to_string(), "Type mismatch: expected Int64, found Utf8");
}
#[test]
fn test_error_context() {
fn failing_operation() -> Result<()> {
Err(TermError::Internal("Something went wrong".to_string()))
}
let result = failing_operation().context("During data validation");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("During data validation"));
}
}