Skip to content

Commit a65b933

Browse files
pawels-optimizelyMike Davisthomaszurkan-optimizely
authored
feat: Semantic Versioning implementation (#289)
* feat: added semver support to our existing matchers * fix linters * fix linters * added more changes * consolidate all the semver code * added ge and le evaluators * simplify tests * Update pkg/decision/evaluator/matchers/semver_test.go Co-authored-by: Mike Davis <[email protected]> * cleaning up * addressing nit comments * addressing PR comments * increase coverage * increase coverage * corrected tests * increased coverage * increase coverage * small improvement in the coverage * add a few more tests and fix split when not build or release * fix merge errors * slight refactor for lint * fixing after a refactor right in the middle of doing this pr. * fix lint error Co-authored-by: Mike Davis <[email protected]> Co-authored-by: Tom Zurkan <[email protected]>
1 parent 9c75529 commit a65b933

File tree

10 files changed

+967
-1
lines changed

10 files changed

+967
-1
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ require (
1212
github.com/twmb/murmur3 v1.0.0
1313
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
1414
)
15+
1516
// Work around issue with git.apache.org/thrift.git
1617
replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0

pkg/decision/evaluator/condition_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,69 @@ func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorWithUnknownTyp
136136
s.mockLogger.AssertExpectations(s.T())
137137
}
138138

139+
func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemver() {
140+
conditionEvaluator := CustomAttributeConditionEvaluator{}
141+
condition := entities.Condition{
142+
Match: "semver_ge",
143+
Value: "2.9",
144+
Name: "string_foo",
145+
Type: "custom_attribute",
146+
}
147+
148+
// Test condition passes
149+
user := entities.UserContext{
150+
Attributes: map[string]interface{}{
151+
"string_foo": "2.9.1",
152+
},
153+
}
154+
155+
condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{})
156+
result, _ := conditionEvaluator.Evaluate(condition, condTreeParams)
157+
s.Equal( result, true)
158+
}
159+
160+
func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemverBeta() {
161+
conditionEvaluator := CustomAttributeConditionEvaluator{}
162+
condition := entities.Condition{
163+
Match: "semver_ge",
164+
Value: "3.7.0",
165+
Name: "string_foo",
166+
Type: "custom_attribute",
167+
}
168+
169+
// Test condition passes
170+
user := entities.UserContext{
171+
Attributes: map[string]interface{}{
172+
"string_foo": "3.7.1-beta",
173+
},
174+
}
175+
176+
condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{})
177+
result, _ := conditionEvaluator.Evaluate(condition, condTreeParams)
178+
s.Equal(true, result)
179+
}
180+
181+
func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemverInvalid() {
182+
conditionEvaluator := CustomAttributeConditionEvaluator{}
183+
condition := entities.Condition{
184+
Match: "semver_ge",
185+
Value: "3.7.0",
186+
Name: "string_foo",
187+
Type: "custom_attribute",
188+
}
189+
190+
// Test condition passes
191+
user := entities.UserContext{
192+
Attributes: map[string]interface{}{
193+
"string_foo": "3.7.2.2",
194+
},
195+
}
196+
197+
condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{})
198+
_, err := conditionEvaluator.Evaluate(condition, condTreeParams)
199+
s.NotNil(err)
200+
}
201+
139202
func TestConditionTestSuite(t *testing.T) {
140203
suite.Run(t, new(ConditionTestSuite))
141204
}

pkg/decision/evaluator/matchers/ge.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/****************************************************************************
2+
* Copyright 2020, 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+
package matchers
19+
20+
import (
21+
"fmt"
22+
"github.com/optimizely/go-sdk/pkg/logging"
23+
24+
"github.com/optimizely/go-sdk/pkg/decision/evaluator/matchers/utils"
25+
"github.com/optimizely/go-sdk/pkg/entities"
26+
)
27+
28+
// GeMatcher matches against the "ge" match type
29+
func GeMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) {
30+
31+
if floatValue, ok := utils.ToFloat(condition.Value); ok {
32+
attributeValue, err := user.GetFloatAttribute(condition.Name)
33+
if err != nil {
34+
return false, err
35+
}
36+
return floatValue <= attributeValue, nil
37+
}
38+
39+
return false, fmt.Errorf("audience condition %s evaluated to NULL because the condition value type is not supported", condition.Name)
40+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/****************************************************************************
2+
* Copyright 2020, 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+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
24+
"github.com/optimizely/go-sdk/pkg/entities"
25+
)
26+
27+
var geMatcher, _ = Get(GeMatchType)
28+
29+
func TestGeMatcherInt(t *testing.T) {
30+
condition := entities.Condition{
31+
Match: "ge",
32+
Value: 42,
33+
Name: "int_42",
34+
}
35+
36+
// Test match - same type
37+
user := entities.UserContext{
38+
Attributes: map[string]interface{}{
39+
"int_42": 43,
40+
},
41+
}
42+
result, err := geMatcher(condition, user, nil)
43+
assert.NoError(t, err)
44+
assert.True(t, result)
45+
46+
// Test match int to float
47+
user = entities.UserContext{
48+
Attributes: map[string]interface{}{
49+
"int_42": 42.9999,
50+
},
51+
}
52+
53+
result, err = geMatcher(condition, user, nil)
54+
assert.NoError(t, err)
55+
assert.True(t, result)
56+
57+
// Test match: 42.9999 converts to 42
58+
user = entities.UserContext{
59+
Attributes: map[string]interface{}{
60+
"int_42": 42.00000,
61+
},
62+
}
63+
64+
result, err = geMatcher(condition, user, nil)
65+
assert.NoError(t, err)
66+
assert.True(t, result)
67+
68+
// Test no match
69+
user = entities.UserContext{
70+
Attributes: map[string]interface{}{
71+
"int_42": 41,
72+
},
73+
}
74+
75+
result, err = geMatcher(condition, user, nil)
76+
assert.NoError(t, err)
77+
assert.False(t, result)
78+
79+
// Test attribute not found
80+
user = entities.UserContext{
81+
Attributes: map[string]interface{}{
82+
"int_43": 42,
83+
},
84+
}
85+
86+
_, err = geMatcher(condition, user, nil)
87+
assert.Error(t, err)
88+
89+
// Test wrong int
90+
user = entities.UserContext{
91+
Attributes: map[string]interface{}{
92+
"int_43": "some_string",
93+
},
94+
}
95+
96+
_, err = geMatcher(condition, user, nil)
97+
assert.Error(t, err)
98+
99+
// Test bad condition
100+
condition = entities.Condition{
101+
Match: "ge",
102+
Value: "123",
103+
Name: "int_42",
104+
}
105+
user = entities.UserContext{
106+
Attributes: map[string]interface{}{
107+
"int_43": "some_string",
108+
},
109+
}
110+
111+
_, err = geMatcher(condition, user, nil)
112+
assert.Error(t, err)
113+
}
114+
115+
func TestGeMatcherFloat(t *testing.T) {
116+
condition := entities.Condition{
117+
Match: "ge",
118+
Value: 4.2,
119+
Name: "float_4_2",
120+
}
121+
122+
// Test match float to int
123+
user := entities.UserContext{
124+
Attributes: map[string]interface{}{
125+
"float_4_2": 5,
126+
},
127+
}
128+
result, err := geMatcher(condition, user, nil)
129+
assert.NoError(t, err)
130+
assert.True(t, result)
131+
132+
// Test match
133+
user = entities.UserContext{
134+
Attributes: map[string]interface{}{
135+
"float_4_2": 4.29999,
136+
},
137+
}
138+
result, err = geMatcher(condition, user, nil)
139+
assert.NoError(t, err)
140+
assert.True(t, result)
141+
142+
// Test match
143+
user = entities.UserContext{
144+
Attributes: map[string]interface{}{
145+
"float_4_2": 4.2,
146+
},
147+
}
148+
149+
result, err = geMatcher(condition, user, nil)
150+
assert.NoError(t, err)
151+
assert.True(t, result)
152+
153+
// Test attribute not found
154+
user = entities.UserContext{
155+
Attributes: map[string]interface{}{
156+
"float_4_3": 4.2,
157+
},
158+
}
159+
160+
_, err = geMatcher(condition, user, nil)
161+
assert.Error(t, err)
162+
}

pkg/decision/evaluator/matchers/le.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/****************************************************************************
2+
* Copyright 2020, 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+
package matchers
19+
20+
import (
21+
"fmt"
22+
"github.com/optimizely/go-sdk/pkg/logging"
23+
24+
"github.com/optimizely/go-sdk/pkg/decision/evaluator/matchers/utils"
25+
"github.com/optimizely/go-sdk/pkg/entities"
26+
)
27+
28+
// LeMatcher matches against the "le" match type
29+
func LeMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) {
30+
31+
if floatValue, ok := utils.ToFloat(condition.Value); ok {
32+
attributeValue, err := user.GetFloatAttribute(condition.Name)
33+
if err != nil {
34+
return false, err
35+
}
36+
return floatValue >= attributeValue, nil
37+
}
38+
39+
return false, fmt.Errorf("audience condition %s evaluated to NULL because the condition value type is not supported", condition.Name)
40+
}

0 commit comments

Comments
 (0)