rule_kit 0.1.1

A fast, composable, and DSL-friendly rule engine kit for Rust.
Documentation
use rule_kit::Rule;

#[derive(Debug)]
pub struct UserContext {
    pub age: u32,
    pub score: u32,
    pub applied: Vec<String>,
}

#[derive(Debug)]
pub struct AgeRule;

#[derive(Debug)]
pub struct ScoreRule;

impl Rule<UserContext> for AgeRule {
    type RuleError = ();

    fn name(&self) -> &str {
        "AgeRule"
    }

    fn priority(&self) -> u32 {
        10
    }

    fn evaluate(&self, ctx: &UserContext) -> Result<bool, Self::RuleError> {
        Ok(ctx.age >= 18)
    }

    fn apply(&mut self, ctx: &mut UserContext) -> Result<(), Self::RuleError> {
        ctx.applied.push("Passed age check".into());
        Ok(())
    }

    fn before_apply(&self, _ctx: &UserContext) {
        println!("About to apply AgeRule");
    }

    fn after_apply(&self, _ctx: &UserContext) {
        println!("Finished applying AgeRule");
    }
}

impl Rule<UserContext> for ScoreRule {
    type RuleError = ();

    fn name(&self) -> &str {
        "ScoreRule"
    }

    fn priority(&self) -> u32 {
        5
    }

    fn evaluate(&self, ctx: &UserContext) -> Result<bool, Self::RuleError> {
        Ok(ctx.score >= 80)
    }

    fn apply(&mut self, ctx: &mut UserContext) -> Result<(), Self::RuleError> {
        ctx.applied.push("Passed score check".into());
        Ok(())
    }

    fn before_apply(&self, _ctx: &UserContext) {
        println!("About to apply ScoreRule");
    }

    fn after_apply(&self, _ctx: &UserContext) {
        println!("Finished applying ScoreRule");
    }
}

#[derive(Debug)]
pub enum UserRule {
    Age(AgeRule),
    Score(ScoreRule),
}

impl Rule<UserContext> for UserRule {
    type RuleError = ();

    fn name(&self) -> &str {
        match self {
            UserRule::Age(_) => "AgeRule",
            UserRule::Score(_) => "ScoreRule",
        }
    }

    fn priority(&self) -> u32 {
        match self {
            UserRule::Age(r) => r.priority(),
            UserRule::Score(r) => r.priority(),
        }
    }

    fn evaluate(&self, ctx: &UserContext) -> Result<bool, Self::RuleError> {
        match self {
            UserRule::Age(r) => r.evaluate(ctx),
            UserRule::Score(r) => r.evaluate(ctx),
        }
    }

    fn apply(&mut self, ctx: &mut UserContext) -> Result<(), Self::RuleError> {
        match self {
            UserRule::Age(r) => r.apply(ctx),
            UserRule::Score(r) => r.apply(ctx),
        }
    }

    fn before_apply(&self, ctx: &UserContext) {
        match self {
            UserRule::Age(r) => r.before_apply(ctx),
            UserRule::Score(r) => r.before_apply(ctx),
        }
    }

    fn after_apply(&self, ctx: &UserContext) {
        match self {
            UserRule::Age(r) => r.after_apply(ctx),
            UserRule::Score(r) => r.after_apply(ctx),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_apply_mutates_context() {
        let mut rules: Vec<UserRule> = vec![
            UserRule::Age(AgeRule),
            UserRule::Score(ScoreRule),
        ];

        rules.sort_by_key(|r| r.priority());

        let mut ctx = UserContext {
            age: 20,
            score: 90,
            applied: vec![],
        };

        for rule in &mut rules {
            if rule.evaluate(&ctx).unwrap() {
                rule.before_apply(&ctx);
                rule.apply(&mut ctx).unwrap();
                rule.after_apply(&ctx);
            }
        }

        assert_eq!(ctx.applied, vec![
            "Passed score check",
            "Passed age check"
        ]);
    }
}