Go-Rules-Engines is a powerful, lightweight, un-opinionated rules engine written in Go. Rules are expressed in simple JSON, and can be stored anywhere (in standalone files, source code, or as data stored in databases), and edited by anyone (even persons with no technical skill).
- Deterministic: uses JSON as an AST (Abstract Syntax Tree) from which to draw inferences and publish reactive events
- Supports "any" and "all" context operators
- Blazing fast
- Secure and sandboxed - JSON AST is never evaluated
- Easily extensible - Perfect for building larger expert systems via composition
- Easily modifiable - JSON AST can be modified by anybody -- no technical expertise required
Works best with Go >=1.8.
go get github.com/icheka/go-rules-engine
Go-Rules-Engine is build around the concept of Rules. A rule is an expression of business logic as a combination of one or more conditions and an event to be fired when those conditions are met.
Go-Rules-Engine
|
-----------
| |
Conditions Event
As an example, a simple rule for a fictional discount engine might be stated as: "Offer a 10% discount if the customer buys 2 apples". Writing a Rule for this discount is easy enough:
Conditions are groups of statements that are evaluated by Go-Rules-Engine. Evaluating to true
will cause their corresponding event to be fired. Firing an event, instead of directly executing an action, allows Go-Rules-Engine to remain un-opinionated, leaving full control over results processing in the hands of the engineer. This makes Go-Rules-Engine extremely flexible and easily integratable.
Conditions comprise two parts: all
and any
. all
is used enforce that all statements (enclosed by all
evaluate to true
) for the corresponding event to be fired. any
works a bit differently: it requires just one of its statements to evaluate to true
for the corresponding event to be fired.
The condition of the discount above will look like:
{
"condition": {
"all": [
{
"identifier": "applesCount",
"operator": "=",
"value": 2
}
]
}
}
Go-Rules-Engine fires a Rule's event when its Conditions evaluates to true. Events are allowed two properties: type
and payload
and they are both up to the engineer to customise.
The event for the discount above could look like:
{
...
"event": {
"type": "discount",
"payload": {
"percentage": 10,
"item": "apple"
}
}
}
Thus, the discount Rule can be expressed as:
{
"condition": {
"all": [
{
"identifier": "applesCount",
"operator": "=",
"value": 2
}
]
},
"event": {
"type": "discount",
"payload": {
"percentage": 10,
"item": "apple"
}
}
}
Following the example above, assuming that the discount Rule is stored in the file system, we can process the Rule like so:
package main
import (
"fmt"
"os"
ruleEngine "github.com/Icheka/go-rules-engine/rule_engine"
)
func main() {
// read discount rule
jsonBytes, err := os.ReadFile("apple-discount-rule.json")
if err != nil {
panic(err)
}
// a map[string]interface{} representing a customer's cart at checkout
// cart contains a key (applesCount) matching the `identifier` in our rule's condition
cart := map[string]interface{}{
"applesCount": 3,
"orangesCount": 5,
"cookiesCount": 1
}
// create a new Rule Engine...
engine := ruleEngine.New(nil)
// ... and add the discount rule
engine.AddRule(string(jsonByres))
// then process it
fmt.Printf("%+v", engine.EvaluateRules(cart))
// [{Type:discount Payload:map[item:apple percentage:10]}]
}
A rule for the statement: "player A wins the match if player A has no cards left, or if player B has up to 20 cards left" has two possible paths:
- Player A has no cards left
- Player B has up to 20 (i.e greater or equal to 20) cards left
These can be expressed aptly using any
:
{
"condition": {
"any": [
{
"identifier": "playerACards",
"operator": "=",
"value": 0
},
{
"identifier": "playerBCards",
"operator": ">=",
"value": 20
}
]
},
"event": {
"type": "win"
}
}
// [{Type:win Payload:<nil>}]
Both event.type
and event.payload
are optional and entirely up to the rule creator to specify, provided they are valid JSON structures.
By default, the Rules Engine will panic if it is unable to find the value referenced by identifier
:
// rule
{
"condition": {
"any": [
{
"identifier": "undefinedProperty",
"operator": "=",
"value": 0
},
{
"identifier": "playerBCards",
"operator": ">=",
"value": [20]
}
]
},
"event": {
"type": "win"
}
}
game := map[string]interface{}{
"playerACards": 2,
"playerBCards": 20,
}
engine := ruleEngine.New(nil)
engine.AddRule(string(rule))
fmt.Printf("%+v", engine.EvaluateRules(game))
// this will panic "value for identifier undefinedProperty not found" because the "undefinedProperty" identifier was not found in the game map.
If this is not the behaviour you want, you can switch this check off by passing an options
struct to the ruleEngine.New
constructor:
...
engine := ruleEngine.New(&ruleEngine.EvaluatorOptions{
AllowUndefinedVars: true,
})
...
Now, when Rules Engine encounters an undefined property, it will evaluate that statement to false and continue processing the rule.
The following operators are available in Go-Rules-Engine:
Operator | Alias | Description |
---|---|---|
= | eq | Equals (e.g 3 equals 3) |
!= | neq | Is not equal (e.g 3 is not equal to 4) |
< | lt | Is less than (e.g 3 is less than 4) |
> | gt | Is greater than (e.g 5 is greater than 4) |
<= | lte | Is less than or equal (e.g 5 is less than or equal to 6) |
>= | gte | Is greater than or equal (e.g 5 is greater than or equal to 3) |
The following operators will be added in future:
- Array contains (contains)
- Array does not contain (!contains)
- Support for adding custom operators
Although Go-Rules-Engine requires facts to be evaluated against rules to have a map[string]interface{} type, most Go code is designed and implemented around structs (not maps). Go-Rules-Engine provides a utility for converting your struct to a map:
import "github.com/Icheka/go-rules-engine/ast"
s := &MyStruct{
Name: "Icheka",
}
ast.Mapify(s)
// map[Name:"Icheka"]
Special thanks to @CacheControl for his work on json-rules-engine which inspired this.