use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EvaluationErrorKind {
UndefinedFunction {
name: String,
},
ArgumentCount {
expected: Option<u32>,
actual: Option<u32>,
},
TypeError {
detail: String,
},
Other,
}
impl fmt::Display for EvaluationErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EvaluationErrorKind::UndefinedFunction { name } => {
write!(f, "undefined function '{}'", name)
}
EvaluationErrorKind::ArgumentCount {
expected: Some(exp),
actual: Some(act),
} => write!(f, "expected {} arguments, found {}", exp, act),
EvaluationErrorKind::ArgumentCount { .. } => write!(f, "wrong number of arguments"),
EvaluationErrorKind::TypeError { detail } => write!(f, "type error: {}", detail),
EvaluationErrorKind::Other => write!(f, "evaluation error"),
}
}
}
pub(crate) fn classify_evaluation_error(message: &str) -> EvaluationErrorKind {
if let Some(rest) = message
.strip_prefix("Runtime error: Call to undefined function ")
.or_else(|| message.strip_prefix("Call to undefined function "))
{
let name = rest.split([' ', '(']).next().unwrap_or(rest).to_string();
return EvaluationErrorKind::UndefinedFunction { name };
}
if message.contains("arguments: expected") {
let expected = extract_number_after(message, "expected ");
let actual = extract_number_after(message, "found ");
return EvaluationErrorKind::ArgumentCount { expected, actual };
}
if message.contains("expects type") {
let detail = message
.strip_prefix("Runtime error: ")
.unwrap_or(message)
.to_string();
return EvaluationErrorKind::TypeError { detail };
}
EvaluationErrorKind::Other
}
fn extract_number_after(s: &str, prefix: &str) -> Option<u32> {
let idx = s.find(prefix)?;
let after = &s[idx + prefix.len()..];
let num_str: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
num_str.parse().ok()
}
#[derive(Debug, Error)]
pub enum EngineError {
#[error("Invalid expression: {0}")]
InvalidExpression(String),
#[error("Invalid JSON: {0}")]
InvalidJson(String),
#[error("Evaluation failed: {message}")]
EvaluationFailed {
message: String,
kind: EvaluationErrorKind,
},
#[error("Unknown function: {0}")]
UnknownFunction(String),
#[error("Query not found: {0}")]
QueryNotFound(String),
#[error("Registration failed: {0}")]
RegistrationFailed(String),
#[error("Config error: {0}")]
ConfigError(String),
#[error("Internal error: {0}")]
Internal(String),
#[cfg(feature = "arrow")]
#[error("Arrow error: {0}")]
ArrowError(String),
}
impl EngineError {
pub(crate) fn evaluation_failed(message: String) -> Self {
let kind = classify_evaluation_error(&message);
EngineError::EvaluationFailed { message, kind }
}
}
pub type Result<T> = std::result::Result<T, EngineError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_classify_undefined_function() {
let kind = classify_evaluation_error(
"Runtime error: Call to undefined function foo_bar (line 0, column 7)",
);
assert_eq!(
kind,
EvaluationErrorKind::UndefinedFunction {
name: "foo_bar".to_string()
}
);
}
#[test]
fn test_classify_too_many_arguments() {
let kind = classify_evaluation_error(
"Runtime error: Too many arguments: expected 1, found 2 (line 0, column 6)",
);
assert_eq!(
kind,
EvaluationErrorKind::ArgumentCount {
expected: Some(1),
actual: Some(2)
}
);
}
#[test]
fn test_classify_not_enough_arguments() {
let kind = classify_evaluation_error(
"Runtime error: Not enough arguments: expected 2, found 1 (line 0, column 4)",
);
assert_eq!(
kind,
EvaluationErrorKind::ArgumentCount {
expected: Some(2),
actual: Some(1)
}
);
}
#[test]
fn test_classify_type_error() {
let kind = classify_evaluation_error(
"Runtime error: Argument 0 expects type array[number], given object (line 0, column 3)",
);
match kind {
EvaluationErrorKind::TypeError { detail } => {
assert!(detail.contains("expects type"));
}
other => panic!("Expected TypeError, got {:?}", other),
}
}
#[test]
fn test_classify_other() {
let kind = classify_evaluation_error("Some unknown error");
assert_eq!(kind, EvaluationErrorKind::Other);
}
#[test]
fn test_display_undefined_function() {
let kind = EvaluationErrorKind::UndefinedFunction {
name: "foo".to_string(),
};
assert_eq!(kind.to_string(), "undefined function 'foo'");
}
#[test]
fn test_display_argument_count_with_numbers() {
let kind = EvaluationErrorKind::ArgumentCount {
expected: Some(2),
actual: Some(3),
};
assert_eq!(kind.to_string(), "expected 2 arguments, found 3");
}
#[test]
fn test_display_argument_count_without_numbers() {
let kind = EvaluationErrorKind::ArgumentCount {
expected: None,
actual: None,
};
assert_eq!(kind.to_string(), "wrong number of arguments");
}
#[test]
fn test_display_type_error() {
let kind = EvaluationErrorKind::TypeError {
detail: "some detail".to_string(),
};
assert_eq!(kind.to_string(), "type error: some detail");
}
#[test]
fn test_display_other() {
let kind = EvaluationErrorKind::Other;
assert_eq!(kind.to_string(), "evaluation error");
}
#[test]
fn test_engine_error_display_invalid_expression() {
let err = EngineError::InvalidExpression("unexpected token".to_string());
assert_eq!(err.to_string(), "Invalid expression: unexpected token");
}
#[test]
fn test_engine_error_display_invalid_json() {
let err = EngineError::InvalidJson("expected value at line 1".to_string());
assert_eq!(err.to_string(), "Invalid JSON: expected value at line 1");
}
#[test]
fn test_engine_error_display_evaluation_failed() {
let err = EngineError::EvaluationFailed {
message: "something went wrong".to_string(),
kind: EvaluationErrorKind::Other,
};
assert_eq!(err.to_string(), "Evaluation failed: something went wrong");
}
#[test]
fn test_engine_error_display_all_variants() {
let unknown = EngineError::UnknownFunction("mystery".to_string());
assert_eq!(unknown.to_string(), "Unknown function: mystery");
let not_found = EngineError::QueryNotFound("my_query".to_string());
assert_eq!(not_found.to_string(), "Query not found: my_query");
let reg_failed = EngineError::RegistrationFailed("duplicate name".to_string());
assert_eq!(
reg_failed.to_string(),
"Registration failed: duplicate name"
);
let config = EngineError::ConfigError("missing field".to_string());
assert_eq!(config.to_string(), "Config error: missing field");
let internal = EngineError::Internal("lock poisoned".to_string());
assert_eq!(internal.to_string(), "Internal error: lock poisoned");
}
#[test]
fn test_evaluation_failed_constructor() {
let err = EngineError::evaluation_failed("Call to undefined function foo".to_string());
match err {
EngineError::EvaluationFailed {
ref message,
ref kind,
} => {
assert_eq!(message, "Call to undefined function foo");
assert_eq!(
*kind,
EvaluationErrorKind::UndefinedFunction {
name: "foo".to_string()
}
);
}
other => panic!("Expected EvaluationFailed, got {:?}", other),
}
}
}