rust-runes 0.1.0

A rule engine compatible with Grule's rule language (GRL) and execution model
Documentation
# Getting started

## Installation

```toml
[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:

```grl
// 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:

```rust,no_run
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:

```rust
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/`](https://github.com/eduardomilo/rust-runes/tree/main/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`.