Skip to content

Commit f1d7673

Browse files
author
Michael Ng
authored
feat(decision): Implement the condition tree evaluator. (#29)
1 parent c9b6aab commit f1d7673

File tree

3 files changed

+434
-25
lines changed

3 files changed

+434
-25
lines changed

optimizely/decision/evaluator/condition.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,11 @@ import (
2323
"github.com/optimizely/go-sdk/optimizely/entities"
2424
)
2525

26-
// MatchType dictates how the condition value will be matched to the corresponding attribute value
27-
type MatchType int
28-
2926
const (
3027
// EXACT match type performs an equality comparison
31-
EXACT MatchType = iota
28+
exactMatchType = "exact"
3229
)
3330

34-
func (m MatchType) String() string {
35-
return [...]string{
36-
"exact",
37-
}[m]
38-
}
39-
4031
// ConditionEvaluator evaluates a condition against the given user's attributes
4132
type ConditionEvaluator interface {
4233
Evaluate(entities.Condition, entities.UserContext) (bool, error)
@@ -55,7 +46,7 @@ func (c CustomAttributeConditionEvaluator) Evaluate(condition entities.Condition
5546
var matcher matchers.Matcher
5647
matchType := condition.Match
5748
switch matchType {
58-
case EXACT.String():
49+
case exactMatchType:
5950
matcher = matchers.ExactMatcher{
6051
Condition: condition,
6152
}

optimizely/decision/evaluator/condition_tree.go

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,15 @@ import (
2020
"github.com/optimizely/go-sdk/optimizely/entities"
2121
)
2222

23-
// conditionEvalResult is the result of evaluating a Condition, which can be true/false or null if the condition could not be evaluated
24-
type conditionEvalResult string
25-
2623
const customAttributeType = "custom_attribute"
2724

2825
const (
29-
// TRUE means the condition passes
30-
TRUE conditionEvalResult = "TRUE"
31-
// FALSE means the condition does not pass
32-
FALSE conditionEvalResult = "FALSE"
33-
// NULL means the condition could not be evaluated
34-
NULL conditionEvalResult = "NULL"
26+
// "and" operator returns true if all conditions evaluate to true
27+
andOperator = "and"
28+
// "not" operator negates the result of the given condition
29+
notOperator = "not"
30+
// "or" operator returns true if any of the conditions evaluate to true
31+
orOperator = "or"
3532
)
3633

3734
// ConditionTreeEvaluator evaluates a condition tree
@@ -52,12 +49,86 @@ func NewConditionTreeEvaluator() *ConditionTreeEvaluator {
5249
// Evaluate returns true if the userAttributes satisfy the given condition tree
5350
func (c ConditionTreeEvaluator) Evaluate(node *entities.ConditionTreeNode, user entities.UserContext) bool {
5451
// This wrapper method converts the conditionEvalResult to a boolean
55-
result := c.evaluate(node, user)
56-
return result == TRUE
52+
result, _ := c.evaluate(node, user)
53+
return result == true
5754
}
5855

5956
// Helper method to recursively evaluate a condition tree
60-
func (c ConditionTreeEvaluator) evaluate(node *entities.ConditionTreeNode, user entities.UserContext) conditionEvalResult {
61-
// @TODO(mng): Implement tree evaluator with and/or/not operators
62-
return TRUE
57+
// Returns the result of the evaluation and whether the evaluation of the condition is valid or not (to handle null bubbling)
58+
func (c ConditionTreeEvaluator) evaluate(node *entities.ConditionTreeNode, user entities.UserContext) (evalResult bool, isValid bool) {
59+
operator := node.Operator
60+
if operator != "" {
61+
switch operator {
62+
case andOperator:
63+
return c.evaluateAnd(node.Nodes, user)
64+
case notOperator:
65+
return c.evaluateNot(node.Nodes, user)
66+
case orOperator:
67+
fallthrough
68+
default:
69+
return c.evaluateOr(node.Nodes, user)
70+
}
71+
}
72+
73+
conditionEvaluator, ok := c.conditionEvaluatorMap[node.Condition.Type]
74+
if !ok {
75+
// TODO(mng): log error
76+
// Result is invalid
77+
return false, false
78+
}
79+
result, err := conditionEvaluator.Evaluate(node.Condition, user)
80+
if err != nil {
81+
// Result is invalid
82+
return false, false
83+
}
84+
return result, true
85+
}
86+
87+
func (c ConditionTreeEvaluator) evaluateAnd(nodes []*entities.ConditionTreeNode, user entities.UserContext) (evalResult bool, isValid bool) {
88+
sawInvalid := false
89+
for _, node := range nodes {
90+
result, isValid := c.evaluate(node, user)
91+
if !isValid {
92+
return false, isValid
93+
} else if result == false {
94+
return result, isValid
95+
}
96+
}
97+
98+
if sawInvalid {
99+
// bubble up the invalid result
100+
return false, false
101+
}
102+
103+
return true, true
104+
}
105+
106+
func (c ConditionTreeEvaluator) evaluateNot(nodes []*entities.ConditionTreeNode, user entities.UserContext) (evalResult bool, isValid bool) {
107+
if len(nodes) > 0 {
108+
result, isValid := c.evaluate(nodes[0], user)
109+
if !isValid {
110+
return false, false
111+
}
112+
return !result, isValid
113+
}
114+
return false, false
115+
}
116+
117+
func (c ConditionTreeEvaluator) evaluateOr(nodes []*entities.ConditionTreeNode, user entities.UserContext) (evalResult bool, isValid bool) {
118+
sawInvalid := false
119+
for _, node := range nodes {
120+
result, isValid := c.evaluate(node, user)
121+
if !isValid {
122+
sawInvalid = true
123+
} else if result == true {
124+
return result, isValid
125+
}
126+
}
127+
128+
if sawInvalid {
129+
// bubble up the invalid result
130+
return false, false
131+
}
132+
133+
return false, true
63134
}

0 commit comments

Comments
 (0)