use crate::base::{EVALUATION_ERROR, ErrorKind, Result, VALIDATION_ERROR};
use crate::expr::Expr;
use crate::function::{FunctionConfig, Outcome, SyncCtx};
use ::log::{debug, error};
#[derive(Debug, FunctionConfig)]
pub(crate) struct ValidationConfig {
pub rules: Vec<Rule>,
}
#[derive(Debug, FunctionConfig)]
pub(crate) struct Rule {
pub logic: Expr,
#[config(records)]
pub message: Expr<String>,
}
impl ValidationConfig {
pub(crate) fn call<'p>(&'p self, cx: &mut SyncCtx<'_, '_, 'p>) -> Result<Outcome> {
let mut passed = true;
for (idx, rule) in self.rules.iter().enumerate() {
debug!("Processing validation rule {idx}");
let (code, text) = match cx.test(&rule.logic) {
Ok(true) => {
debug!("Validation passed for rule {idx}");
continue;
}
Ok(false) => (
VALIDATION_ERROR,
match cx.eval(&rule.message) {
Ok(text) => text.into_owned(),
Err(e) if e.kind() == &ErrorKind::Budget => return Err(e),
Err(e) => {
error!("Validation: rule {idx} message failed to render: {e:?}");
"Validation failed".to_owned()
}
},
),
Err(e) if e.kind() == &ErrorKind::Budget => return Err(e),
Err(e) => {
error!("Validation: Error evaluating rule {idx}: {e:?}");
(
EVALUATION_ERROR,
format!("Failed to evaluate rule {idx}: {e}"),
)
}
};
passed = false;
let record = cx.record(code, text);
cx.errors.push(record);
}
Ok(if passed {
Outcome::Ok
} else {
Outcome::Status(400)
})
}
}
#[cfg(test)]
mod tests {
use crate::function::builtins::one_task::{Golden, Observed};
use serde_json::{Value, json};
fn context() -> Value {
json!({"data": {"a": 5, "ok": true, "name": "x", "zero": 0, "list": [1]}})
}
fn rules(golden: &mut Golden, rules: Value) -> Observed {
golden.observe(
"validation",
&json!({"rules": rules}),
&Value::Null,
&context(),
)
}
#[test]
fn every_rule_passing_is_ok_and_records_nothing() {
let mut golden = Golden::new("validation", "every_rule_passing_is_ok_and_records_nothing");
let observed = rules(
&mut golden,
json!([
{"logic": true, "message": "m0"},
{"logic": {"var": "data.ok"}, "message": "m1"},
{"logic": {"==": [{"var": "data.a"}, 5]}, "message": "m2"},
]),
);
assert_eq!(observed.status, Some(200));
assert_eq!(observed.errors, "[]");
}
#[test]
fn a_rule_that_is_not_a_literal_true_fails_with_its_message() {
let mut golden = Golden::new(
"validation",
"a_rule_that_is_not_a_literal_true_fails_with_its_message",
);
for logic in [
json!(false),
json!(1),
json!("yes"),
json!([1]),
json!({"var": "data.a"}),
json!({"var": "data.list"}),
json!({"var": "data.missing"}),
] {
let observed = rules(&mut golden, json!([{"logic": logic, "message": "nope"}]));
assert_eq!(observed.status, Some(400), "{logic}");
assert!(observed.errors.contains("VALIDATION_ERROR"), "{logic}");
assert!(observed.errors.contains("nope"), "{logic}");
}
}
#[test]
fn an_evaluation_error_is_evaluation_error_and_the_rest_still_run() {
let mut golden = Golden::new(
"validation",
"an_evaluation_error_is_evaluation_error_and_the_rest_still_run",
);
let observed = rules(
&mut golden,
json!([
{"logic": true, "message": "m0"},
{"logic": false, "message": "m1"},
{"logic": {"/": [1, {"var": "data.zero"}]}, "message": "m2"},
{"logic": false, "message": "m3"},
]),
);
assert_eq!(observed.status, Some(400));
assert!(observed.errors.contains("Failed to evaluate rule 2: "));
assert!(observed.errors.contains("EVALUATION_ERROR"));
assert!(observed.errors.contains("m3"));
}
#[test]
fn a_message_is_rendered_only_when_its_rule_fails() {
let mut golden = Golden::new(
"validation",
"a_message_is_rendered_only_when_its_rule_fails",
);
let broken = json!({"/": [1, {"var": "data.zero"}]});
let observed = rules(&mut golden, json!([{"logic": true, "message": broken}]));
assert_eq!(observed.status, Some(200));
assert_eq!(observed.errors, "[]");
let observed = rules(&mut golden, json!([{"logic": false, "message": broken}]));
assert!(observed.errors.contains("Validation failed"));
let observed = rules(
&mut golden,
json!([{
"logic": false,
"message": {"cat": ["a was ", {"var": "data.a"}, " and name ", {"var": "data.name"}]}
}]),
);
assert!(observed.errors.contains("a was 5 and name x"));
rules(
&mut golden,
json!([{"logic": false, "message": {"var": "data.list"}}]),
);
rules(&mut golden, json!([{"logic": false, "message": 7}]));
}
#[test]
fn no_rules_is_ok() {
let mut golden = Golden::new("validation", "no_rules_is_ok");
assert_eq!(rules(&mut golden, json!([])).status, Some(200));
}
}