Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
80d1d25
feat: added semver support to our existing matchers
pawels-optimizely Aug 13, 2020
2981c20
fix linters
pawels-optimizely Aug 13, 2020
ad65d4b
fix linters
pawels-optimizely Aug 14, 2020
cf66c93
added more changes
pawels-optimizely Aug 17, 2020
a044757
added semver for registry pattern
pawels-optimizely Aug 18, 2020
068ac3d
consolidate all the semver code
pawels-optimizely Aug 19, 2020
e3d7150
added ge and le evaluators
pawels-optimizely Aug 19, 2020
de95b6a
simplify tests
pawels-optimizely Aug 20, 2020
e89d07d
Update pkg/decision/evaluator/matchers/semver_test.go
pawels-optimizely Aug 20, 2020
5cafc5a
cleaning up
pawels-optimizely Aug 20, 2020
89b9406
addressing nit comments
pawels-optimizely Aug 20, 2020
e272112
addressing PR comments
pawels-optimizely Aug 20, 2020
a91371e
increase coverage
pawels-optimizely Aug 20, 2020
ff5136d
increase coverage
pawels-optimizely Aug 20, 2020
18361fd
corrected tests
pawels-optimizely Aug 21, 2020
ae97a2d
increased coverage
pawels-optimizely Aug 21, 2020
9a9ca1b
increase coverage
pawels-optimizely Aug 21, 2020
be1e4a5
small improvement in the coverage
pawels-optimizely Aug 21, 2020
8eaadb8
add a few more tests and fix split when not build or release
thomaszurkan-optimizely Aug 21, 2020
7849cae
Merge branch 'master' into pawel/semver
thomaszurkan-optimizely Aug 21, 2020
ef25dcd
fix merge errors
thomaszurkan-optimizely Aug 21, 2020
57bebb1
slight refactor for lint
thomaszurkan-optimizely Aug 21, 2020
a1ba38b
fixing after a refactor right in the middle of doing this pr.
thomaszurkan-optimizely Aug 21, 2020
f4c2f88
Merge branch 'master' into pawel/semver
thomaszurkan-optimizely Aug 21, 2020
64ae09c
fix lint error
thomaszurkan-optimizely Aug 21, 2020
d2f1ba5
Merge branch 'pawel/semver' of https://github.com/optimizely/go-sdk i…
thomaszurkan-optimizely Aug 21, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions pkg/decision/evaluator/condition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,24 @@ func TestCustomAttributeConditionEvaluatorWithoutMatchType(t *testing.T) {
result, _ = conditionEvaluator.Evaluate(condition, condTreeParams)
assert.Equal(t, result, false)
}

func TestCustomAttributeConditionEvaluatorForSemver(t *testing.T) {
conditionEvaluator := CustomAttributeConditionEvaluator{}
condition := entities.Condition{
Match: "semver_ge",
Value: "2.9",
Name: "string_foo",
Type: "custom_attribute",
}

// Test condition passes
user := entities.UserContext{
Attributes: map[string]interface{}{
"string_foo": "2.9.1",
},
}

condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{})
result, _ := conditionEvaluator.Evaluate(condition, condTreeParams)
assert.Equal(t, result, true)
}
23 changes: 19 additions & 4 deletions pkg/decision/evaluator/matchers/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,29 @@ const (
GtMatchType = "gt"
// SubstringMatchType name for the "substring" matcher
SubstringMatchType = "substring"
// SemverEqMatchType name for the semver_eq matcher
SemverEqMatchType = "semver_eq"
// SemverLtMatchType name for the semver_eq matcher
SemverLtMatchType = "semver_lt"
// SemverLeMatchType name for the semver_eq matcher
SemverLeMatchType = "semver_le"
// SemverGtMatchType name for the semver_eq matcher
SemverGtMatchType = "semver_gt"
// SemverGeMatchType name for the semver_eq matcher
SemverGeMatchType = "semver_ge"
)

var registry = map[string]Matcher{
ExactMatchType: ExactMatcher,
ExistsMatchType: ExistsMatcher,
LtMatchType: LtMatcher,
GtMatchType: GtMatcher,
ExactMatchType: ExactMatcher,
ExistsMatchType: ExistsMatcher,
LtMatchType: LtMatcher,
GtMatchType: GtMatcher,
SubstringMatchType: SubstringMatcher,
SemverEqMatchType: SemverEqMatcher,
SemverLtMatchType: SemverLtMatcher,
SemverLeMatchType: SemverLeMatcher,
SemverGtMatchType: SemverGtMatcher,
SemverGeMatchType: SemverGeMatcher,
}

var lock = sync.RWMutex{}
Expand Down
163 changes: 163 additions & 0 deletions pkg/decision/evaluator/matchers/semver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/****************************************************************************
* Copyright 2020, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
***************************************************************************/

// Package matchers //
package matchers

import (
"fmt"
"regexp"
"strconv"
"strings"

"github.com/optimizely/go-sdk/pkg/decision/reasons"
"github.com/optimizely/go-sdk/pkg/entities"

"github.com/pkg/errors"
)

// SemanticVersion defines the class
type SemanticVersion struct {
Condition string // condition is always a string here
}

func (sv SemanticVersion) compareVersion(attribute string) (int, error) {

targetedVersionParts, err := sv.splitSemanticVersion(sv.Condition)
if err != nil {
return 0, err
}
versionParts, e := sv.splitSemanticVersion(attribute)
if e != nil {
return 0, e
}

// Up to the precision of targetedVersion, expect version to match exactly.
for idx := range targetedVersionParts {

switch {
case len(versionParts) <= idx:
return -1, nil
case !sv.isNumber(versionParts[idx]):
// Compare strings
if versionParts[idx] < targetedVersionParts[idx] {
return -1, nil
} else if versionParts[idx] > targetedVersionParts[idx] {
return 1, nil
}
case sv.isNumber(targetedVersionParts[idx]): // both targetedVersionParts and versionParts are digits
if sv.toInt(versionParts[idx]) < sv.toInt(targetedVersionParts[idx]) {
return -1, nil
} else if sv.toInt(versionParts[idx]) > sv.toInt(targetedVersionParts[idx]) {
return 1, nil
}
default:
return -1, nil
}
}

if sv.isPreRelease(attribute) && !sv.isPreRelease(sv.Condition) {
return -1, nil
}

return 0, nil
}

func (sv SemanticVersion) splitSemanticVersion(targetedVersion string) ([]string, error) {

if sv.hasWhiteSpace(targetedVersion) {
return []string{}, errors.New(string(reasons.AttributeFormatInvalid))
}

splitBy := ""
if sv.isBuild(targetedVersion) {
splitBy = sv.buildSeperator()
} else if sv.isPreRelease(targetedVersion) {
splitBy = sv.preReleaseSeperator()
}
targetParts := strings.Split(targetedVersion, splitBy)
if len(targetParts) == 0 {
return []string{}, errors.New(string(reasons.AttributeFormatInvalid))
}

targetPrefix := targetParts[0]
targetSuffix := targetParts[1:]

// Expect a version string of the form x.y.z
targetedVersionParts := strings.Split(targetPrefix, ".")

if len(targetedVersionParts) > 3 {
return []string{}, errors.New(string(reasons.AttributeFormatInvalid))
}

if len(targetedVersionParts) == 0 {
return []string{}, errors.New(string(reasons.AttributeFormatInvalid))
}

targetedVersionParts = append(targetedVersionParts, targetSuffix...)
return targetedVersionParts, nil
}

func (sv SemanticVersion) isNumber(str string) bool {
var digitCheck = regexp.MustCompile(`^[0-9]+$`)
return (digitCheck.MatchString(str))
}

func (sv SemanticVersion) toInt(str string) int {
i, e := strconv.Atoi(str)
if e != nil {
return 0
}
return i
}

func (sv SemanticVersion) isPreRelease(str string) bool {
return strings.Contains(str, "-")
}

func (sv SemanticVersion) isBuild(str string) bool {
return strings.Contains(str, "+")
}

func (sv SemanticVersion) hasWhiteSpace(str string) bool {
return strings.Contains(str, " ")
}

func (sv SemanticVersion) buildSeperator() string {
return "+"
}

func (sv SemanticVersion) preReleaseSeperator() string {
return "-"
}

// SemverEvaluator is a help function to wrap a common evaluation code
func SemverEvaluator(cond entities.Condition, user entities.UserContext) (int, error) {

if stringValue, ok := cond.Value.(string); ok {
attributeValue, err := user.GetStringAttribute(cond.Name)
if err != nil {
return 0, err
}
semVer := SemanticVersion{stringValue}
comparison, e := semVer.compareVersion(attributeValue)
if e != nil {
return 0, e
}
return comparison, nil
}
return 0, fmt.Errorf("audience condition %s evaluated to NULL because the condition value type is not supported", cond.Name)
}
31 changes: 31 additions & 0 deletions pkg/decision/evaluator/matchers/semver_eq.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/****************************************************************************
* Copyright 2020, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
***************************************************************************/

// Package matchers //
package matchers

import (
"github.com/optimizely/go-sdk/pkg/entities"
)

// SemverEqMatcher returns true if the user's semver attribute is equal to the semver condition value
func SemverEqMatcher(condition entities.Condition, user entities.UserContext) (bool, error) {
comparison, err := SemverEvaluator(condition, user)
if err != nil {
return false, err
}
return comparison == 0, nil
}
98 changes: 98 additions & 0 deletions pkg/decision/evaluator/matchers/semver_eq_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/****************************************************************************
* Copyright 2020, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
***************************************************************************/

package matchers

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/optimizely/go-sdk/pkg/entities"
)

func TestSemverEqMatcher(t *testing.T) {

condition := entities.Condition{
Match: "semver_eq",
Value: "2.0",
Name: "version",
}

user := entities.UserContext{
Attributes: map[string]interface{}{
"version": "2.0.0",
},
}
result, err := SemverEqMatcher(condition, user)
assert.NoError(t, err)
assert.True(t, result)

user = entities.UserContext{
Attributes: map[string]interface{}{
"version": "2.9",
},
}

result, err = SemverEqMatcher(condition, user)
assert.NoError(t, err)
assert.False(t, result)

user = entities.UserContext{
Attributes: map[string]interface{}{
"version": "1.9",
},
}

result, err = SemverEqMatcher(condition, user)
assert.NoError(t, err)
assert.False(t, result)

// Test attribute not found
user = entities.UserContext{
Attributes: map[string]interface{}{
"version1": "2.0",
},
}

_, err = SemverEqMatcher(condition, user)
assert.Error(t, err)
}

func TestSemverEqMatcherInvalidType(t *testing.T) {
condition := entities.Condition{
Match: "semver_eq",
Value: "2.0",
Name: "version",
}

user := entities.UserContext{
Attributes: map[string]interface{}{
"version": true,
},
}
_, err := SemverEqMatcher(condition, user)
assert.Error(t, err)

user = entities.UserContext{
Attributes: map[string]interface{}{
"version": 37,
},
}

_, err = SemverEqMatcher(condition, user)
assert.Error(t, err)
}
31 changes: 31 additions & 0 deletions pkg/decision/evaluator/matchers/semver_ge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/****************************************************************************
* Copyright 2020, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
***************************************************************************/

// Package matchers //
package matchers

import (
"github.com/optimizely/go-sdk/pkg/entities"
)

// SemverGeMatcher returns true if the user's semver attribute is greater or equal to the semver condition value
func SemverGeMatcher(condition entities.Condition, user entities.UserContext) (bool, error) {
comparison, err := SemverEvaluator(condition, user)
if err != nil {
return false, err
}
return comparison >= 0, nil
}
Loading