├── go.mod ├── go.sum ├── .gitignore ├── src ├── ast │ ├── ast_test.go │ └── ast.go └── evaluator │ ├── operators_test.go │ ├── evaluator.go │ ├── operators.go │ └── evaluator_test.go ├── LICENSE ├── rule_engine └── rule_engine.go ├── README.md └── gopher-ladder.svg /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Icheka/go-rules-engine 2 | 3 | go 1.18 4 | 5 | require github.com/fatih/structs v1.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 2 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | main.go -------------------------------------------------------------------------------- /src/ast/ast_test.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestMapify(t *testing.T) { 9 | // Mapify converts a struct to a map[string]interface{} 10 | type S struct { 11 | Key string 12 | } 13 | s := S{ 14 | Key: "value", 15 | } 16 | m := Mapify(s) 17 | 18 | if m["Key"] == nil || m["Key"] != "value" { 19 | t.Fatalf("expected m[\"Key\"] to be %s, got %s", s.Key, m["Key"]) 20 | } 21 | } 22 | 23 | func TestParseJSON(t *testing.T) { 24 | j := `{ 25 | "condition": { 26 | "any": [{ 27 | "identifier": "myVar", 28 | "operator": "eq", 29 | "value": "hello world" 30 | }] 31 | }, 32 | "event": { 33 | "type": "result", 34 | "payload": { 35 | "data": { 36 | "say": "Hello World!" 37 | } 38 | } 39 | } 40 | }` 41 | 42 | rule := ParseJSON(j) 43 | 44 | if fmt.Sprintf("%T", rule) != "*ast.Rule" { 45 | t.Fatalf("expected rule to be *ast.Rule, got %T", rule) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/evaluator/operators_test.go: -------------------------------------------------------------------------------- 1 | package evaluator 2 | 3 | import "testing" 4 | 5 | func TestEvaluateOperator(t *testing.T) { 6 | tests := []struct { 7 | identifier interface{} 8 | value interface{} 9 | operator string 10 | expected bool 11 | }{ 12 | {"hi", "hi", "eq", true}, 13 | {"hi", "hi", "=", true}, 14 | {"hi", "his", "=", false}, 15 | {"hi", 4, "=", false}, 16 | {4, 4, "=", true}, 17 | 18 | {4, 4, "!=", false}, 19 | {4, 5, "neq", true}, 20 | 21 | {4, 5, "<", true}, 22 | {6, 5, "lt", false}, 23 | 24 | {4, 5, ">", false}, 25 | {6, 5, "gt", true}, 26 | 27 | {4, 5, ">=", false}, 28 | {6, 5, "gte", true}, 29 | {5, 5, "gte", true}, 30 | 31 | {4, 5, "<=", true}, 32 | {6, 5, "lte", false}, 33 | {5, 5, "lte", true}, 34 | } 35 | 36 | for i, tt := range tests { 37 | ok, err := EvaluateOperator(tt.identifier, tt.value, tt.operator) 38 | if err != nil { 39 | t.Errorf("tests[%d] - unexpected error (%s)", i, err) 40 | } 41 | if ok != tt.expected { 42 | t.Errorf("tests[%d] - expected EvaluateOperator to be %t, got=%t", i, tt.expected, ok) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Icheka Ozuru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ast/ast.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/fatih/structs" 7 | ) 8 | 9 | // Conditionals are the basic units of rules 10 | type Conditional struct { 11 | Fact string `json:"identifier"` 12 | Operator string `json:"operator"` 13 | Value interface{} `json:"value"` 14 | } 15 | 16 | // A Condition is a group of conditionals within a binding context 17 | // that determines how the group will be evaluated. 18 | type Condition struct { 19 | Any []Conditional `json:"any"` 20 | All []Conditional `json:"all"` 21 | } 22 | 23 | // Fired when a identifier matches a rule 24 | type Event struct { 25 | Type string `json:"type"` 26 | Payload interface{} `json:"payload"` 27 | } 28 | 29 | type Rule struct { 30 | Condition Condition `json:"condition"` 31 | Event Event `json:"event"` 32 | } 33 | 34 | // parse JSON string as Rule 35 | func ParseJSON(j string) *Rule { 36 | var rule *Rule 37 | if err := json.Unmarshal([]byte(j), &rule); err != nil { 38 | panic("expected valid JSON") 39 | } 40 | return rule 41 | } 42 | 43 | // Convert struct to map. Can be used to generate a identifier (which has to be of type map[string]interface{}) from a struct. 44 | func Mapify(s interface{}) map[string]interface{} { 45 | return structs.Map(s) 46 | } 47 | -------------------------------------------------------------------------------- /rule_engine/rule_engine.go: -------------------------------------------------------------------------------- 1 | package ruleEngine 2 | 3 | import ( 4 | "github.com/Icheka/go-rules-engine/src/ast" 5 | "github.com/Icheka/go-rules-engine/src/evaluator" 6 | ) 7 | 8 | type results []ast.Event 9 | 10 | type EvaluatorOptions struct { 11 | AllowUndefinedVars bool 12 | } 13 | 14 | var defaultOptions = &EvaluatorOptions{ 15 | AllowUndefinedVars: false, 16 | } 17 | 18 | type RuleEngine struct { 19 | EvaluatorOptions 20 | Rules []string 21 | Results results 22 | } 23 | 24 | func (re *RuleEngine) EvaluateStruct(jsonText *ast.Rule, identifier evaluator.Data) bool { 25 | return evaluator.EvaluateRule(jsonText, identifier, &evaluator.Options{ 26 | AllowUndefinedVars: re.AllowUndefinedVars, 27 | }) 28 | } 29 | 30 | func (re *RuleEngine) AddRule(rule string) *RuleEngine { 31 | re.Rules = append(re.Rules, rule) 32 | return re 33 | } 34 | 35 | func (re *RuleEngine) AddRules(rules ...string) *RuleEngine { 36 | re.Rules = append(re.Rules, rules...) 37 | return re 38 | } 39 | 40 | func (re *RuleEngine) EvaluateRules(data evaluator.Data) results { 41 | for _, j := range re.Rules { 42 | rule := ast.ParseJSON(j) 43 | 44 | if re.EvaluateStruct(rule, data) { 45 | re.Results = append(re.Results, rule.Event) 46 | } 47 | } 48 | return re.Results 49 | } 50 | 51 | func New(options *EvaluatorOptions) *RuleEngine { 52 | opts := options 53 | if opts == nil { 54 | opts = defaultOptions 55 | } 56 | 57 | return &RuleEngine{ 58 | EvaluatorOptions: *opts, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/evaluator/evaluator.go: -------------------------------------------------------------------------------- 1 | package evaluator 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Icheka/go-rules-engine/src/ast" 7 | ) 8 | 9 | type Data map[string]interface{} 10 | type Options struct { 11 | AllowUndefinedVars bool 12 | } 13 | 14 | var options *Options 15 | 16 | func EvaluateConditional(conditional *ast.Conditional, identifier interface{}) bool { 17 | ok, err := EvaluateOperator(identifier, conditional.Value, conditional.Operator) 18 | if err != nil { 19 | panic(err) 20 | } 21 | return ok 22 | } 23 | 24 | func GetFactValue(condition *ast.Conditional, data Data) interface{} { 25 | value := data[condition.Fact] 26 | 27 | if value == nil { 28 | if options.AllowUndefinedVars { 29 | return false 30 | } 31 | panic(fmt.Sprintf("value for identifier %s not found", condition.Fact)) 32 | } 33 | 34 | return value 35 | } 36 | 37 | func EvaluateAllCondition(conditions *[]ast.Conditional, data Data) bool { 38 | isFalse := false 39 | 40 | for _, condition := range *conditions { 41 | value := GetFactValue(&condition, data) 42 | if !EvaluateConditional(&condition, value) { 43 | isFalse = true 44 | } 45 | 46 | if isFalse { 47 | return false 48 | } 49 | } 50 | 51 | return true 52 | } 53 | 54 | func EvaluateAnyCondition(conditions *[]ast.Conditional, data Data) bool { 55 | for _, condition := range *conditions { 56 | value := GetFactValue(&condition, data) 57 | if EvaluateConditional(&condition, value) { 58 | return true 59 | } 60 | } 61 | 62 | return false 63 | } 64 | 65 | func EvaluateCondition(condition *[]ast.Conditional, kind string, data Data) bool { 66 | switch kind { 67 | case "all": 68 | return EvaluateAllCondition(condition, data) 69 | case "any": 70 | return EvaluateAnyCondition(condition, data) 71 | default: 72 | panic(fmt.Sprintf("condition type %s is invalid", kind)) 73 | } 74 | } 75 | 76 | func EvaluateRule(rule *ast.Rule, data Data, opts *Options) bool { 77 | options = opts 78 | any, all := false, false 79 | 80 | if len(rule.Condition.Any) == 0 { 81 | any = true 82 | } else { 83 | any = EvaluateCondition(&rule.Condition.Any, "any", data) 84 | } 85 | if len(rule.Condition.All) == 0 { 86 | all = true 87 | } else { 88 | all = EvaluateCondition(&rule.Condition.All, "all", data) 89 | } 90 | 91 | return any && all 92 | } 93 | -------------------------------------------------------------------------------- /src/evaluator/operators.go: -------------------------------------------------------------------------------- 1 | package evaluator 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func EvaluateOperator(identifier, value interface{}, operator string) (bool, error) { 8 | switch operator { 9 | case "=": 10 | fallthrough 11 | case "eq": 12 | factNum, err := assertIsNumber(identifier) 13 | if err == nil { 14 | valueNum, err := assertIsNumber(value) 15 | if err != nil { 16 | return false, err 17 | } 18 | return factNum == valueNum, nil 19 | } 20 | 21 | return identifier == value, nil 22 | 23 | case "!=": 24 | fallthrough 25 | case "neq": 26 | factNum, err := assertIsNumber(identifier) 27 | if err == nil { 28 | valueNum, err := assertIsNumber(value) 29 | if err != nil { 30 | return false, err 31 | } 32 | return factNum != valueNum, nil 33 | } 34 | 35 | return identifier != value, nil 36 | 37 | case "<": 38 | fallthrough 39 | case "lt": 40 | factNum, err := assertIsNumber(identifier) 41 | if err != nil { 42 | return false, err 43 | } 44 | valueNum, err := assertIsNumber(value) 45 | if err != nil { 46 | return false, err 47 | } 48 | 49 | return factNum < valueNum, nil 50 | 51 | case ">": 52 | fallthrough 53 | case "gt": 54 | factNum, err := assertIsNumber(identifier) 55 | if err != nil { 56 | return false, err 57 | } 58 | valueNum, err := assertIsNumber(value) 59 | if err != nil { 60 | return false, err 61 | } 62 | 63 | return factNum > valueNum, nil 64 | 65 | case ">=": 66 | fallthrough 67 | case "gte": 68 | factNum, err := assertIsNumber(identifier) 69 | if err != nil { 70 | return false, err 71 | } 72 | valueNum, err := assertIsNumber(value) 73 | if err != nil { 74 | return false, err 75 | } 76 | 77 | return factNum >= valueNum, nil 78 | 79 | case "<=": 80 | fallthrough 81 | case "lte": 82 | factNum, err := assertIsNumber(identifier) 83 | if err != nil { 84 | return false, err 85 | } 86 | valueNum, err := assertIsNumber(value) 87 | if err != nil { 88 | return false, err 89 | } 90 | 91 | return factNum <= valueNum, nil 92 | 93 | default: 94 | return false, fmt.Errorf("unrecognised operator %s", operator) 95 | } 96 | } 97 | 98 | func assertIsNumber(v interface{}) (float64, error) { 99 | isFloat := true 100 | var d int 101 | var f float64 102 | 103 | d, ok := v.(int) 104 | if !ok { 105 | f, ok = v.(float64) 106 | if !ok { 107 | return 0, fmt.Errorf("%s is not a number", v) 108 | } 109 | } else { 110 | isFloat = false 111 | } 112 | 113 | if isFloat { 114 | return f, nil 115 | } 116 | return float64(d), nil 117 | } 118 | -------------------------------------------------------------------------------- /src/evaluator/evaluator_test.go: -------------------------------------------------------------------------------- 1 | package evaluator 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Icheka/go-rules-engine/src/ast" 7 | ) 8 | 9 | func TestEvaluateConditional(t *testing.T) { 10 | tests := []struct { 11 | conditional *ast.Conditional 12 | identifier interface{} 13 | expected bool 14 | }{ 15 | {&ast.Conditional{ 16 | Fact: "name", 17 | Operator: "eq", 18 | Value: "Icheka", 19 | }, 20 | "Icheka", 21 | true, 22 | }, 23 | {&ast.Conditional{ 24 | Fact: "name", 25 | Operator: "eq", 26 | Value: "Icheka", 27 | }, 28 | "Ronie", 29 | false, 30 | }, 31 | } 32 | 33 | for i, tt := range tests { 34 | if ok := EvaluateConditional(tt.conditional, tt.identifier); ok != tt.expected { 35 | t.Errorf("tests[%d] - expected EvaluateConditional to return %t, got=%t", i, tt.expected, ok) 36 | } 37 | } 38 | } 39 | 40 | func TestEvaluateAllCondition(t *testing.T) { 41 | tests := []struct { 42 | payload struct { 43 | conditions []ast.Conditional 44 | identifier Data 45 | } 46 | expected bool 47 | }{ 48 | { 49 | payload: struct { 50 | conditions []ast.Conditional 51 | identifier Data 52 | }{ 53 | conditions: []ast.Conditional{ 54 | { 55 | Fact: "planet", 56 | Operator: "eq", 57 | Value: "Neptune", 58 | }, 59 | { 60 | Fact: "colour", 61 | Operator: "eq", 62 | Value: "black", 63 | }, 64 | }, 65 | identifier: Data{ 66 | "planet": "Neptune", 67 | "colour": "black", 68 | }, 69 | }, 70 | expected: true, 71 | }, 72 | { 73 | payload: struct { 74 | conditions []ast.Conditional 75 | identifier Data 76 | }{ 77 | conditions: []ast.Conditional{ 78 | { 79 | Fact: "planet", 80 | Operator: "eq", 81 | Value: "Saturn", 82 | }, 83 | { 84 | Fact: "colour", 85 | Operator: "eq", 86 | Value: "black", 87 | }, 88 | }, 89 | identifier: Data{ 90 | "planet": "Neptune", 91 | "colour": "black", 92 | }, 93 | }, 94 | expected: false, 95 | }, 96 | } 97 | 98 | for i, tt := range tests { 99 | if ok := EvaluateAllCondition(&tt.payload.conditions, tt.payload.identifier); ok != tt.expected { 100 | t.Errorf("tests[%d] - expected EvaluateAllCondition to be %t, got=%t", i, tt.expected, ok) 101 | } 102 | } 103 | } 104 | 105 | func TestEvaluateAnyCondition(t *testing.T) { 106 | tests := []struct { 107 | payload struct { 108 | conditions []ast.Conditional 109 | identifier Data 110 | } 111 | expected bool 112 | }{ 113 | { 114 | payload: struct { 115 | conditions []ast.Conditional 116 | identifier Data 117 | }{ 118 | conditions: []ast.Conditional{ 119 | { 120 | Fact: "planet", 121 | Operator: "eq", 122 | Value: "Neptune", 123 | }, 124 | { 125 | Fact: "colour", 126 | Operator: "eq", 127 | Value: "black", 128 | }, 129 | }, 130 | identifier: Data{ 131 | "planet": "Neptune", 132 | "colour": "black", 133 | }, 134 | }, 135 | expected: true, 136 | }, 137 | { 138 | payload: struct { 139 | conditions []ast.Conditional 140 | identifier Data 141 | }{ 142 | conditions: []ast.Conditional{ 143 | { 144 | Fact: "planet", 145 | Operator: "eq", 146 | Value: "Saturn", 147 | }, 148 | { 149 | Fact: "colour", 150 | Operator: "eq", 151 | Value: "black", 152 | }, 153 | }, 154 | identifier: Data{ 155 | "planet": "Neptune", 156 | "colour": "black", 157 | }, 158 | }, 159 | expected: true, 160 | }, 161 | { 162 | payload: struct { 163 | conditions []ast.Conditional 164 | identifier Data 165 | }{ 166 | conditions: []ast.Conditional{ 167 | { 168 | Fact: "planet", 169 | Operator: "eq", 170 | Value: "Saturn", 171 | }, 172 | { 173 | Fact: "colour", 174 | Operator: "eq", 175 | Value: "white", 176 | }, 177 | }, 178 | identifier: Data{ 179 | "planet": "Neptune", 180 | "colour": "black", 181 | }, 182 | }, 183 | expected: false, 184 | }, 185 | } 186 | 187 | for i, tt := range tests { 188 | if ok := EvaluateAnyCondition(&tt.payload.conditions, tt.payload.identifier); ok != tt.expected { 189 | t.Errorf("tests[%d] - expected EvaluateAnyCondition to be %t, got=%t", i, tt.expected, ok) 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![The right way to Go](https://raw.githubusercontent.com/Icheka/go-rules-engine/ef41df8a2a2effdb340fc2d352d673e9ca82ad50/gopher-ladder.svg "The right way to Go") 2 | 3 | ### **"The right way to Go"** 4 | 5 |
6 | 7 | # Go-Rules-Engine 8 | 9 | ### A JSON-based rule engine, written in Go. 10 | 11 | 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). 12 | 13 | ## Features 14 | 15 | - **Deterministic**: uses JSON as an AST (Abstract Syntax Tree) from which to draw inferences and publish reactive events 16 | - **Supports "any" and "all" context operators** 17 | - **Blazing fast** 18 | - **Secure and sandboxed** - JSON AST is never evaluated 19 | - **Easily extensible** - Perfect for building larger expert systems via composition 20 | - **Easily modifiable** - JSON AST can be modified by anybody -- no technical expertise required 21 | 22 | ## Installation 23 | 24 | Works best with Go >=1.8. 25 | 26 | ```bash 27 | go get github.com/icheka/go-rules-engine 28 | ``` 29 | 30 | ## Synopsis 31 | 32 | 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. 33 | 34 | Go-Rules-Engine 35 | | 36 | ----------- 37 | | | 38 | Conditions Event 39 | 40 | As an example, a simple rule for a fictional discount engine might be stated as: 41 | "Offer a 10% discount if the customer buys 2 apples". Writing a Rule for this discount is easy enough: 42 | 43 | ### Conditions 44 | 45 | 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. 46 | 47 | 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. 48 | 49 | The condition of the discount above will look like: 50 | 51 | ```json 52 | { 53 | "condition": { 54 | "all": [ 55 | { 56 | "identifier": "applesCount", 57 | "operator": "=", 58 | "value": 2 59 | } 60 | ] 61 | } 62 | } 63 | ``` 64 | 65 | ### Events 66 | 67 | 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. 68 | 69 | The event for the discount above could look like: 70 | 71 | ```json 72 | { 73 | ... 74 | "event": { 75 | "type": "discount", 76 | "payload": { 77 | "percentage": 10, 78 | "item": "apple" 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | Thus, the discount Rule can be expressed as: 85 | 86 | ```json 87 | { 88 | "condition": { 89 | "all": [ 90 | { 91 | "identifier": "applesCount", 92 | "operator": "=", 93 | "value": 2 94 | } 95 | ] 96 | }, 97 | "event": { 98 | "type": "discount", 99 | "payload": { 100 | "percentage": 10, 101 | "item": "apple" 102 | } 103 | } 104 | } 105 | ``` 106 | 107 | ## Processing Rules 108 | 109 | Following the example above, assuming that the discount Rule is stored in the file system, we can process the Rule like so: 110 | 111 | ```go 112 | package main 113 | 114 | import ( 115 | "fmt" 116 | "os" 117 | 118 | ruleEngine "github.com/Icheka/go-rules-engine/rule_engine" 119 | ) 120 | 121 | func main() { 122 | // read discount rule 123 | jsonBytes, err := os.ReadFile("apple-discount-rule.json") 124 | if err != nil { 125 | panic(err) 126 | } 127 | 128 | // a map[string]interface{} representing a customer's cart at checkout 129 | // cart contains a key (applesCount) matching the `identifier` in our rule's condition 130 | cart := map[string]interface{}{ 131 | "applesCount": 3, 132 | "orangesCount": 5, 133 | "cookiesCount": 1 134 | } 135 | 136 | // create a new Rule Engine... 137 | engine := ruleEngine.New(nil) 138 | // ... and add the discount rule 139 | engine.AddRule(string(jsonByres)) 140 | // then process it 141 | fmt.Printf("%+v", engine.EvaluateRules(cart)) 142 | // [{Type:discount Payload:map[item:apple percentage:10]}] 143 | } 144 | ``` 145 | 146 | ## More Complex Rules 147 | 148 | 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: 149 | 150 | 1. Player A has no cards left 151 | 2. Player B has up to 20 (i.e greater or equal to 20) cards left 152 | 153 | These can be expressed aptly using `any`: 154 | 155 | ```json 156 | { 157 | "condition": { 158 | "any": [ 159 | { 160 | "identifier": "playerACards", 161 | "operator": "=", 162 | "value": 0 163 | }, 164 | { 165 | "identifier": "playerBCards", 166 | "operator": ">=", 167 | "value": 20 168 | } 169 | ] 170 | }, 171 | "event": { 172 | "type": "win" 173 | } 174 | } 175 | ``` 176 | 177 | ```go 178 | // [{Type:win Payload:}] 179 | ``` 180 | 181 | Both `event.type` and `event.payload` are optional and entirely up to the rule creator to specify, provided they are valid JSON structures. 182 | 183 | ## Configuring Go-Rules-Engine 184 | 185 | By default, the Rules Engine will panic if it is unable to find the value referenced by `identifier`: 186 | 187 | ```go 188 | // rule 189 | { 190 | "condition": { 191 | "any": [ 192 | { 193 | "identifier": "undefinedProperty", 194 | "operator": "=", 195 | "value": 0 196 | }, 197 | { 198 | "identifier": "playerBCards", 199 | "operator": ">=", 200 | "value": [20] 201 | } 202 | ] 203 | }, 204 | "event": { 205 | "type": "win" 206 | } 207 | } 208 | 209 | game := map[string]interface{}{ 210 | "playerACards": 2, 211 | "playerBCards": 20, 212 | } 213 | 214 | engine := ruleEngine.New(nil) 215 | engine.AddRule(string(rule)) 216 | fmt.Printf("%+v", engine.EvaluateRules(game)) 217 | // this will panic "value for identifier undefinedProperty not found" because the "undefinedProperty" identifier was not found in the game map. 218 | ``` 219 | 220 | If this is not the behaviour you want, you can switch this check off by passing an `options` struct to the `ruleEngine.New` constructor: 221 | 222 | ```go 223 | ... 224 | engine := ruleEngine.New(&ruleEngine.EvaluatorOptions{ 225 | AllowUndefinedVars: true, 226 | }) 227 | ... 228 | ``` 229 | 230 | Now, when Rules Engine encounters an undefined property, it will evaluate that statement to false and continue processing the rule. 231 | 232 | ## Operators 233 | 234 | The following operators are available in Go-Rules-Engine: 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 249 | 252 | 255 | 256 | 257 | 260 | 263 | 266 | 267 | 268 | 271 | 274 | 277 | 278 | 279 | 282 | 285 | 288 | 289 | 290 | 293 | 296 | 299 | 300 | 301 | 304 | 307 | 310 | 311 | 312 |
OperatorAliasDescription
247 | = 248 | 250 | eq 251 | 253 | Equals (e.g 3 equals 3) 254 |
258 | != 259 | 261 | neq 262 | 264 | Is not equal (e.g 3 is not equal to 4) 265 |
269 | < 270 | 272 | lt 273 | 275 | Is less than (e.g 3 is less than 4) 276 |
280 | > 281 | 283 | gt 284 | 286 | Is greater than (e.g 5 is greater than 4) 287 |
291 | <= 292 | 294 | lte 295 | 297 | Is less than or equal (e.g 5 is less than or equal to 6) 298 |
302 | >= 303 | 305 | gte 306 | 308 | Is greater than or equal (e.g 5 is greater than or equal to 3) 309 |
313 | 314 | The following operators will be added in future: 315 | 316 | - Array contains (contains) 317 | - Array does not contain (!contains) 318 | - Support for adding custom operators 319 | 320 | ## Converting Structs to Maps 321 | 322 | 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: 323 | 324 | ```go 325 | import "github.com/Icheka/go-rules-engine/ast" 326 | 327 | s := &MyStruct{ 328 | Name: "Icheka", 329 | } 330 | ast.Mapify(s) 331 | // map[Name:"Icheka"] 332 | ``` 333 | 334 | ## Credits 335 | 336 | Special thanks to [@CacheControl](https://github.com/CacheControl) for his work on [json-rules-engine](https://github.com/CacheControl/json-rules-engine) which inspired this. 337 | -------------------------------------------------------------------------------- /gopher-ladder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | --------------------------------------------------------------------------------