dataflow_rs/engine/functions/
validation.rs1use crate::engine::error::{DataflowError, ErrorInfo, Result};
35use crate::engine::executor::{ArenaContext, with_arena};
36use crate::engine::functions::path_template::ParamCtx;
37use crate::engine::functions::template::Template;
38use crate::engine::message::{Change, Message};
39use crate::engine::task_outcome::TaskOutcome;
40use datalogic_rs::{Engine, Logic};
41use datavalue::DataValue;
42use log::{debug, error};
43use serde::Deserialize;
44use serde_json::Value;
45use std::sync::Arc;
46
47#[derive(Debug, Clone, Deserialize)]
53pub struct ValidationConfig {
54 pub rules: Vec<ValidationRule>,
56}
57
58#[derive(Debug, Clone, Deserialize)]
63pub struct ValidationRule {
64 pub logic: Value,
67
68 pub message: Template,
86
87 #[serde(skip)]
90 pub compiled_logic: Option<Arc<Logic>>,
91}
92
93impl ValidationConfig {
94 pub fn from_json(input: &Value) -> Result<Self> {
105 let rules = input.get("rules").ok_or_else(|| {
106 DataflowError::Validation("Missing 'rules' array in input".to_string())
107 })?;
108
109 let rules_arr = rules
110 .as_array()
111 .ok_or_else(|| DataflowError::Validation("'rules' must be an array".to_string()))?;
112
113 let mut parsed_rules = Vec::new();
114
115 for rule in rules_arr {
116 let logic = rule
117 .get("logic")
118 .ok_or_else(|| DataflowError::Validation("Missing 'logic' in rule".to_string()))?
119 .clone();
120
121 let message = Template::from(
122 rule.get("message")
123 .cloned()
124 .unwrap_or_else(|| Value::from("Validation failed")),
125 );
126
127 parsed_rules.push(ValidationRule {
128 logic,
129 message,
130 compiled_logic: None,
131 });
132 }
133
134 Ok(Self {
135 rules: parsed_rules,
136 })
137 }
138
139 pub fn execute(
153 &self,
154 message: &mut Message,
155 engine: &Arc<Engine>,
156 ) -> Result<(TaskOutcome, Vec<Change>)> {
157 with_arena(|arena| {
162 let ctx_av: DataValue<'_> = message.context.to_arena(arena);
163 self.run_rules(message, ctx_av, arena, engine)
164 })
165 }
166
167 pub(crate) fn execute_in_arena(
172 &self,
173 message: &mut Message,
174 arena_ctx: &mut ArenaContext<'_>,
175 engine: &Arc<Engine>,
176 ) -> Result<(TaskOutcome, Vec<Change>)> {
177 let arena = arena_ctx.arena();
178 let ctx_av = arena_ctx.as_data_value();
179 self.run_rules(message, ctx_av, arena, engine)
180 }
181
182 fn run_rules(
185 &self,
186 message: &mut Message,
187 ctx_av: DataValue<'_>,
188 arena: &bumpalo::Bump,
189 engine: &Arc<Engine>,
190 ) -> Result<(TaskOutcome, Vec<Change>)> {
191 let changes = Vec::new();
192 let mut validation_errors = Vec::new();
193
194 for (idx, rule) in self.rules.iter().enumerate() {
195 debug!("Processing validation rule {idx}");
196
197 let compiled_logic = match &rule.compiled_logic {
198 Some(logic) => logic,
199 None => {
200 error!("Validation: Logic not compiled for rule at index {}", idx);
201 validation_errors.push(ErrorInfo::simple_ref(
202 "COMPILATION_ERROR",
203 &format!("Logic not compiled for rule at index: {}", idx),
204 None,
205 ));
206 continue;
207 }
208 };
209
210 match engine.evaluate(compiled_logic, ctx_av, arena) {
215 Ok(value) => {
216 if !matches!(value, DataValue::Bool(true)) {
217 let text = rule
223 .message
224 .resolve_string_in_arena(ParamCtx::new(engine, ctx_av, arena))
225 .unwrap_or_else(|e| {
226 error!("Validation: rule {idx} message failed to render: {e:?}");
227 "Validation failed".to_string()
228 });
229 debug!("Validation failed for rule {idx}: {text}");
230 validation_errors.push(ErrorInfo::simple_ref(
231 "VALIDATION_ERROR",
232 &text,
233 None,
234 ));
235 } else {
236 debug!("Validation passed for rule {}", idx);
237 }
238 }
239 Err(e) => {
240 error!("Validation: Error evaluating rule {}: {:?}", idx, e);
241 validation_errors.push(ErrorInfo::simple_ref(
242 "EVALUATION_ERROR",
243 &format!("Failed to evaluate rule {}: {}", idx, e),
244 None,
245 ));
246 }
247 }
248 }
249
250 if !validation_errors.is_empty() {
251 message.errors.extend(validation_errors);
252 Ok((TaskOutcome::Status(400), changes))
253 } else {
254 Ok((TaskOutcome::Success, changes))
255 }
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use datavalue::OwnedDataValue;
263 use serde_json::json;
264
265 #[test]
266 fn test_validation_config_from_json() {
267 let input = json!({
268 "rules": [
269 {
270 "logic": {"!!": [{"var": "data.required_field"}]},
271 "path": "data",
272 "message": "Required field is missing"
273 },
274 {
275 "logic": {">": [{"var": "data.age"}, 18]},
276 "message": "Must be over 18"
277 }
278 ]
279 });
280
281 let config = ValidationConfig::from_json(&input).unwrap();
282 assert_eq!(config.rules.len(), 2);
283 assert_eq!(
284 config.rules[0].message.as_json(),
285 &json!("Required field is missing")
286 );
287 assert_eq!(config.rules[1].message.as_json(), &json!("Must be over 18"));
288 }
289
290 #[test]
291 fn test_validation_config_missing_rules() {
292 let input = json!({});
293 let result = ValidationConfig::from_json(&input);
294 assert!(result.is_err());
295 }
296
297 #[test]
298 fn test_validation_config_invalid_rules() {
299 let input = json!({
300 "rules": "not_an_array"
301 });
302 let result = ValidationConfig::from_json(&input);
303 assert!(result.is_err());
304 }
305
306 #[test]
307 fn test_validation_config_missing_logic() {
308 let input = json!({
309 "rules": [
310 {
311 "path": "data",
312 "message": "Some error"
313 }
314 ]
315 });
316 let result = ValidationConfig::from_json(&input);
317 assert!(result.is_err());
318 }
319
320 #[test]
321 fn test_validation_config_defaults() {
322 let input = json!({
323 "rules": [
324 {
325 "logic": {"var": "data.field"}
326 }
327 ]
328 });
329
330 let config = ValidationConfig::from_json(&input).unwrap();
331 assert_eq!(
332 config.rules[0].message.as_json(),
333 &json!("Validation failed")
334 );
335 }
336
337 fn dv(v: serde_json::Value) -> OwnedDataValue {
338 OwnedDataValue::from(&v)
339 }
340
341 fn message_with_data(initial: serde_json::Value) -> crate::engine::message::Message {
342 use crate::engine::message::Message;
343 Message::builder().data(dv(initial)).build()
344 }
345
346 fn compile_rules(engine: &Arc<Engine>, config: &mut ValidationConfig) {
349 for rule in &mut config.rules {
350 rule.compiled_logic = Some(engine.compile_arc(&rule.logic).unwrap());
351 }
352 }
353
354 #[test]
355 fn test_validation_execute_passes() {
356 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
357
358 let mut message = message_with_data(json!({
359 "email": "test@example.com",
360 "age": 25
361 }));
362
363 let mut config = ValidationConfig {
364 rules: vec![
365 ValidationRule {
366 logic: json!({"!!": [{"var": "data.email"}]}),
367 message: Template::from(json!("Email is required")),
368 compiled_logic: None,
369 },
370 ValidationRule {
371 logic: json!({">": [{"var": "data.age"}, 18]}),
372 message: Template::from(json!("Must be over 18")),
373 compiled_logic: None,
374 },
375 ],
376 };
377 compile_rules(&engine, &mut config);
378
379 let result = config.execute(&mut message, &engine);
380 assert!(result.is_ok());
381
382 let (outcome, changes) = result.unwrap();
383 assert_eq!(outcome, TaskOutcome::Success);
384 assert!(changes.is_empty());
385 assert!(message.errors.is_empty());
386 }
387
388 #[test]
389 fn test_validation_execute_fails() {
390 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
391
392 let mut message = message_with_data(json!({ "age": 15 }));
393
394 let mut config = ValidationConfig {
395 rules: vec![
396 ValidationRule {
397 logic: json!({"!!": [{"var": "data.email"}]}),
398 message: Template::from(json!("Email is required")),
399 compiled_logic: None,
400 },
401 ValidationRule {
402 logic: json!({">": [{"var": "data.age"}, 18]}),
403 message: Template::from(json!("Must be over 18")),
404 compiled_logic: None,
405 },
406 ],
407 };
408 compile_rules(&engine, &mut config);
409
410 let result = config.execute(&mut message, &engine);
411 assert!(result.is_ok());
412
413 let (outcome, _changes) = result.unwrap();
414 assert_eq!(outcome, TaskOutcome::Status(400));
415 assert_eq!(message.errors.len(), 2);
416
417 let error_messages: Vec<&str> = message.errors.iter().map(|e| e.message.as_str()).collect();
418 assert!(error_messages.contains(&"Email is required"));
419 assert!(error_messages.contains(&"Must be over 18"));
420 }
421
422 #[test]
423 fn test_validation_uncompiled_logic() {
424 use crate::engine::message::Message;
425
426 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
427
428 let mut message = Message::new(Arc::new(dv(json!({}))));
429
430 let config = ValidationConfig {
431 rules: vec![ValidationRule {
432 logic: json!(true),
433 message: Template::from(json!("Test")),
434 compiled_logic: None,
435 }],
436 };
437
438 let result = config.execute(&mut message, &engine);
439 assert!(result.is_ok());
440
441 let (outcome, _) = result.unwrap();
442 assert_eq!(outcome, TaskOutcome::Status(400));
443 assert!(!message.errors.is_empty());
444 assert!(message.errors[0].code == "COMPILATION_ERROR");
445 }
446}