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| {
166 let Message {
167 context, errors, ..
168 } = message;
169 let ctx_av: DataValue<'_> = context.view_in(arena);
170 self.run_rules(errors, ctx_av, arena, engine)
171 })
172 }
173
174 pub(crate) fn execute_in_arena(
179 &self,
180 message: &mut Message,
181 arena_ctx: &mut ArenaContext<'_>,
182 engine: &Arc<Engine>,
183 ) -> Result<(TaskOutcome, Vec<Change>)> {
184 let arena = arena_ctx.arena();
185 let ctx_av = arena_ctx.as_data_value();
186 self.run_rules(&mut message.errors, ctx_av, arena, engine)
187 }
188
189 fn run_rules(
192 &self,
193 errors: &mut Vec<ErrorInfo>,
194 ctx_av: DataValue<'_>,
195 arena: &bumpalo::Bump,
196 engine: &Arc<Engine>,
197 ) -> Result<(TaskOutcome, Vec<Change>)> {
198 let changes = Vec::new();
199 let mut validation_errors = Vec::new();
200
201 for (idx, rule) in self.rules.iter().enumerate() {
202 debug!("Processing validation rule {idx}");
203
204 let compiled_logic = match &rule.compiled_logic {
205 Some(logic) => logic,
206 None => {
207 error!("Validation: Logic not compiled for rule at index {}", idx);
208 validation_errors.push(ErrorInfo::simple_ref(
209 "COMPILATION_ERROR",
210 &format!("Logic not compiled for rule at index: {}", idx),
211 None,
212 ));
213 continue;
214 }
215 };
216
217 match engine.evaluate(compiled_logic, ctx_av, arena) {
222 Ok(value) => {
223 if !matches!(value, DataValue::Bool(true)) {
224 let text = rule
230 .message
231 .resolve_string_in_arena(ParamCtx::new(engine, ctx_av, arena))
232 .unwrap_or_else(|e| {
233 error!("Validation: rule {idx} message failed to render: {e:?}");
234 "Validation failed".to_string()
235 });
236 debug!("Validation failed for rule {idx}: {text}");
237 validation_errors.push(ErrorInfo::simple_ref(
238 "VALIDATION_ERROR",
239 &text,
240 None,
241 ));
242 } else {
243 debug!("Validation passed for rule {}", idx);
244 }
245 }
246 Err(e) => {
247 error!("Validation: Error evaluating rule {}: {:?}", idx, e);
248 validation_errors.push(ErrorInfo::simple_ref(
249 "EVALUATION_ERROR",
250 &format!("Failed to evaluate rule {}: {}", idx, e),
251 None,
252 ));
253 }
254 }
255 }
256
257 if !validation_errors.is_empty() {
258 errors.extend(validation_errors);
259 Ok((TaskOutcome::Status(400), changes))
260 } else {
261 Ok((TaskOutcome::Success, changes))
262 }
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use datavalue::OwnedDataValue;
270 use serde_json::json;
271
272 #[test]
273 fn test_validation_config_from_json() {
274 let input = json!({
275 "rules": [
276 {
277 "logic": {"!!": [{"var": "data.required_field"}]},
278 "path": "data",
279 "message": "Required field is missing"
280 },
281 {
282 "logic": {">": [{"var": "data.age"}, 18]},
283 "message": "Must be over 18"
284 }
285 ]
286 });
287
288 let config = ValidationConfig::from_json(&input).unwrap();
289 assert_eq!(config.rules.len(), 2);
290 assert_eq!(
291 config.rules[0].message.as_json(),
292 &json!("Required field is missing")
293 );
294 assert_eq!(config.rules[1].message.as_json(), &json!("Must be over 18"));
295 }
296
297 #[test]
298 fn test_validation_config_missing_rules() {
299 let input = json!({});
300 let result = ValidationConfig::from_json(&input);
301 assert!(result.is_err());
302 }
303
304 #[test]
305 fn test_validation_config_invalid_rules() {
306 let input = json!({
307 "rules": "not_an_array"
308 });
309 let result = ValidationConfig::from_json(&input);
310 assert!(result.is_err());
311 }
312
313 #[test]
314 fn test_validation_config_missing_logic() {
315 let input = json!({
316 "rules": [
317 {
318 "path": "data",
319 "message": "Some error"
320 }
321 ]
322 });
323 let result = ValidationConfig::from_json(&input);
324 assert!(result.is_err());
325 }
326
327 #[test]
328 fn test_validation_config_defaults() {
329 let input = json!({
330 "rules": [
331 {
332 "logic": {"var": "data.field"}
333 }
334 ]
335 });
336
337 let config = ValidationConfig::from_json(&input).unwrap();
338 assert_eq!(
339 config.rules[0].message.as_json(),
340 &json!("Validation failed")
341 );
342 }
343
344 fn dv(v: serde_json::Value) -> OwnedDataValue {
345 OwnedDataValue::from(&v)
346 }
347
348 fn message_with_data(initial: serde_json::Value) -> crate::engine::message::Message {
349 use crate::engine::message::Message;
350 Message::builder().data(dv(initial)).build()
351 }
352
353 fn compile_rules(engine: &Arc<Engine>, config: &mut ValidationConfig) {
356 for rule in &mut config.rules {
357 rule.compiled_logic = Some(engine.compile_arc(&rule.logic).unwrap());
358 }
359 }
360
361 #[test]
362 fn test_validation_execute_passes() {
363 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
364
365 let mut message = message_with_data(json!({
366 "email": "test@example.com",
367 "age": 25
368 }));
369
370 let mut config = ValidationConfig {
371 rules: vec![
372 ValidationRule {
373 logic: json!({"!!": [{"var": "data.email"}]}),
374 message: Template::from(json!("Email is required")),
375 compiled_logic: None,
376 },
377 ValidationRule {
378 logic: json!({">": [{"var": "data.age"}, 18]}),
379 message: Template::from(json!("Must be over 18")),
380 compiled_logic: None,
381 },
382 ],
383 };
384 compile_rules(&engine, &mut config);
385
386 let result = config.execute(&mut message, &engine);
387 assert!(result.is_ok());
388
389 let (outcome, changes) = result.unwrap();
390 assert_eq!(outcome, TaskOutcome::Success);
391 assert!(changes.is_empty());
392 assert!(message.errors.is_empty());
393 }
394
395 #[test]
396 fn test_validation_execute_fails() {
397 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
398
399 let mut message = message_with_data(json!({ "age": 15 }));
400
401 let mut config = ValidationConfig {
402 rules: vec![
403 ValidationRule {
404 logic: json!({"!!": [{"var": "data.email"}]}),
405 message: Template::from(json!("Email is required")),
406 compiled_logic: None,
407 },
408 ValidationRule {
409 logic: json!({">": [{"var": "data.age"}, 18]}),
410 message: Template::from(json!("Must be over 18")),
411 compiled_logic: None,
412 },
413 ],
414 };
415 compile_rules(&engine, &mut config);
416
417 let result = config.execute(&mut message, &engine);
418 assert!(result.is_ok());
419
420 let (outcome, _changes) = result.unwrap();
421 assert_eq!(outcome, TaskOutcome::Status(400));
422 assert_eq!(message.errors.len(), 2);
423
424 let error_messages: Vec<&str> = message.errors.iter().map(|e| e.message.as_str()).collect();
425 assert!(error_messages.contains(&"Email is required"));
426 assert!(error_messages.contains(&"Must be over 18"));
427 }
428
429 #[test]
430 fn test_validation_uncompiled_logic() {
431 use crate::engine::message::Message;
432
433 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
434
435 let mut message = Message::new(Arc::new(dv(json!({}))));
436
437 let config = ValidationConfig {
438 rules: vec![ValidationRule {
439 logic: json!(true),
440 message: Template::from(json!("Test")),
441 compiled_logic: None,
442 }],
443 };
444
445 let result = config.execute(&mut message, &engine);
446 assert!(result.is_ok());
447
448 let (outcome, _) = result.unwrap();
449 assert_eq!(outcome, TaskOutcome::Status(400));
450 assert!(!message.errors.is_empty());
451 assert!(message.errors[0].code == "COMPILATION_ERROR");
452 }
453}