use ooroo::{bound_field, field, rule_ref, Bound, Context, RuleSet, RuleSetBuilder, Verdict};
#[test]
fn between_literal_bounds_within_range() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| r.when(field("age").between(18_i64, 65_i64)))
.terminal("r", 0)
.compile()
.unwrap();
assert!(ruleset
.evaluate(&Context::new().set("age", 18_i64))
.is_some());
assert!(ruleset
.evaluate(&Context::new().set("age", 40_i64))
.is_some());
assert!(ruleset
.evaluate(&Context::new().set("age", 65_i64))
.is_some());
assert!(ruleset
.evaluate(&Context::new().set("age", 17_i64))
.is_none());
assert!(ruleset
.evaluate(&Context::new().set("age", 66_i64))
.is_none());
}
#[test]
fn between_field_bounds_both_sides() {
let ruleset = RuleSetBuilder::new()
.rule("in_tier", |r| {
r.when(field("score").between(bound_field("tier.min"), bound_field("tier.max")))
})
.terminal("in_tier", 0)
.compile()
.unwrap();
let ctx = Context::new()
.set("score", 75_i64)
.set("tier.min", 60_i64)
.set("tier.max", 89_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("score", 90_i64)
.set("tier.min", 60_i64)
.set("tier.max", 89_i64);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn between_field_bounds_missing_bound_field_returns_false() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| {
r.when(field("score").between(bound_field("tier.min"), bound_field("tier.max")))
})
.terminal("r", 0)
.compile()
.unwrap();
let ctx = Context::new().set("score", 75_i64).set("tier.min", 60_i64);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn between_mixed_bounds_literal_low_field_high() {
let ruleset = RuleSetBuilder::new()
.rule("eligible", |r| {
r.when(field("age").between(18_i64, bound_field("policy.max_age")))
})
.terminal("eligible", 0)
.compile()
.unwrap();
let ctx = Context::new()
.set("age", 25_i64)
.set("policy.max_age", 60_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("age", 65_i64)
.set("policy.max_age", 60_i64);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn between_mixed_bounds_field_low_literal_high() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| {
r.when(field("score").between(bound_field("tier.min"), 100_i64))
})
.terminal("r", 0)
.compile()
.unwrap();
let ctx = Context::new().set("score", 80_i64).set("tier.min", 60_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("score", 50_i64).set("tier.min", 60_i64);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn between_dsl_literal_bounds() {
let ruleset = RuleSet::from_dsl("rule r (priority 0):\n age BETWEEN 18, 65").unwrap();
assert!(ruleset
.evaluate(&Context::new().set("age", 30_i64))
.is_some());
assert!(ruleset
.evaluate(&Context::new().set("age", 10_i64))
.is_none());
}
#[test]
fn between_dsl_field_bounds() {
let ruleset =
RuleSet::from_dsl("rule r (priority 0):\n score BETWEEN tier.min, tier.max").unwrap();
let ctx = Context::new()
.set("score", 75_i64)
.set("tier.min", 60_i64)
.set("tier.max", 89_i64);
assert!(ruleset.evaluate(&ctx).is_some());
}
#[test]
fn between_dsl_mixed_bounds() {
let ruleset =
RuleSet::from_dsl("rule r (priority 0):\n score BETWEEN 10, tier.max_score").unwrap();
let ctx = Context::new()
.set("score", 50_i64)
.set("tier.max_score", 100_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("score", 5_i64)
.set("tier.max_score", 100_i64);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn in_with_field_ref_member() {
let ruleset = RuleSetBuilder::new()
.rule("allowed", |r| {
r.when(field("role").is_in([Bound::from("admin"), bound_field("team.default_role")]))
})
.terminal("allowed", 0)
.compile()
.unwrap();
let ctx = Context::new()
.set("role", "admin")
.set("team.default_role", "member");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("role", "editor")
.set("team.default_role", "editor");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("role", "viewer")
.set("team.default_role", "editor");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn not_in_with_field_ref_member() {
let ruleset = RuleSetBuilder::new()
.rule("not_blocked", |r| {
r.when(
field("status")
.not_in([Bound::from("banned"), bound_field("account.override_block")]),
)
})
.terminal("not_blocked", 0)
.compile()
.unwrap();
let ctx = Context::new()
.set("status", "active")
.set("account.override_block", "suspended");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("status", "banned")
.set("account.override_block", "suspended");
assert!(ruleset.evaluate(&ctx).is_none());
let ctx = Context::new()
.set("status", "suspended")
.set("account.override_block", "suspended");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn in_missing_field_ref_member_skipped() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| {
r.when(field("role").is_in([Bound::from("admin"), bound_field("team.default_role")]))
})
.terminal("r", 0)
.compile()
.unwrap();
let ctx = Context::new().set("role", "admin");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("role", "editor");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn in_dsl_field_ref_member() {
let ruleset =
RuleSet::from_dsl("rule r (priority 0):\n role IN [\"admin\", team.default_role]")
.unwrap();
let ctx = Context::new()
.set("role", "editor")
.set("team.default_role", "editor");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("role", "viewer")
.set("team.default_role", "editor");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn between_composes_with_and() {
let ruleset = RuleSetBuilder::new()
.rule("age_ok", |r| {
r.when(field("age").between(bound_field("policy.min_age"), 65_i64))
})
.rule("status_ok", |r| r.when(field("status").eq("active")))
.rule("eligible", |r| {
r.when(rule_ref("age_ok").and(rule_ref("status_ok")))
})
.terminal("eligible", 0)
.compile()
.unwrap();
let ctx = Context::new()
.set("age", 30_i64)
.set("policy.min_age", 18_i64)
.set("status", "active");
assert_eq!(ruleset.evaluate(&ctx), Some(Verdict::new("eligible", true)));
let ctx = Context::new()
.set("age", 15_i64)
.set("policy.min_age", 18_i64)
.set("status", "active");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn in_composes_with_or_and_not() {
let ruleset = RuleSetBuilder::new()
.rule("allowed_region", |r| {
r.when(field("region").is_in([
Bound::from("us-east"),
Bound::from("us-west"),
bound_field("org.extra_region"),
]))
})
.rule("not_flagged", |r| r.when(!field("flagged").eq(true)))
.rule("can_access", |r| {
r.when(rule_ref("allowed_region").and(rule_ref("not_flagged")))
})
.terminal("can_access", 0)
.compile()
.unwrap();
let ctx = Context::new()
.set("region", "eu-west")
.set("org.extra_region", "eu-west")
.set("flagged", false);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("region", "us-east")
.set("org.extra_region", "eu-west")
.set("flagged", true);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn policy_loan_approval_natural_rules() {
let ruleset = RuleSetBuilder::new()
.rule("credit_score_ok", |r| {
r.when(
field("applicant.credit_score")
.between(bound_field("applicant.min_required_score"), 850_i64),
)
})
.rule("income_tier_ok", |r| {
r.when(field("applicant.income_tier").is_in([
Bound::from("standard"),
Bound::from("premium"),
bound_field("loan.extra_allowed_tier"),
]))
})
.rule("not_on_blocklist", |r| {
r.when(!field("applicant.on_blocklist").eq(true))
})
.rule("approve", |r| {
r.when(
rule_ref("credit_score_ok")
.and(rule_ref("income_tier_ok"))
.and(rule_ref("not_on_blocklist")),
)
})
.terminal("approve", 0)
.compile()
.unwrap();
let approved_ctx = Context::new()
.set("applicant.credit_score", 720_i64)
.set("applicant.min_required_score", 680_i64)
.set("applicant.income_tier", "standard")
.set("applicant.on_blocklist", false)
.set("loan.extra_allowed_tier", "trial");
assert_eq!(
ruleset.evaluate(&approved_ctx),
Some(Verdict::new("approve", true))
);
let denied_ctx = Context::new()
.set("applicant.credit_score", 650_i64)
.set("applicant.min_required_score", 680_i64)
.set("applicant.income_tier", "standard")
.set("applicant.on_blocklist", false)
.set("loan.extra_allowed_tier", "trial");
assert!(ruleset.evaluate(&denied_ctx).is_none());
let trial_ctx = Context::new()
.set("applicant.credit_score", 720_i64)
.set("applicant.min_required_score", 680_i64)
.set("applicant.income_tier", "trial")
.set("applicant.on_blocklist", false)
.set("loan.extra_allowed_tier", "trial");
assert!(ruleset.evaluate(&trial_ctx).is_some());
}