use crate::engine::error::{DataflowError, ErrorInfo, Result};
use crate::engine::message::{Change, Message};
use datalogic_rs::{CompiledLogic, DataLogic};
use log::{debug, error};
use serde::Deserialize;
use serde_json::Value;
use std::sync::Arc;
#[derive(Debug, Clone, Deserialize)]
pub struct ValidationConfig {
pub rules: Vec<ValidationRule>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ValidationRule {
pub logic: Value,
pub message: String,
#[serde(skip)]
pub logic_index: Option<usize>,
}
impl ValidationConfig {
pub fn from_json(input: &Value) -> Result<Self> {
let rules = input.get("rules").ok_or_else(|| {
DataflowError::Validation("Missing 'rules' array in input".to_string())
})?;
let rules_arr = rules
.as_array()
.ok_or_else(|| DataflowError::Validation("'rules' must be an array".to_string()))?;
let mut parsed_rules = Vec::new();
for rule in rules_arr {
let logic = rule
.get("logic")
.ok_or_else(|| DataflowError::Validation("Missing 'logic' in rule".to_string()))?
.clone();
let message = rule
.get("message")
.and_then(Value::as_str)
.unwrap_or("Validation failed")
.to_string();
parsed_rules.push(ValidationRule {
logic,
message,
logic_index: None,
});
}
Ok(ValidationConfig {
rules: parsed_rules,
})
}
pub fn execute(
&self,
message: &mut Message,
datalogic: &Arc<DataLogic>,
logic_cache: &[Arc<CompiledLogic>],
) -> Result<(usize, Vec<Change>)> {
let changes = Vec::new();
let mut validation_errors = Vec::new();
let context_arc = message.get_context_arc();
for (idx, rule) in self.rules.iter().enumerate() {
debug!("Processing validation rule {}: {}", idx, rule.message);
let compiled_logic = match rule.logic_index {
Some(index) => {
if index >= logic_cache.len() {
error!(
"Validation: Logic index {} out of bounds (cache size: {}) for rule at index {}",
index,
logic_cache.len(),
idx
);
validation_errors.push(ErrorInfo::simple_ref(
"COMPILATION_ERROR",
&format!(
"Logic index {} out of bounds for rule at index {}",
index, idx
),
None,
));
continue;
}
&logic_cache[index]
}
None => {
error!(
"Validation: Logic not compiled (no index) for rule at index {}",
idx
);
validation_errors.push(ErrorInfo::simple_ref(
"COMPILATION_ERROR",
&format!("Logic not compiled for rule at index: {}", idx),
None,
));
continue;
}
};
let result = datalogic.evaluate(compiled_logic, Arc::clone(&context_arc));
match result {
Ok(value) => {
if value != Value::Bool(true) {
debug!("Validation failed for rule {}: {}", idx, rule.message);
validation_errors.push(ErrorInfo::simple_ref(
"VALIDATION_ERROR",
&rule.message,
None,
));
} else {
debug!("Validation passed for rule {}", idx);
}
}
Err(e) => {
error!("Validation: Error evaluating rule {}: {:?}", idx, e);
validation_errors.push(ErrorInfo::simple_ref(
"EVALUATION_ERROR",
&format!("Failed to evaluate rule {}: {}", idx, e),
None,
));
}
}
}
if !validation_errors.is_empty() {
message.errors.extend(validation_errors);
Ok((400, changes)) } else {
Ok((200, changes))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_validation_config_from_json() {
let input = json!({
"rules": [
{
"logic": {"!!": [{"var": "data.required_field"}]},
"path": "data",
"message": "Required field is missing"
},
{
"logic": {">": [{"var": "data.age"}, 18]},
"message": "Must be over 18"
}
]
});
let config = ValidationConfig::from_json(&input).unwrap();
assert_eq!(config.rules.len(), 2);
assert_eq!(config.rules[0].message, "Required field is missing");
assert_eq!(config.rules[1].message, "Must be over 18");
}
#[test]
fn test_validation_config_missing_rules() {
let input = json!({});
let result = ValidationConfig::from_json(&input);
assert!(result.is_err());
}
#[test]
fn test_validation_config_invalid_rules() {
let input = json!({
"rules": "not_an_array"
});
let result = ValidationConfig::from_json(&input);
assert!(result.is_err());
}
#[test]
fn test_validation_config_missing_logic() {
let input = json!({
"rules": [
{
"path": "data",
"message": "Some error"
}
]
});
let result = ValidationConfig::from_json(&input);
assert!(result.is_err());
}
#[test]
fn test_validation_config_defaults() {
let input = json!({
"rules": [
{
"logic": {"var": "data.field"}
}
]
});
let config = ValidationConfig::from_json(&input).unwrap();
assert_eq!(config.rules[0].message, "Validation failed");
}
#[test]
fn test_validation_execute_passes() {
use crate::engine::message::Message;
let datalogic = Arc::new(DataLogic::with_preserve_structure());
let mut message = Message::new(Arc::new(json!({})));
message.context["data"] = json!({
"email": "test@example.com",
"age": 25
});
let mut config = ValidationConfig {
rules: vec![
ValidationRule {
logic: json!({"!!": [{"var": "data.email"}]}),
message: "Email is required".to_string(),
logic_index: None,
},
ValidationRule {
logic: json!({">": [{"var": "data.age"}, 18]}),
message: "Must be over 18".to_string(),
logic_index: None,
},
],
};
let mut logic_cache = Vec::new();
for (i, rule) in config.rules.iter_mut().enumerate() {
logic_cache.push(datalogic.compile(&rule.logic).unwrap());
rule.logic_index = Some(i);
}
let result = config.execute(&mut message, &datalogic, &logic_cache);
assert!(result.is_ok());
let (status, changes) = result.unwrap();
assert_eq!(status, 200);
assert!(changes.is_empty()); assert!(message.errors.is_empty()); }
#[test]
fn test_validation_execute_fails() {
use crate::engine::message::Message;
let datalogic = Arc::new(DataLogic::with_preserve_structure());
let mut message = Message::new(Arc::new(json!({})));
message.context["data"] = json!({
"age": 15 });
let mut config = ValidationConfig {
rules: vec![
ValidationRule {
logic: json!({"!!": [{"var": "data.email"}]}),
message: "Email is required".to_string(),
logic_index: None,
},
ValidationRule {
logic: json!({">": [{"var": "data.age"}, 18]}),
message: "Must be over 18".to_string(),
logic_index: None,
},
],
};
let mut logic_cache = Vec::new();
for (i, rule) in config.rules.iter_mut().enumerate() {
logic_cache.push(datalogic.compile(&rule.logic).unwrap());
rule.logic_index = Some(i);
}
let result = config.execute(&mut message, &datalogic, &logic_cache);
assert!(result.is_ok());
let (status, _changes) = result.unwrap();
assert_eq!(status, 400); assert_eq!(message.errors.len(), 2);
let error_messages: Vec<&str> = message.errors.iter().map(|e| e.message.as_str()).collect();
assert!(error_messages.contains(&"Email is required"));
assert!(error_messages.contains(&"Must be over 18"));
}
#[test]
fn test_validation_uncompiled_logic() {
use crate::engine::message::Message;
let datalogic = Arc::new(DataLogic::with_preserve_structure());
let mut message = Message::new(Arc::new(json!({})));
let config = ValidationConfig {
rules: vec![ValidationRule {
logic: json!(true),
message: "Test".to_string(),
logic_index: None, }],
};
let logic_cache = Vec::new();
let result = config.execute(&mut message, &datalogic, &logic_cache);
assert!(result.is_ok());
let (status, _) = result.unwrap();
assert_eq!(status, 400); assert!(!message.errors.is_empty());
assert!(message.errors[0].code == "COMPILATION_ERROR");
}
}