Skip to content

Commit 5f87650

Browse files
Add support for semver evaluation (FF-1570) (#34)
* Add support for semver evaluation (FF-1570) * fix expectation * simplify code; numeric first
1 parent 283a2e9 commit 5f87650

File tree

4 files changed

+86
-39
lines changed

4 files changed

+86
-39
lines changed

eppoclient/rules.go

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"regexp"
88
"strconv"
99
"strings"
10+
11+
semver "github.com/Masterminds/semver/v3"
1012
)
1113

1214
type condition struct {
@@ -27,7 +29,7 @@ func findMatchingRule(subjectAttributes dictionary, rules []rule) (rule, error)
2729
}
2830
}
2931

30-
return rule{}, errors.New("No matching rule")
32+
return rule{}, errors.New("no matching rule")
3133
}
3234

3335
func matchesRule(subjectAttributes dictionary, rule rule) bool {
@@ -41,25 +43,48 @@ func matchesRule(subjectAttributes dictionary, rule rule) bool {
4143
}
4244

4345
func evaluateCondition(subjectAttributes dictionary, condition condition) bool {
44-
subjectValue := subjectAttributes[condition.Attribute]
46+
subjectValue, exists := subjectAttributes[condition.Attribute]
47+
if !exists {
48+
return false
49+
}
4550

46-
if subjectValue != nil {
47-
if condition.Operator == "MATCHES" {
48-
v := reflect.ValueOf(subjectValue)
49-
if v.Kind() != reflect.String {
50-
subjectValue = strconv.Itoa(subjectValue.(int))
51+
switch condition.Operator {
52+
case "MATCHES":
53+
v := reflect.ValueOf(subjectValue)
54+
if v.Kind() != reflect.String {
55+
subjectValue = strconv.Itoa(subjectValue.(int))
56+
}
57+
r, _ := regexp.MatchString(condition.Value.(string), subjectValue.(string))
58+
return r
59+
case "ONE_OF":
60+
return isOneOf(subjectValue, convertToStringArray(condition.Value))
61+
case "NOT_ONE_OF":
62+
return isNotOneOf(subjectValue, convertToStringArray(condition.Value))
63+
default:
64+
// Attempt to evaluate as numeric condition if both values are numeric.
65+
subjectValueNumeric, isNumericSubject := subjectValue.(float64) // Assuming float64 for general numeric comparison; adjust as needed.
66+
conditionValueNumeric, isNumericCondition := condition.Value.(float64) // Same assumption as above.
67+
if isNumericSubject && isNumericCondition {
68+
return evaluateNumericCondition(subjectValueNumeric, conditionValueNumeric, condition)
69+
}
70+
71+
// Attempt to compare using semantic versioning if both values are strings.
72+
subjectValueStr, isStringSubject := subjectValue.(string)
73+
conditionValueStr, isStringCondition := condition.Value.(string)
74+
if isStringSubject && isStringCondition {
75+
// Attempt to parse both values as semantic versions.
76+
subjectSemVer, errSubject := semver.NewVersion(subjectValueStr)
77+
conditionSemVer, errCondition := semver.NewVersion(conditionValueStr)
78+
79+
// If parsing succeeds, evaluate the semver condition.
80+
if errSubject == nil && errCondition == nil {
81+
return evaluateSemVerCondition(subjectSemVer, conditionSemVer, condition)
5182
}
52-
r, _ := regexp.MatchString(condition.Value.(string), subjectValue.(string))
53-
return r
54-
} else if condition.Operator == "ONE_OF" {
55-
return isOneOf(subjectValue, convertToStringArray(condition.Value))
56-
} else if condition.Operator == "NOT_ONE_OF" {
57-
return isNotOneOf(subjectValue, convertToStringArray(condition.Value))
58-
} else {
59-
return evaluateNumericCondition(subjectValue, condition)
6083
}
84+
85+
// Fallback logic if neither numeric nor semver comparison is applicable.
86+
return false
6187
}
62-
return false
6388
}
6489

6590
func convertToStringArray(conditionValue interface{}) []string {
@@ -101,26 +126,31 @@ func getMatchingStringValues(attributeValue interface{}, conditionValue []string
101126
return result
102127
}
103128

104-
func evaluateNumericCondition(subjectValue interface{}, condition condition) bool {
105-
v := reflect.ValueOf(subjectValue)
106-
107-
if v.Kind() == reflect.String {
108-
return false
109-
}
110-
111-
if v.Kind() == reflect.Int {
112-
subjectValue = float64(subjectValue.(int))
129+
func evaluateSemVerCondition(subjectValue *semver.Version, conditionValue *semver.Version, condition condition) bool {
130+
switch condition.Operator {
131+
case "GT":
132+
return subjectValue.GreaterThan(conditionValue)
133+
case "GTE":
134+
return subjectValue.GreaterThan(conditionValue) || subjectValue.Equal(conditionValue)
135+
case "LT":
136+
return subjectValue.LessThan(conditionValue)
137+
case "LTE":
138+
return subjectValue.LessThan(conditionValue) || subjectValue.Equal(conditionValue)
139+
default:
140+
panic("Incorrect condition operator")
113141
}
142+
}
114143

144+
func evaluateNumericCondition(subjectValue float64, conditionValue float64, condition condition) bool {
115145
switch condition.Operator {
116146
case "GT":
117-
return subjectValue.(float64) > condition.Value.(float64)
147+
return subjectValue > conditionValue
118148
case "GTE":
119-
return subjectValue.(float64) >= condition.Value.(float64)
149+
return subjectValue >= conditionValue
120150
case "LT":
121-
return subjectValue.(float64) < condition.Value.(float64)
151+
return subjectValue < conditionValue
122152
case "LTE":
123-
return subjectValue.(float64) <= condition.Value.(float64)
153+
return subjectValue <= conditionValue
124154
default:
125155
panic("Incorrect condition operator")
126156
}

eppoclient/rules_test.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ var greaterThanCondition = condition{Operator: "GT", Value: 10.0, Attribute: "ag
1010
var lessThanCondition = condition{Operator: "LT", Value: 100.0, Attribute: "age"}
1111
var numericRule = rule{Conditions: []condition{greaterThanCondition, lessThanCondition}}
1212

13+
var greaterThanAppVersionCondition = condition{Operator: "GTE", Value: "1.2.0", Attribute: "appVersion"}
14+
var lessThanAppVersionCondition = condition{Operator: "LT", Value: "2.2.0", Attribute: "appVersion"}
15+
var semverRule = rule{Conditions: []condition{greaterThanAppVersionCondition, lessThanAppVersionCondition}}
16+
1317
var matchesEmailCondition = condition{Operator: "MATCHES", Value: ".*@email.com", Attribute: "email"}
1418
var textRule = rule{AllocationKey: "allocation-key", Conditions: []condition{matchesEmailCondition}}
1519
var ruleWithEmptyConditions = rule{Conditions: []condition{}}
16-
var expectedNoMatchErrorMessage = "No matching rule"
20+
var expectedNoMatchErrorMessage = "no matching rule"
1721

1822
func Test_findMatchingRule_withEmptyRules(t *testing.T) {
1923
subjectAttributes := make(dictionary)
@@ -45,6 +49,16 @@ func Test_findMatchingRule_Success(t *testing.T) {
4549
assert.Equal(t, numericRule, result)
4650
}
4751

52+
func Test_findMatchingSemVerRule_Success(t *testing.T) {
53+
subjectAttributes := make(dictionary)
54+
subjectAttributes["age"] = 99.0
55+
subjectAttributes["appVersion"] = "1.15.0"
56+
57+
result, _ := findMatchingRule(subjectAttributes, []rule{semverRule})
58+
59+
assert.Equal(t, semverRule, result)
60+
}
61+
4862
func Test_findMatchingRule_NoAttributeForCondition(t *testing.T) {
4963
subjectAttributes := make(dictionary)
5064

@@ -83,9 +97,9 @@ func Test_findMatchingRule_NumericValueAndRegex(t *testing.T) {
8397
}
8498

8599
type MatchesAnyRuleTest []struct {
86-
a dictionary
87-
b []rule
88-
expectedRule rule
100+
a dictionary
101+
b []rule
102+
expectedRule rule
89103
expectedError string
90104
}
91105

@@ -133,7 +147,7 @@ func Test_findMatchingRule_OneOfOperatorCaseInsensitive(t *testing.T) {
133147
result, err := findMatchingRule(tt.a, tt.b)
134148

135149
assert.Equal(t, tt.expectedRule, result)
136-
if (tt.expectedError != "") {
150+
if tt.expectedError != "" {
137151
assert.EqualError(t, err, tt.expectedError)
138152
}
139153
}
@@ -185,7 +199,7 @@ func Test_findMatchingRule_OneOfOperatorWithString(t *testing.T) {
185199
result, err := findMatchingRule(tt.a, tt.b)
186200

187201
assert.Equal(t, tt.expectedRule, result)
188-
if (tt.expectedError != "") {
202+
if tt.expectedError != "" {
189203
assert.EqualError(t, err, tt.expectedError)
190204
}
191205
}
@@ -223,7 +237,7 @@ func Test_findMatchingRule_OneOfOperatorWithNumber(t *testing.T) {
223237
result, err := findMatchingRule(tt.a, tt.b)
224238

225239
assert.Equal(t, tt.expectedRule, result)
226-
if (tt.expectedError != "") {
240+
if tt.expectedError != "" {
227241
assert.EqualError(t, err, tt.expectedError)
228242
}
229243
}
@@ -273,18 +287,18 @@ func Test_isNotOneOf_Fail(t *testing.T) {
273287

274288
func Test_evaluateNumericCondition_Success(t *testing.T) {
275289
expected := false
276-
result := evaluateNumericCondition(40, condition{Operator: "LT", Value: 30.0})
290+
result := evaluateNumericCondition(40, 30.0, condition{Operator: "LT", Value: 30.0})
277291

278292
assert.Equal(t, expected, result)
279293
}
280294

281295
func Test_evaluateNumericCondition_Fail(t *testing.T) {
282296
expected := true
283-
result := evaluateNumericCondition(25, condition{Operator: "LT", Value: 30.0})
297+
result := evaluateNumericCondition(25, 30.0, condition{Operator: "LT", Value: 30.0})
284298

285299
assert.Equal(t, expected, result)
286300
}
287301

288302
func Test_evaluateNumericCondition_IncorrectOperator(t *testing.T) {
289-
assert.Panics(t, func() { evaluateNumericCondition(25, condition{Operator: "LTGT", Value: 30.0}) })
303+
assert.Panics(t, func() { evaluateNumericCondition(25, 30.0, condition{Operator: "LTGT", Value: 30.0}) })
290304
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
)
99

1010
require (
11+
github.com/Masterminds/semver/v3 v3.2.1
1112
github.com/davecgh/go-spew v1.1.1 // indirect
1213
github.com/kr/pretty v0.1.0 // indirect
1314
github.com/pmezard/go-difflib v1.0.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
2+
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
13
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
35
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

0 commit comments

Comments
 (0)