///|
pub(all) struct ConditionNote {
path : String
passed : Bool
reason : String
}
///|
pub struct Evaluation {
result : Bool
notes : Array[ConditionNote]
}
///|
pub fn Evaluation::matched(self : Evaluation) -> Bool {
self.result
}
///|
pub fn Evaluation::notes(self : Evaluation) -> Array[ConditionNote] {
self.notes.copy()
}
///|
fn validate_evaluation(
condition : Condition,
captures : Array[Capture],
) -> Unit raise CepError {
validate_condition_structure(condition)
if captures.length() > 32 {
raise Resource(resource="evaluation captures", limit=32)
}
let seen : Map[String, Bool] = Map([])
for capture in captures {
require_label(capture.name, "evaluation.capture")
if capture.events.length() > 128 {
raise Resource(resource="capture events", limit=128)
}
if seen.contains(capture.name) {
raise Invalid(path="evaluation.capture", reason="duplicate capture name")
}
seen.set(capture.name, true)
}
}
///|
/// Validate untrusted mutable condition arrays before standalone evaluation.
/// Recognition uses a private fast path over the Pattern's sealed owned AST.
pub fn Condition::evaluate(
self : Condition,
event : Event,
captures : Array[Capture],
) -> Bool raise CepError {
validate_evaluation(self, captures)
self.evaluate_unchecked(event, captures)
}
///|
fn comparison_reason(left : Value?, right : Value?, passed : Bool) -> String {
match (left, right) {
(None, _) | (_, None) => "missing-field"
(Some(a), Some(b)) =>
if a.type_name() != b.type_name() {
"incompatible-types"
} else if passed {
"accepted"
} else {
"comparison-false"
}
}
}
///|
fn condition_reason(
condition : Condition,
event : Event,
captures : Array[Capture],
passed : Bool,
) -> String {
if passed {
return "accepted"
}
match condition {
Always => "accepted"
Kind(_) => "kind-mismatch"
Has(_) => "missing-field"
Compare(field, _, value) =>
comparison_reason(event.field(field), Some(value), passed)
FieldCompare(a, _, b) =>
comparison_reason(event.field(a), event.field(b), passed)
CaptureCompare(field, _, name, other) =>
match find_condition_capture(captures, name) {
None => "missing-capture"
Some(capture) =>
if capture.events.is_empty() {
"empty-capture"
} else {
comparison_reason(
event.field(field),
capture.events[capture.events.length() - 1].field(other),
passed,
)
}
}
CaptureCount(name, _, _) =>
if find_condition_capture(captures, name) is None {
"missing-capture"
} else {
"count-comparison-false"
}
TextPrefix(field, _) | TextContains(field, _) =>
match event.field(field) {
None => "missing-field"
Some(Text(_)) => "text-comparison-false"
_ => "incompatible-types"
}
All(_) => "conjunction-false"
Any(_) => "disjunction-false"
Not(_) => "negation-false"
}
}
///|
fn explain_condition(
condition : Condition,
event : Event,
captures : Array[Capture],
path : String,
notes : Array[ConditionNote],
) -> Bool {
let passed = match condition {
All(children) => {
let mut passed = true
for index, child in children {
// Diagnostic mode deliberately visits every child, including siblings
// that the Boolean fast path would short-circuit.
let accepted = explain_condition(
child,
event,
captures,
"\{path}.all[\{index}]",
notes,
)
passed = passed && accepted
}
passed
}
Any(children) => {
let mut passed = false
for index, child in children {
let accepted = explain_condition(
child,
event,
captures,
"\{path}.any[\{index}]",
notes,
)
passed = passed || accepted
}
passed
}
Not(child) =>
!explain_condition(child, event, captures, "\{path}.not", notes)
_ => condition.evaluate_unchecked(event, captures)
}
notes.push({
path,
passed,
reason: condition_reason(condition, event, captures, passed),
})
passed
}
///|
/// Return value-free diagnostic reasons. Field names and raw values are not
/// copied into the notes; paths refer to condition AST positions.
pub fn Condition::explain(
self : Condition,
event : Event,
captures~ : Array[Capture] = [],
) -> Evaluation raise CepError {
validate_evaluation(self, captures)
let notes = []
let result = explain_condition(self, event, captures, "condition", notes)
{ result, notes, }
}
///|
pub fn evaluation_to_json(evaluation : Evaluation) -> Json {
{
"matched": Json::boolean(evaluation.result),
"notes": Json::array(
evaluation.notes.map(note => {
"path": Json::string(note.path),
"passed": Json::boolean(note.passed),
"reason": Json::string(note.reason),
}),
),
}
}