Skip to content

Commit c9b6aab

Browse files
authored
feat(decision): Condition tree eval skeleton (#27)
1 parent 4bc89e0 commit c9b6aab

File tree

11 files changed

+698
-1
lines changed

11 files changed

+698
-1
lines changed

optimizely/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type OptimizelyClient struct {
3030

3131
// IsFeatureEnabled returns true if the feature is enabled for the given user
3232
func (optly *OptimizelyClient) IsFeatureEnabled(featureKey string, userID string, attributes map[string]interface{}) bool {
33-
userContext := entities.UserContext{ID: userID, Attributes: attributes}
33+
userContext := entities.UserContext{ID: userID, Attributes: entities.UserAttributes{Attributes: attributes}}
3434

3535
// @TODO(mng): we should fetch the Feature entity from the config service instead of manually creating it here
3636
featureExperiment := entities.Experiment{}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package evaluator
18+
19+
import (
20+
"github.com/optimizely/go-sdk/optimizely/entities"
21+
)
22+
23+
// AudienceEvaluator evaluates an audience against the given user's attributes
24+
type AudienceEvaluator interface {
25+
Evaluate(audience entities.Audience, user entities.UserContext) bool
26+
}
27+
28+
// TypedAudienceEvaluator evaluates typed audiences
29+
type TypedAudienceEvaluator struct {
30+
conditionTreeEvaluator ConditionTreeEvaluator
31+
}
32+
33+
// NewTypedAudienceEvaluator creates a new instance of the TypedAudienceEvaluator
34+
func NewTypedAudienceEvaluator() *TypedAudienceEvaluator {
35+
conditionTreeEvaluator := NewConditionTreeEvaluator()
36+
return &TypedAudienceEvaluator{
37+
conditionTreeEvaluator: *conditionTreeEvaluator,
38+
}
39+
}
40+
41+
// Evaluate evaluates the typed audience against the given user's attributes
42+
func (a TypedAudienceEvaluator) Evaluate(audience entities.Audience, user entities.UserContext) bool {
43+
return a.conditionTreeEvaluator.Evaluate(audience.ConditionTree, user)
44+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package evaluator
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/optimizely/go-sdk/optimizely/decision/evaluator/matchers"
23+
"github.com/optimizely/go-sdk/optimizely/entities"
24+
)
25+
26+
// MatchType dictates how the condition value will be matched to the corresponding attribute value
27+
type MatchType int
28+
29+
const (
30+
// EXACT match type performs an equality comparison
31+
EXACT MatchType = iota
32+
)
33+
34+
func (m MatchType) String() string {
35+
return [...]string{
36+
"exact",
37+
}[m]
38+
}
39+
40+
// ConditionEvaluator evaluates a condition against the given user's attributes
41+
type ConditionEvaluator interface {
42+
Evaluate(entities.Condition, entities.UserContext) (bool, error)
43+
}
44+
45+
// CustomAttributeConditionEvaluator evaluates conditions with custom attributes
46+
type CustomAttributeConditionEvaluator struct{}
47+
48+
// Evaluate returns true if the given user's attributes match the condition
49+
func (c CustomAttributeConditionEvaluator) Evaluate(condition entities.Condition, user entities.UserContext) (bool, error) {
50+
// We should only be evaluating custom attributes
51+
if condition.Type != customAttributeType {
52+
return false, fmt.Errorf(`Unable to evaluator condition of type "%s"`, condition.Type)
53+
}
54+
55+
var matcher matchers.Matcher
56+
matchType := condition.Match
57+
switch matchType {
58+
case EXACT.String():
59+
matcher = matchers.ExactMatcher{
60+
Condition: condition,
61+
}
62+
}
63+
64+
result, err := matcher.Match(user)
65+
return result, err
66+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package evaluator
18+
19+
import (
20+
"testing"
21+
22+
"github.com/optimizely/go-sdk/optimizely/entities"
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
func TestCustomAttributeConditionEvaluator(t *testing.T) {
27+
conditionEvaluator := CustomAttributeConditionEvaluator{}
28+
condition := entities.Condition{
29+
Match: "exact",
30+
Value: "foo",
31+
Name: "string_foo",
32+
Type: "custom_attribute",
33+
}
34+
35+
// Test condition passes
36+
user := entities.UserContext{
37+
Attributes: entities.UserAttributes{
38+
Attributes: map[string]interface{}{
39+
"string_foo": "foo",
40+
},
41+
},
42+
}
43+
result, _ := conditionEvaluator.Evaluate(condition, user)
44+
assert.Equal(t, result, true)
45+
46+
// Test condition fails
47+
user = entities.UserContext{
48+
Attributes: entities.UserAttributes{
49+
Attributes: map[string]interface{}{
50+
"string_foo": "not_foo",
51+
},
52+
},
53+
}
54+
result, _ = conditionEvaluator.Evaluate(condition, user)
55+
assert.Equal(t, result, false)
56+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package evaluator
18+
19+
import (
20+
"github.com/optimizely/go-sdk/optimizely/entities"
21+
)
22+
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+
26+
const customAttributeType = "custom_attribute"
27+
28+
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"
35+
)
36+
37+
// ConditionTreeEvaluator evaluates a condition tree
38+
type ConditionTreeEvaluator struct {
39+
conditionEvaluatorMap map[string]ConditionEvaluator
40+
}
41+
42+
// NewConditionTreeEvaluator creates a condition tree evaluator with the out-of-the-box condition evaluators
43+
func NewConditionTreeEvaluator() *ConditionTreeEvaluator {
44+
// For now, only one evaluator per attribute type
45+
conditionEvaluatorMap := make(map[string]ConditionEvaluator)
46+
conditionEvaluatorMap[customAttributeType] = CustomAttributeConditionEvaluator{}
47+
return &ConditionTreeEvaluator{
48+
conditionEvaluatorMap: conditionEvaluatorMap,
49+
}
50+
}
51+
52+
// Evaluate returns true if the userAttributes satisfy the given condition tree
53+
func (c ConditionTreeEvaluator) Evaluate(node *entities.ConditionTreeNode, user entities.UserContext) bool {
54+
// This wrapper method converts the conditionEvalResult to a boolean
55+
result := c.evaluate(node, user)
56+
return result == TRUE
57+
}
58+
59+
// 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
63+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package matchers
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/optimizely/go-sdk/optimizely/decision/evaluator/matchers/utils"
23+
"github.com/optimizely/go-sdk/optimizely/entities"
24+
)
25+
26+
// ExactMatcher matches against the "exact" match type
27+
type ExactMatcher struct {
28+
Condition entities.Condition
29+
}
30+
31+
// Match returns true if the user's attribute match the condition's string value
32+
func (m ExactMatcher) Match(user entities.UserContext) (bool, error) {
33+
if stringValue, ok := m.Condition.Value.(string); ok {
34+
attributeValue, err := user.Attributes.GetString(m.Condition.Name)
35+
if err != nil {
36+
return false, err
37+
}
38+
return stringValue == attributeValue, nil
39+
}
40+
41+
if boolValue, ok := m.Condition.Value.(bool); ok {
42+
attributeValue, err := user.Attributes.GetBool(m.Condition.Name)
43+
if err != nil {
44+
return false, err
45+
}
46+
return boolValue == attributeValue, nil
47+
}
48+
49+
if floatValue, ok := utils.ToFloat(m.Condition.Value); ok {
50+
attributeValue, err := user.Attributes.GetFloat(m.Condition.Name)
51+
if err != nil {
52+
return false, err
53+
}
54+
return floatValue == attributeValue, nil
55+
}
56+
57+
return false, fmt.Errorf("audience condition %s evaluated to NULL because the condition value type is not supported", m.Condition.Name)
58+
}

0 commit comments

Comments
 (0)