Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sem_ver jsonLogic evaluator #675

Merged
merged 17 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions core/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
go.opentelemetry.io/otel/trace v1.16.0
go.uber.org/zap v1.24.0
golang.org/x/crypto v0.8.0
golang.org/x/mod v0.9.0
golang.org/x/net v0.10.0
golang.org/x/sync v0.2.0
google.golang.org/grpc v1.55.0
Expand Down
4 changes: 2 additions & 2 deletions core/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -693,8 +693,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
Expand Down Expand Up @@ -815,6 +813,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
4 changes: 4 additions & 0 deletions core/pkg/eval/json_evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ func NewJSONEvaluator(logger *logger.Logger, s *store.Flags) *JSONEvaluator {
}
jsonlogic.AddOperator("starts_with", sce.StartsWithEvaluation)
jsonlogic.AddOperator("ends_with", sce.EndsWithEvaluation)

sve := SemVerComparisonEvaluator{Logger: ev.Logger}
jsonlogic.AddOperator("sem_ver", sve.SemVerEvaluation)

return &ev
}

Expand Down
144 changes: 144 additions & 0 deletions core/pkg/eval/semver_evaluation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package eval

import (
"errors"
"fmt"
"strings"

"github.com/open-feature/flagd/core/pkg/logger"
"golang.org/x/mod/semver"
)

type SemVerOperator string

const (
Equals SemVerOperator = "="
NotEqual SemVerOperator = "!="
Less SemVerOperator = "<"
LessOrEqual SemVerOperator = "<="
GreaterOrEqual SemVerOperator = ">="
Greater SemVerOperator = ">"
MatchMajor SemVerOperator = "^"
MatchMinor SemVerOperator = "~"
)

func (svo SemVerOperator) compare(v1, v2 string) (bool, error) {
cmpRes := semver.Compare(v1, v2)
switch svo {
case Less:
return cmpRes == -1, nil
case Equals:
return cmpRes == 0, nil
case NotEqual:
return cmpRes != 0, nil
case LessOrEqual:
return cmpRes == -1 || cmpRes == 0, nil
case GreaterOrEqual:
return cmpRes == +1 || cmpRes == 0, nil
case Greater:
return cmpRes == +1, nil
case MatchMinor:
v1MajorMinor := semver.MajorMinor(v1)
v2MajorMinor := semver.MajorMinor(v2)
return semver.Compare(v1MajorMinor, v2MajorMinor) == 0, nil
case MatchMajor:
v1Major := semver.Major(v1)
v2Major := semver.Major(v2)
return semver.Compare(v1Major, v2Major) == 0, nil
default:
return false, errors.New("invalid operator")
}
}

type SemVerComparisonEvaluator struct {
Logger *logger.Logger
}

// SemVerEvaluation checks if the given property matches a semantic versioning condition.
// It returns 'true', if the value of the given property meets the condition, 'false' if not.
// As an example, it can be used in the following way inside an 'if' evaluation:
//
// {
// "if": [
// {
// "sem_ver": [{"var": "version"}, ">=", "1.0.0"]
// },
// "red", null
// ]
// }
//
// This rule can be applied to the following data object, where the evaluation will resolve to 'true':
//
// { "version": "2.0.0" }
//
// Note that the 'sem_ver' evaluation rule must contain exactly three items:
// 1. Target property: this needs which both resolve to a semantic versioning string
// 2. Operator: One of the following: '=', '!=', '>', '<', '>=', '<=', '~', '^'
// 3. Target value: this needs which both resolve to a semantic versioning string
func (je *SemVerComparisonEvaluator) SemVerEvaluation(values, _ interface{}) interface{} {
actualVersion, targetVersion, operator, err := parseSemverEvaluationData(values)
if err != nil {
je.Logger.Error(fmt.Sprintf("parse sem_ver evaluation data: %v", err))
return nil
}
res, err := operator.compare(actualVersion, targetVersion)
if err != nil {
je.Logger.Error(fmt.Sprintf("sem_ver evaluation: %v", err))
return nil
}
return res
}

func parseSemverEvaluationData(values interface{}) (string, string, SemVerOperator, error) {
parsed, ok := values.([]interface{})
if !ok {
return "", "", "", errors.New("sem_ver evaluation is not an array")
}

if len(parsed) != 3 {
return "", "", "", errors.New("sem_ver evaluation must contain a value, an operator and a comparison target")
}

actualVersion, err := parseSemanticVersion(parsed[0])
if err != nil {
return "", "", "", fmt.Errorf("sem_ver evaluation: could not parse target property value: %w", err)
}

operator, err := parseOperator(parsed[1])
if err != nil {
return "", "", "", fmt.Errorf("sem_ver evaluation: could not parse operator: %w", err)
}

targetVersion, err := parseSemanticVersion(parsed[2])
if err != nil {
return "", "", "", fmt.Errorf("sem_ver evaluation: could not parse target value: %w", err)
}
return actualVersion, targetVersion, operator, nil
}

func parseSemanticVersion(v interface{}) (string, error) {
version, ok := v.(string)
if !ok {
return "", errors.New("sem_ver evaluation: property did not resolve to a string value")
}
// version strings are only valid in the semver package if they start with a 'v'
// if it's not present in the given value, we prepend it
if !strings.HasPrefix(version, "v") {
version = "v" + version
}

if !semver.IsValid(version) {
return "", errors.New("not a valid semantic version string")
}

return version, nil
}

func parseOperator(o interface{}) (SemVerOperator, error) {
operatorString, ok := o.(string)
if !ok {
return "", errors.New("could not parse operator")
}

return SemVerOperator(operatorString), nil
}
Loading