use crate::types::{Guard, Operator, Properties, PropertyValue, Rule};
use regex::Regex;
use std::cmp::Ordering;
pub fn evaluate_guard(guard: &Guard, properties: &Properties) -> bool {
for rule in &guard.rules {
if !rule.enabled {
continue;
}
if matches_rule(rule, properties) {
return rule.result;
}
}
guard.default_value
}
fn matches_rule(rule: &Rule, props: &Properties) -> bool {
let raw = match props.get(&rule.property_name) {
Some(v) => v,
None => return false,
};
if rule.values.is_empty() {
return false;
}
let first = rule.values.first();
match rule.operator {
Operator::Equals => values_equal(raw, first),
Operator::NotEquals => !values_equal(raw, first),
Operator::In => rule.values.iter().any(|v| values_equal(raw, Some(v))),
Operator::NotIn => rule.values.iter().all(|v| !values_equal(raw, Some(v))),
Operator::Regex => {
let pattern = match first {
Some(PropertyValue::Text(s)) => s.as_str(),
_ => return false,
};
Regex::new(pattern)
.map(|re| re.is_match(&value_to_string(raw)))
.unwrap_or(false)
}
Operator::Gt => compare_ordered_values(raw, first) == Some(Ordering::Greater),
Operator::Gte => matches!(
compare_ordered_values(raw, first),
Some(Ordering::Greater) | Some(Ordering::Equal)
),
Operator::Lt => compare_ordered_values(raw, first) == Some(Ordering::Less),
Operator::Lte => matches!(
compare_ordered_values(raw, first),
Some(Ordering::Less) | Some(Ordering::Equal)
),
}
}
fn values_equal(a: &PropertyValue, b: Option<&PropertyValue>) -> bool {
let Some(b) = b else {
return false;
};
match (a, b) {
(PropertyValue::Bool(left), PropertyValue::Bool(right)) => left == right,
(PropertyValue::Text(left), PropertyValue::Text(right)) => left == right,
(PropertyValue::Number(left), PropertyValue::Number(right)) => left == right,
_ => false,
}
}
fn value_to_string(v: &PropertyValue) -> String {
match v {
PropertyValue::Text(s) => s.clone(),
PropertyValue::Number(n) => n.to_string(),
PropertyValue::Bool(b) => b.to_string(),
}
}
fn compare_ordered_values(a: &PropertyValue, b: Option<&PropertyValue>) -> Option<Ordering> {
let b = b?;
match (a, b) {
(PropertyValue::Text(left), PropertyValue::Text(right)) => left.partial_cmp(right),
(PropertyValue::Number(left), PropertyValue::Number(right)) => left.partial_cmp(right),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Guard, Operator, Properties, Rule};
fn make_guard(def: bool, rules: Vec<Rule>) -> Guard {
Guard {
name: "test".into(),
rules,
default_value: def,
adopted: true,
rate_limit_per_minute: 0,
rate_limit_properties: vec![],
disable_measurement: None,
}
}
fn rule(prop: &str, op: Operator, values: Vec<PropertyValue>, result: bool) -> Rule {
Rule {
property_name: prop.into(),
operator: op,
values,
result,
enabled: true,
}
}
#[test]
fn no_rules_returns_default() {
assert!(!evaluate_guard(
&make_guard(false, vec![]),
&Properties::new()
));
assert!(evaluate_guard(
&make_guard(true, vec![]),
&Properties::new()
));
}
#[test]
fn equals_match() {
let g = make_guard(
false,
vec![rule("plan", Operator::Equals, vec!["pro".into()], true)],
);
assert!(evaluate_guard(&g, &Properties::new().set("plan", "pro")));
assert!(!evaluate_guard(&g, &Properties::new().set("plan", "free")));
}
#[test]
fn disabled_rule_skipped() {
let mut r = rule("plan", Operator::Equals, vec!["pro".into()], true);
r.enabled = false;
let g = make_guard(false, vec![r]);
assert!(!evaluate_guard(&g, &Properties::new().set("plan", "pro")));
}
#[test]
fn in_operator() {
let g = make_guard(
false,
vec![rule(
"userId",
Operator::In,
vec!["alice".into(), "bob".into()],
true,
)],
);
assert!(evaluate_guard(
&g,
&Properties::new().set("userId", "alice")
));
assert!(!evaluate_guard(
&g,
&Properties::new().set("userId", "charlie")
));
}
#[test]
fn regex_operator() {
let g = make_guard(
false,
vec![rule(
"email",
Operator::Regex,
vec![r"^.*@acme\.com$".into()],
true,
)],
);
assert!(evaluate_guard(
&g,
&Properties::new().set("email", "alice@acme.com")
));
assert!(!evaluate_guard(
&g,
&Properties::new().set("email", "alice@other.com")
));
}
#[test]
fn empty_values_fail_closed() {
let equals = make_guard(false, vec![rule("plan", Operator::Equals, vec![], true)]);
assert!(!evaluate_guard(
&equals,
&Properties::new().set("plan", "pro")
));
let not_in = make_guard(false, vec![rule("userId", Operator::NotIn, vec![], true)]);
assert!(!evaluate_guard(
¬_in,
&Properties::new().set("userId", "alice")
));
let gt = make_guard(false, vec![rule("score", Operator::Gt, vec![], true)]);
assert!(!evaluate_guard(
>,
&Properties::new().set("score", 99.0_f64)
));
let regex = make_guard(false, vec![rule("email", Operator::Regex, vec![], true)]);
assert!(!evaluate_guard(
®ex,
&Properties::new().set("email", "alice@acme.com")
));
}
#[test]
fn numeric_gt() {
let g = make_guard(
false,
vec![rule("score", Operator::Gt, vec![50.0_f64.into()], true)],
);
assert!(evaluate_guard(
&g,
&Properties::new().set("score", 51.0_f64)
));
assert!(!evaluate_guard(
&g,
&Properties::new().set("score", 50.0_f64)
));
}
#[test]
fn first_match_wins() {
let g = make_guard(
false,
vec![
rule("plan", Operator::Equals, vec!["pro".into()], true),
rule("plan", Operator::Equals, vec!["pro".into()], false),
],
);
assert!(evaluate_guard(&g, &Properties::new().set("plan", "pro")));
}
#[test]
fn missing_property_no_match() {
let g = make_guard(
false,
vec![rule("plan", Operator::Equals, vec!["pro".into()], true)],
);
assert!(!evaluate_guard(&g, &Properties::new()));
}
}