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.grlfile under a directory, recursively, in path order.add_rule(rule)adds a rule built in code withRule::newand theast::Expressionconstructors.
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.grlfile, run against a serde struct.methods: methods on your own types called from rules, and anOutfact that rules report rejection reasons into.speed_up: Grule’s classicTestCarrule firing cycle after cycle, with a listener.
Run one with cargo run --example quickstart.