-
Notifications
You must be signed in to change notification settings - Fork 0
/
evaluator.go
126 lines (105 loc) · 3.14 KB
/
evaluator.go
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package coffeemachine
import (
"fmt"
"github.com/anshal21/coffee-machine/expressions"
"github.com/anshal21/coffee-machine/lib/models"
)
// Evaluator is an interface to evaluate a rule-graph against
// given set of parameter values
// It exposes and Evaluate method that evaluates the rule-graph
type Evaluator interface {
Evaluate(req *RuleEngineRequest) (*RuleEngineResponse, error)
}
// NewEvaluator is a constructor for Evaluator
// It takes a rule-graph as an input and returns an instance of Evaluator
func NewEvaluator(ruleGraph *RuleGraph) Evaluator {
return &evaluator{
ruleGraph: ruleGraph,
}
}
type evaluator struct {
ruleGraph *RuleGraph
}
// Evaluate method evaluates the rule-graph against the rule-graph
// It traverses the graph in the dependency order and evaluates the nodes
// for the expressions associated
// It early stops, i.e doesn't evaluate a node if some of it dependecy evaluates to false
func (e *evaluator) Evaluate(req *RuleEngineRequest) (*RuleEngineResponse, error) {
response := &RuleEngineResponse{}
outCh := make(chan *RuleOutput, 100)
// TODO: this can be improved by pre-computing the execution order using topo-sort
err := e.dfs(req, e.ruleGraph.Root, outCh, &evaluationStats{})
if err != nil {
return nil, err
}
close(outCh)
for ruleOutput := range outCh {
response.Outputs = append(response.Outputs, ruleOutput)
}
return response, nil
}
type evaluationStats struct {
evaluated int
evaluatedTrue int
evaluatedRules []string
}
func (e *evaluator) dfs(req *RuleEngineRequest, node *Node, outCh chan<- *RuleOutput, stats *evaluationStats) error {
res, err := node.Rule.Predicate.Evaluate(&expressions.EvaluationRequest{
Variables: req.Variables,
})
if err != nil {
return err
}
if res.Type != models.DataTypeBool {
return fmt.Errorf("rule %v, does not have a boolean expression", node.Rule.ID)
}
if *res.Value.Bool {
if node.Rule.ID != _rootNodeID {
postEvals, err := e.evaluatePostEvals(req, node.Rule.PostEvals)
if err != nil {
return err
}
postEvals.ID = node.Rule.ID
outCh <- postEvals
}
for _, edge := range node.Relations {
err = e.dfs(req, edge.Destination, outCh, stats)
if err != nil {
return err
}
}
}
return nil
}
func (e *evaluator) evaluatePostEvals(req *RuleEngineRequest, postEvals []*RulePostEval) (*RuleOutput, error) {
ruleOutput := &RuleOutput{
PostEvals: make([]*EvaluationOutput, 0, len(postEvals)),
}
for _, postEval := range postEvals {
switch postEval.Type {
case OutputTypeExpression:
res, err := postEval.Evaluable.Evaluate(&expressions.EvaluationRequest{
Variables: req.Variables,
})
if err != nil {
return nil, err
}
ruleOutput.PostEvals = append(ruleOutput.PostEvals, &EvaluationOutput{
ID: postEval.ID,
Value: res.Value,
Type: res.Type,
})
case OutputTypeConstant:
ruleOutput.PostEvals = append(ruleOutput.PostEvals, &EvaluationOutput{
ID: postEval.ID,
Value: models.Value{
String: &postEval.Const,
},
Type: models.DataTypeString,
})
default:
return nil, fmt.Errorf("invalid output type %v in the rule", postEval.Type)
}
}
return ruleOutput, nil
}