use ooroo::{field, rule_ref, Context, RuleSet, RuleSetBuilder};
#[test]
fn dsl_parse_and_evaluate() {
let dsl = r#"
rule eligible_age:
user.age >= 18
rule active_account:
user.status == "active"
rule can_proceed (priority 10):
eligible_age AND active_account
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new()
.set("user.age", 25_i64)
.set("user.status", "active");
let verdict = ruleset.evaluate(&ctx).unwrap();
assert_eq!(verdict.terminal(), "can_proceed");
assert!(verdict.result());
}
#[test]
fn dsl_deny_before_allow() {
let dsl = r#"
rule banned:
user.banned == true
rule eligible:
user.age >= 18
rule deny (priority 0):
banned
rule allow (priority 10):
eligible
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new()
.set("user.banned", true)
.set("user.age", 25_i64);
let verdict = ruleset.evaluate(&ctx).unwrap();
assert_eq!(verdict.terminal(), "deny");
let ctx = Context::new()
.set("user.banned", false)
.set("user.age", 25_i64);
let verdict = ruleset.evaluate(&ctx).unwrap();
assert_eq!(verdict.terminal(), "allow");
}
#[test]
fn dsl_or_expression() {
let dsl = r#"
rule r (priority 0):
x == 1 OR y == 2
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("x", 1_i64).set("y", 99_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("x", 99_i64).set("y", 2_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("x", 99_i64).set("y", 99_i64);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_not_expression() {
let dsl = r#"
rule r (priority 0):
NOT x == 1
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("x", 1_i64);
assert!(ruleset.evaluate(&ctx).is_none());
let ctx = Context::new().set("x", 2_i64);
assert!(ruleset.evaluate(&ctx).is_some());
}
#[test]
fn dsl_parenthesized_grouping() {
let dsl = r#"
rule r (priority 0):
(x == 1 OR x == 2) AND y == 10
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("x", 1_i64).set("y", 10_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("x", 2_i64).set("y", 10_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("x", 3_i64).set("y", 10_i64);
assert!(ruleset.evaluate(&ctx).is_none());
let ctx = Context::new().set("x", 1_i64).set("y", 99_i64);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_and_binds_tighter_than_or() {
let dsl = r#"
rule r (priority 0):
x == 1 OR y == 2 AND z == 3
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new()
.set("x", 1_i64)
.set("y", 99_i64)
.set("z", 99_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("x", 99_i64)
.set("y", 2_i64)
.set("z", 3_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("x", 99_i64)
.set("y", 2_i64)
.set("z", 99_i64);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_all_value_types() {
let dsl = r#"
rule int_check:
x == 42
rule float_check:
y >= 3.14
rule bool_check:
z == true
rule string_check:
w == "hello"
rule all (priority 0):
int_check AND float_check AND bool_check AND string_check
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new()
.set("x", 42_i64)
.set("y", 3.14_f64)
.set("z", true)
.set("w", "hello");
assert!(ruleset.evaluate(&ctx).is_some());
}
#[test]
fn dsl_all_comparison_ops() {
let dsl = r#"
rule r (priority 0):
a == 1 AND b != 2 AND c > 3 AND d >= 4 AND e < 5 AND f <= 6
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new()
.set("a", 1_i64)
.set("b", 99_i64)
.set("c", 4_i64)
.set("d", 4_i64)
.set("e", 4_i64)
.set("f", 6_i64);
assert!(ruleset.evaluate(&ctx).is_some());
}
#[test]
fn dsl_comments_are_ignored() {
let dsl = r#"
# This is a header comment
rule r (priority 0):
# Field comparison
x == 1
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("x", 1_i64);
assert!(ruleset.evaluate(&ctx).is_some());
}
#[test]
fn dsl_parse_error_has_location() {
let dsl = "rule r:\n ==";
let err = RuleSet::from_dsl(dsl);
let msg = err.unwrap_err().to_string();
assert!(msg.contains("line"), "error should mention line: {msg}");
assert!(msg.contains("column"), "error should mention column: {msg}");
}
#[test]
fn dsl_hyphenated_rule_name_gives_descriptive_error() {
let dsl = "rule within-budget: value > 0";
let err = RuleSet::from_dsl(dsl).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("letters") || msg.contains("underscore") || msg.contains("invalid"),
"error should describe the naming constraint, got: {msg}"
);
}
#[test]
fn dsl_underscored_rule_name_still_works() {
let dsl = "rule within_budget (priority 0):\n value > 0";
let ctx = Context::new().set("value", 10_i64);
let ruleset = RuleSet::from_dsl(dsl).unwrap();
assert!(ruleset.evaluate(&ctx).is_some());
}
#[test]
fn dsl_compile_error_propagates() {
let dsl = r#"
rule r (priority 0):
nonexistent
"#;
let err = RuleSet::from_dsl(dsl);
assert!(err.is_err());
let msg = err.unwrap_err().to_string();
assert!(msg.contains("undefined rule reference"));
}
#[test]
fn dsl_matches_builder_api() {
let dsl = r#"
rule age_ok:
user.age >= 18
rule active:
user.status == "active"
rule allowed (priority 0):
age_ok AND active
"#;
let dsl_ruleset = RuleSet::from_dsl(dsl).unwrap();
let builder_ruleset = RuleSetBuilder::new()
.rule("age_ok", |r| r.when(field("user.age").gte(18_i64)))
.rule("active", |r| r.when(field("user.status").eq("active")))
.rule("allowed", |r| {
r.when(rule_ref("age_ok").and(rule_ref("active")))
})
.terminal("allowed", 0)
.compile()
.unwrap();
let ctx = Context::new()
.set("user.age", 25_i64)
.set("user.status", "active");
let dsl_result = dsl_ruleset.evaluate(&ctx);
let builder_result = builder_ruleset.evaluate(&ctx);
assert_eq!(
dsl_result.as_ref().map(|v| v.terminal()),
builder_result.as_ref().map(|v| v.terminal()),
);
assert_eq!(
dsl_result.as_ref().map(|v| v.result()),
builder_result.as_ref().map(|v| v.result()),
);
}
#[test]
fn dsl_negative_number() {
let dsl = r#"
rule r (priority 0):
x == -5
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("x", -5_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("x", 5_i64);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_in_expression() {
let dsl = r#"
rule valid_country (priority 0):
user.country IN ["US", "CA", "GB", "AU"]
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("user.country", "US");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("user.country", "GB");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("user.country", "FR");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_not_in_expression() {
let dsl = r#"
rule not_banned_country (priority 0):
user.country NOT IN ["NK", "IR"]
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("user.country", "US");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("user.country", "NK");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_between_expression() {
let dsl = r#"
rule age_range (priority 0):
user.age BETWEEN 18, 65
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("user.age", 25_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("user.age", 18_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("user.age", 65_i64);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("user.age", 17_i64);
assert!(ruleset.evaluate(&ctx).is_none());
let ctx = Context::new().set("user.age", 66_i64);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_like_expression() {
let dsl = r#"
rule gmail_user (priority 0):
user.email LIKE "%@gmail.com"
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("user.email", "john@gmail.com");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("user.email", "john@yahoo.com");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_not_like_expression() {
let dsl = r#"
rule non_test_email (priority 0):
user.email NOT LIKE "%@test.%"
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("user.email", "john@gmail.com");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("user.email", "bot@test.com");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_is_null_expression() {
let dsl = r#"
rule missing_email (priority 0):
user.email IS NULL
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new();
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("user.email", "x@y.com");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_is_not_null_expression() {
let dsl = r#"
rule has_email (priority 0):
user.email IS NOT NULL
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new().set("user.email", "x@y.com");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new();
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_combined_new_operators() {
let dsl = r#"
rule valid_country:
user.country IN ["US", "CA", "GB"]
rule valid_age:
user.age BETWEEN 18, 65
rule has_email:
user.email IS NOT NULL
rule eligible (priority 0):
valid_country AND valid_age AND has_email
"#;
let ruleset = RuleSet::from_dsl(dsl).unwrap();
let ctx = Context::new()
.set("user.country", "US")
.set("user.age", 30_i64)
.set("user.email", "john@example.com");
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new()
.set("user.country", "US")
.set("user.age", 30_i64);
assert!(ruleset.evaluate(&ctx).is_none());
let ctx = Context::new()
.set("user.country", "FR")
.set("user.age", 30_i64)
.set("user.email", "john@example.com");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_from_file() {
let ruleset = RuleSet::from_file("examples/rules.ooroo").unwrap();
let ctx = Context::new()
.set("user.age", 25_i64)
.set("user.status", "active")
.set("user.banned", false);
let verdict = ruleset.evaluate(&ctx).unwrap();
assert_eq!(verdict.terminal(), "can_proceed");
}