Skip to main content

rust_runes/
lib.rs

1//! A rule engine compatible with [Grule](https://github.com/hyperjumptech/grule-rule-engine)'s
2//! rule language (GRL) and execution model, for embedding business rules in Rust.
3//!
4//! Rules are written in GRL and loaded into a [`RuleEngine`]; facts are named
5//! [`FactValue`]s, typically converted from your own serde types. Executing runs rules
6//! until none apply: each cycle fires the highest-salience rule whose condition holds.
7//!
8//! ```
9//! use rust_runes::{Fact, RuleEngine};
10//! use serde::{Deserialize, Serialize};
11//! use std::collections::HashMap;
12//!
13//! #[derive(Serialize, Deserialize)]
14//! #[allow(non_snake_case)]
15//! struct Ride {
16//!     Distance: i64,
17//!     NetAmount: f64,
18//!     Priced: bool,
19//! }
20//!
21//! let mut engine = RuleEngine::new();
22//! engine.add_rules_from_str(r#"
23//!     rule LongRide "Long rides get a flat rate" salience 10 {
24//!         when Ride.Distance > 5000 && !Ride.Priced
25//!         then Ride.NetAmount = 143.32; Ride.Priced = true;
26//!     }
27//! "#)?;
28//!
29//! let ride = Ride { Distance: 6000, NetAmount: 0.0, Priced: false };
30//! let mut facts = HashMap::from([("Ride".to_string(), Fact::from_serialize("Ride", &ride)?)]);
31//! let result = engine.execute(&mut facts)?;
32//!
33//! let ride: Ride = facts["Ride"].deserialize()?;
34//! assert_eq!(result.rules_fired, ["LongRide"]);
35//! assert_eq!(ride.NetAmount, 143.32);
36//! # Ok::<(), Box<dyn std::error::Error>>(())
37//! ```
38//!
39//! See the [guide](https://eduardomilo.github.io/rust-runes/) for the rule language,
40//! built-in functions, execution model, and how this differs from Grule.
41
42#![warn(missing_docs)]
43
44/// Expression trees for rule conditions and actions.
45pub mod ast;
46mod bitset;
47mod compiled;
48/// The rule engine and its execution model.
49pub mod engine;
50/// Fact values and conversion to and from Rust types.
51pub mod facts;
52mod functions;
53/// Storage and loading of rules.
54pub mod knowledge_base;
55mod methods;
56mod operators;
57/// Parsing GRL text into rules.
58pub mod parser;
59/// The rule type.
60pub mod rule;
61mod time_functions;
62
63pub use engine::{EngineError, EngineListener, ExecutionResult, RuleEngine, RulePhase};
64pub use facts::{serde_time, Fact, FactValue};
65pub use knowledge_base::KnowledgeBase;
66pub use rule::Rule;
67
68// Runs the Rust examples in the README and the guide (`docs/`) as doc tests.
69// `tests/docs_grl.rs` checks their GRL examples.
70#[cfg(doctest)]
71mod guide {
72    #[doc = include_str!("../README.md")]
73    struct Readme;
74    #[doc = include_str!("../docs/src/introduction.md")]
75    struct Introduction;
76    #[doc = include_str!("../docs/src/getting-started.md")]
77    struct GettingStarted;
78    #[doc = include_str!("../docs/src/facts.md")]
79    struct Facts;
80    #[doc = include_str!("../docs/src/language.md")]
81    struct Language;
82    #[doc = include_str!("../docs/src/functions.md")]
83    struct Functions;
84    #[doc = include_str!("../docs/src/execution.md")]
85    struct Execution;
86    #[doc = include_str!("../docs/src/extending.md")]
87    struct Extending;
88    #[doc = include_str!("../docs/src/migrating.md")]
89    struct Migrating;
90    #[doc = include_str!("../docs/src/performance.md")]
91    struct Performance;
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::ast::Expression;
98    use std::collections::HashMap;
99
100    #[test]
101    fn test_rule_engine_basic() {
102        let mut engine = RuleEngine::new();
103        let mut facts = HashMap::new();
104
105        // Simple rule: if x > 5 then y = 10
106        let rule = Rule::new(
107            "test_rule".to_string(),
108            0,
109            Expression::GreaterThan(
110                Box::new(Expression::Variable("x".to_string())),
111                Box::new(Expression::Integer(5)),
112            ),
113            vec![
114                Expression::assign(&["y"], Expression::Integer(10)),
115                Expression::call("Retract", vec![Expression::String("test_rule".to_string())]),
116            ],
117        );
118
119        engine.add_rule(rule).unwrap();
120        facts.insert("x".to_string(), Fact::number_fact("x".to_string(), 7.0));
121
122        let result = engine.execute(&mut facts).unwrap();
123
124        assert_eq!(result.rules_fired.len(), 1);
125        assert_eq!(result.rules_fired[0], "test_rule");
126        assert_eq!(facts.get("y").unwrap().value, FactValue::Integer(10));
127    }
128
129    #[test]
130    fn test_fact_manipulation() {
131        let mut fact = Fact::from_object(
132            "test".to_string(),
133            HashMap::from([
134                (
135                    "field1".to_string(),
136                    FactValue::String("value1".to_string()),
137                ),
138                ("field2".to_string(), FactValue::Integer(42)),
139            ]),
140        );
141
142        assert_eq!(
143            fact.get_field("field1"),
144            Some(&FactValue::String("value1".to_string()))
145        );
146        assert_eq!(fact.get_field("field2"), Some(&FactValue::Integer(42)));
147
148        fact.set_field("field3".to_string(), FactValue::Boolean(true))
149            .unwrap();
150        assert_eq!(fact.get_field("field3"), Some(&FactValue::Boolean(true)));
151    }
152
153    #[test]
154    fn test_knowledge_base() {
155        let mut kb = KnowledgeBase::new();
156
157        let rule1 = Rule::new("rule1".to_string(), 10, Expression::Boolean(true), vec![]);
158        let rule2 = Rule::new("rule2".to_string(), 5, Expression::Boolean(true), vec![]);
159
160        kb.add_rule(rule1).unwrap();
161        kb.add_rule(rule2).unwrap();
162
163        assert_eq!(kb.len(), 2);
164
165        let sorted_rules = kb.get_rules_sorted_by_salience();
166        assert_eq!(sorted_rules[0].name, "rule1"); // Higher salience first
167        assert_eq!(sorted_rules[1].name, "rule2");
168    }
169
170    #[test]
171    fn test_grl_parser_simple_rule() {
172        let parser = parser::GrlParser::new();
173
174        let grl_text = r#"
175            rule SimpleRule "Test simple rule" salience 5 {
176                when
177                    x == 10
178                then
179                    y = 20;
180            }
181        "#;
182
183        let rule = parser.parse_rule(grl_text).unwrap();
184
185        assert_eq!(rule.name, "SimpleRule");
186        assert_eq!(rule.description, Some("Test simple rule".to_string()));
187        assert_eq!(rule.salience, 5);
188
189        // Test the condition (x == 10)
190        match &rule.when_condition {
191            Expression::Equal(left, right) => match (&**left, &**right) {
192                (Expression::Variable(var), Expression::Integer(val)) => {
193                    assert_eq!(var, "x");
194                    assert_eq!(*val, 10);
195                }
196                _ => panic!("Unexpected expression structure in condition"),
197            },
198            _ => panic!("Expected Equal expression for condition"),
199        }
200
201        // Test the action (y = 20)
202        assert_eq!(
203            rule.then_actions,
204            vec![Expression::assign(&["y"], Expression::Integer(20))]
205        );
206    }
207
208    #[test]
209    fn test_grl_parser_complex_rule() {
210        let parser = parser::GrlParser::new();
211
212        let rule_age = Rule::new(
213            "AgeRule".to_string(),
214            10,
215            Expression::GreaterEqual(
216                Box::new(Expression::FieldAccess(
217                    Box::new(Expression::Variable("customer".to_string())),
218                    "age".to_string(),
219                )),
220                Box::new(Expression::Integer(18)),
221            ),
222            vec![
223                Expression::assign(&["customer", "eligible"], Expression::Boolean(true)),
224                Expression::assign(
225                    &["message"],
226                    Expression::String("Customer is eligible for premium service".to_string()),
227                ),
228            ],
229        );
230
231        // Testing with a simpler format to test basic field access
232        let grl_text = r#"
233            rule EligibilityRule "Tests eligibility" salience 10 {
234                when
235                    customer.eligible == true
236                then
237                    message = "Confirmed eligible";
238            }
239        "#;
240
241        let rule = parser.parse_rule(grl_text).unwrap();
242
243        assert_eq!(rule.name, "EligibilityRule");
244        assert_eq!(rule.description, Some("Tests eligibility".to_string()));
245        assert_eq!(rule.salience, 10);
246
247        // Test the condition (customer.eligible == true)
248        match &rule.when_condition {
249            Expression::Equal(left, right) => match (&**left, &**right) {
250                (Expression::FieldAccess(obj, field), Expression::Boolean(val)) => match &**obj {
251                    Expression::Variable(obj_name) => {
252                        assert_eq!(obj_name, "customer");
253                        assert_eq!(field, "eligible");
254                        assert!(*val);
255                    }
256                    _ => panic!("Expected Variable expression for object"),
257                },
258                _ => panic!("Unexpected expression structure in condition"),
259            },
260            _ => panic!("Expected Equal expression for condition"),
261        }
262
263        // Test the action (message = "Confirmed eligible")
264        assert_eq!(
265            rule.then_actions,
266            vec![Expression::assign(
267                &["message"],
268                Expression::String("Confirmed eligible".to_string()),
269            )]
270        );
271
272        // Also test our programmatically created rule structure
273        assert_eq!(rule_age.name, "AgeRule");
274        assert_eq!(rule_age.salience, 10);
275
276        // Test the condition (customer.age >= 18)
277        match &rule_age.when_condition {
278            Expression::GreaterEqual(left_field, left_val) => match (&**left_field, &**left_val) {
279                (Expression::FieldAccess(obj, field), Expression::Integer(val)) => match &**obj {
280                    Expression::Variable(obj_name) => {
281                        assert_eq!(obj_name, "customer");
282                        assert_eq!(field, "age");
283                        assert_eq!(*val, 18);
284                    }
285                    _ => panic!("Expected Variable expression for object"),
286                },
287                _ => panic!("Unexpected expression structure in age condition"),
288            },
289            _ => panic!("Expected GreaterEqual expression for age condition"),
290        }
291
292        // Test the actions (customer.eligible = true; message = "Customer is eligible for premium service")
293        assert_eq!(
294            rule_age.then_actions,
295            vec![
296                Expression::assign(&["customer", "eligible"], Expression::Boolean(true)),
297                Expression::assign(
298                    &["message"],
299                    Expression::String("Customer is eligible for premium service".to_string()),
300                ),
301            ]
302        );
303    }
304
305    #[test]
306    fn test_grl_parser_arithmetic_expressions() {
307        let parser = parser::GrlParser::new();
308
309        let grl_text = r#"
310            rule ArithmeticRule "Test arithmetic operations" {
311                when
312                    x > 5
313                then
314                    y = x + 10;
315            }
316        "#;
317
318        let rule = parser.parse_rule(grl_text).unwrap();
319
320        assert_eq!(rule.name, "ArithmeticRule");
321
322        // Test the action (y = x + 10)
323        assert_eq!(
324            rule.then_actions,
325            vec![Expression::assign(
326                &["y"],
327                Expression::Add(
328                    Box::new(Expression::Variable("x".to_string())),
329                    Box::new(Expression::Integer(10)),
330                ),
331            )]
332        );
333    }
334}