///|
pub(all) enum EventPredicate {
TopicMatches(String)
HeaderEquals(String, String)
HeaderExists(String)
PayloadExists(String)
TextEquals(String, String)
TextContains(String, String)
IntAtLeast(String, Int)
IntAtMost(String, Int)
IntBetween(String, Int, Int)
BoolEquals(String, Bool)
AllOf(Array[EventPredicate])
AnyOf(Array[EventPredicate])
Not(EventPredicate)
} derive(Eq, @debug.Debug)
///|
pub(all) struct RuleResult {
passed : Bool
code : String
reason : String
} derive(Eq, @debug.Debug)
///|
pub(all) enum RuleDecision {
Accept(String)
Reject(String)
} derive(Eq, @debug.Debug)
///|
pub(all) struct RuleSet {
name : String
predicates : Array[EventPredicate]
} derive(Eq, @debug.Debug)
///|
pub fn rule_set(
name : StringView,
predicates : ArrayView[EventPredicate],
) -> RuleSet {
{ name: name.to_owned(), predicates: predicates.to_owned() }
}
///|
pub fn RuleResult::ok(code : StringView, reason : StringView) -> RuleResult {
{ passed: true, code: code.to_owned(), reason: reason.to_owned() }
}
///|
pub fn RuleResult::fail(code : StringView, reason : StringView) -> RuleResult {
{ passed: false, code: code.to_owned(), reason: reason.to_owned() }
}
///|
pub fn EventPredicate::evaluate(
self : EventPredicate,
event : Envelope,
) -> RuleResult {
match self {
TopicMatches(pattern) => evaluate_topic_match(pattern, event)
HeaderEquals(key, expected) => evaluate_header_equals(event, key, expected)
HeaderExists(key) => evaluate_header_exists(event, key)
PayloadExists(path) => evaluate_payload_exists(event, path)
TextEquals(path, expected) => evaluate_text_equals(event, path, expected)
TextContains(path, needle) => evaluate_text_contains(event, path, needle)
IntAtLeast(path, minimum) => evaluate_int_at_least(event, path, minimum)
IntAtMost(path, maximum) => evaluate_int_at_most(event, path, maximum)
IntBetween(path, minimum, maximum) =>
evaluate_int_between(event, path, minimum, maximum)
BoolEquals(path, expected) => evaluate_bool_equals(event, path, expected)
AllOf(predicates) => evaluate_all(predicates, event)
AnyOf(predicates) => evaluate_any(predicates, event)
Not(predicate) => {
let result = predicate.evaluate(event)
if result.passed {
RuleResult::fail("not", "negated predicate passed: \{result.reason}")
} else {
RuleResult::ok("not", "negated predicate failed as expected")
}
}
}
}
///|
pub fn RuleSet::evaluate(self : RuleSet, event : Envelope) -> RuleResult {
evaluate_all(self.predicates, event)
}
///|
pub fn RuleSet::decide(self : RuleSet, event : Envelope) -> RuleDecision {
let result = self.evaluate(event)
if result.passed {
Accept("\{self.name}: \{result.reason}")
} else {
Reject("\{self.name}: \{result.reason}")
}
}
///|
pub fn RuleDecision::to_handler_result(self : RuleDecision) -> HandlerResult {
match self {
Accept(message) => HandlerAck(message)
Reject(reason) => HandlerDrop(reason)
}
}
///|
fn evaluate_topic_match(pattern : String, event : Envelope) -> RuleResult {
match topic_pattern(pattern) {
Err(err) => RuleResult::fail("topic.invalid", err.message())
Ok(parsed) =>
match parsed.matches_topic(event.topic) {
Err(err) => RuleResult::fail("topic.invalid", err.message())
Ok(true) => RuleResult::ok("topic.match", "topic matched \{pattern}")
Ok(false) =>
RuleResult::fail(
"topic.mismatch",
"topic \{event.topic} did not match \{pattern}",
)
}
}
}
///|
fn evaluate_header_exists(event : Envelope, key : String) -> RuleResult {
match event.header(key) {
Some(_) => RuleResult::ok("header.exists", "header \{key} exists")
None => RuleResult::fail("header.missing", "header \{key} is missing")
}
}
///|
fn evaluate_header_equals(
event : Envelope,
key : String,
expected : String,
) -> RuleResult {
match event.header(key) {
Some(actual) if actual == expected =>
RuleResult::ok("header.equals", "header \{key} matched")
Some(actual) =>
RuleResult::fail(
"header.mismatch",
"header \{key} expected \{expected} but got \{actual}",
)
None => RuleResult::fail("header.missing", "header \{key} is missing")
}
}
///|
fn evaluate_payload_exists(event : Envelope, path : String) -> RuleResult {
match event.payload_path(path) {
Some(_) => RuleResult::ok("payload.exists", "payload path \{path} exists")
None =>
RuleResult::fail("payload.missing", "payload path \{path} is missing")
}
}
///|
fn evaluate_text_equals(
event : Envelope,
path : String,
expected : String,
) -> RuleResult {
match event.payload_path(path) {
Some(VText(actual)) if actual == expected =>
RuleResult::ok("text.equals", "payload text \{path} matched")
Some(VText(actual)) =>
RuleResult::fail(
"text.mismatch",
"payload text \{path} expected \{expected} but got \{actual}",
)
Some(_) => RuleResult::fail("text.type", "payload path \{path} is not text")
None =>
RuleResult::fail("payload.missing", "payload path \{path} is missing")
}
}
///|
fn evaluate_text_contains(
event : Envelope,
path : String,
needle : String,
) -> RuleResult {
match event.payload_path(path) {
Some(VText(actual)) if actual.contains(needle) =>
RuleResult::ok("text.contains", "payload text \{path} contains \{needle}")
Some(VText(_)) =>
RuleResult::fail(
"text.not_contains",
"payload text \{path} does not contain \{needle}",
)
Some(_) => RuleResult::fail("text.type", "payload path \{path} is not text")
None =>
RuleResult::fail("payload.missing", "payload path \{path} is missing")
}
}
///|
fn evaluate_int_at_least(
event : Envelope,
path : String,
minimum : Int,
) -> RuleResult {
match event.payload_path(path) {
Some(VInt(actual)) if actual >= minimum =>
RuleResult::ok("int.min", "payload int \{path} >= \{minimum}")
Some(VInt(actual)) =>
RuleResult::fail(
"int.too_small",
"payload int \{path} expected >= \{minimum} but got \{actual}",
)
Some(_) => RuleResult::fail("int.type", "payload path \{path} is not int")
None =>
RuleResult::fail("payload.missing", "payload path \{path} is missing")
}
}
///|
fn evaluate_int_at_most(
event : Envelope,
path : String,
maximum : Int,
) -> RuleResult {
match event.payload_path(path) {
Some(VInt(actual)) if actual <= maximum =>
RuleResult::ok("int.max", "payload int \{path} <= \{maximum}")
Some(VInt(actual)) =>
RuleResult::fail(
"int.too_large",
"payload int \{path} expected <= \{maximum} but got \{actual}",
)
Some(_) => RuleResult::fail("int.type", "payload path \{path} is not int")
None =>
RuleResult::fail("payload.missing", "payload path \{path} is missing")
}
}
///|
fn evaluate_int_between(
event : Envelope,
path : String,
minimum : Int,
maximum : Int,
) -> RuleResult {
if minimum > maximum {
return RuleResult::fail(
"int.range.invalid",
"minimum \{minimum} is greater than maximum \{maximum}",
)
}
let low = evaluate_int_at_least(event, path, minimum)
if !low.passed {
return low
}
evaluate_int_at_most(event, path, maximum)
}
///|
fn evaluate_bool_equals(
event : Envelope,
path : String,
expected : Bool,
) -> RuleResult {
match event.payload_path(path) {
Some(VBool(actual)) if actual == expected =>
RuleResult::ok("bool.equals", "payload bool \{path} matched")
Some(VBool(actual)) =>
RuleResult::fail(
"bool.mismatch",
"payload bool \{path} expected \{expected} but got \{actual}",
)
Some(_) => RuleResult::fail("bool.type", "payload path \{path} is not bool")
None =>
RuleResult::fail("payload.missing", "payload path \{path} is missing")
}
}
///|
fn evaluate_all(
predicates : Array[EventPredicate],
event : Envelope,
) -> RuleResult {
for predicate in predicates {
let result = predicate.evaluate(event)
if !result.passed {
return result
}
}
RuleResult::ok("all", "all predicates passed")
}
///|
fn evaluate_any(
predicates : Array[EventPredicate],
event : Envelope,
) -> RuleResult {
guard predicates.length() > 0 else {
return RuleResult::fail("any.empty", "no predicates provided")
}
let reasons : Array[String] = []
for predicate in predicates {
let result = predicate.evaluate(event)
if result.passed {
return result
}
reasons.push(result.reason)
}
RuleResult::fail("any.none", reasons.join("; "))
}