Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting started

Installation

[dependencies]
rust-runes = "0.1.0"
serde = { version = "1", features = ["derive"] }

rust-runes requires Rust 1.83 or later. serde is only needed to convert your own types into facts, which is the usual way to use the engine.

Rules

Rules are usually kept in .grl files, outside your code:

// rules/pricing.grl
rule LongRide "Long rides get a flat rate" salience 10 {
    when
        Ride.Distance > 5000 && !Ride.Priced
    then
        Ride.NetAmount = 143.32;
        Ride.Priced = true;
        Log("Priced a long ride of " + Ride.Distance + "m");
}

rule ShortRide "Short rides are priced per meter" {
    when
        Ride.Distance <= 5000 && !Ride.Priced
    then
        Ride.NetAmount = Ride.Distance * 0.025;
        Ride.Priced = true;
}

Each rule has a name, an optional description, an optional salience (priority, default 0), a when condition and then actions. Ride is a fact your program provides.

Running them

Facts can be any serializable type. Field names are used as written, so structs that rules read often keep Grule-style capitalized names:

use rust_runes::{Fact, RuleEngine};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
struct Ride {
    Distance: i64,
    NetAmount: f64,
    Priced: bool,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut engine = RuleEngine::new();
    engine.add_rules_from_dir("rules")?;

    let ride = Ride { Distance: 6000, NetAmount: 0.0, Priced: false };
    let mut facts = HashMap::from([("Ride".to_string(), Fact::from_serialize("Ride", &ride)?)]);

    let result = engine.execute(&mut facts)?;

    let ride: Ride = facts["Ride"].deserialize()?;
    println!("fired {:?}, net amount {}", result.rules_fired, ride.NetAmount);
    Ok(())
}

Facts are passed as a map from the name rules use to the fact. execute runs rules until none applies and changes the facts in place; deserialize turns a fact back into your type. The result lists the rules that fired, in order.

The same program with the rules inline, as a test would write it:

use rust_runes::{Fact, RuleEngine};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize)]
#[allow(non_snake_case)]
struct Ride {
    Distance: i64,
    NetAmount: f64,
    Priced: bool,
}

let mut engine = RuleEngine::new();
engine.add_rules_from_str(r#"
    rule LongRide salience 10 {
        when Ride.Distance > 5000 && !Ride.Priced
        then Ride.NetAmount = 143.32; Ride.Priced = true;
    }
    rule ShortRide {
        when Ride.Distance <= 5000 && !Ride.Priced
        then Ride.NetAmount = Ride.Distance * 0.025; Ride.Priced = true;
    }
"#)?;

for (distance, expected) in [(6000, 143.32), (1200, 30.0)] {
    let ride = Ride { Distance: distance, NetAmount: 0.0, Priced: false };
    let mut facts = HashMap::from([("Ride".to_string(), Fact::from_serialize("Ride", &ride)?)]);
    engine.execute(&mut facts)?;
    let ride: Ride = facts["Ride"].deserialize()?;
    assert_eq!(ride.NetAmount, expected);
}
fn shareable<T: Send + Sync>(_: &T) {}
shareable(&engine);
Ok::<(), Box<dyn std::error::Error>>(())

execute takes &self, so one engine can serve many executions, including from several threads at once.

Loading rules

  • add_rules_from_str(grl) loads rules from text.
  • add_rules_from_file(path) loads one file.
  • add_rules_from_dir(dir) loads every .grl file under a directory, recursively, in path order.
  • add_rule(rule) adds a rule built in code with Rule::new and the ast::Expression constructors.

Each returns the number of rules added. A syntax error reports the file, line and column, and nothing from a file that fails to load is added (files loaded before it in the same directory stay loaded). Rule names must be unique.

Examples

The repository’s examples/ directory has complete programs:

  • quickstart: rules from a .grl file, run against a serde struct.
  • methods: methods on your own types called from rules, and an Out fact that rules report rejection reasons into.
  • speed_up: Grule’s classic TestCar rule firing cycle after cycle, with a listener.

Run one with cargo run --example quickstart.