use crate::base::{EVALUATION_ERROR, ErrorKind, ErrorSite, Result, SiteKind, VALIDATION_ERROR};
use crate::expr::Expr;
use crate::function::{FunctionConfig, Outcome, SyncCtx};
use ::log::{debug, error};
use std::sync::Arc;
#[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 rule_site = |cx: &SyncCtx<'_, '_, 'p>, kind: SiteKind, leaf: &'static str| {
let path = cx
.task_path
.join("function")
.join("input")
.join("rules")
.join(idx)
.join(leaf);
ErrorSite {
rule_index: Some(idx),
..cx.site(kind, path)
}
};
let (code, text, cause, site) = match cx.test(&rule.logic) {
Ok(true) => {
debug!("Validation passed for rule {idx}");
continue;
}
Ok(false) => {
let (msg, cause, site) = match cx.eval(&rule.message) {
Ok(text) => (
text.into_owned(),
None,
rule_site(cx, SiteKind::ValidationRule, "logic"),
),
Err(e) if e.kind() == &ErrorKind::Budget => {
let site = rule_site(cx, SiteKind::ValidationMessage, "message");
return Err(e.with_site(site));
}
Err(e) => {
error!("Validation: rule {idx} message failed to render: {e:?}");
(
"Validation failed".to_owned(),
e.expr_cause_arc(),
rule_site(cx, SiteKind::ValidationMessage, "message"),
)
}
};
(VALIDATION_ERROR, msg, cause, site)
}
Err(e) if e.kind() == &ErrorKind::Budget => {
let site = rule_site(cx, SiteKind::ValidationRule, "logic");
return Err(e.with_site(site));
}
Err(e) => {
error!("Validation: Error evaluating rule {idx}: {e:?}");
let cause = e.expr_cause_arc();
let site = rule_site(cx, SiteKind::ValidationRule, "logic");
(
EVALUATION_ERROR,
format!("Failed to evaluate rule {idx}: {e}"),
cause,
site,
)
}
};
passed = false;
let mut record = cx.record(code, text);
record.cause = cause;
record.site = Some(Arc::new(site));
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));
}
}