Skip to content

Commit

Permalink
Merge branch 'semver-matcher' of github.com:splitio/go-split-commons …
Browse files Browse the repository at this point in the history
…into semver-matcher
  • Loading branch information
mmelograno committed May 6, 2024
2 parents a7d20b8 + bd0d10b commit 4e5faa2
Show file tree
Hide file tree
Showing 7 changed files with 447 additions and 5 deletions.
186 changes: 186 additions & 0 deletions engine/grammar/matchers/datatypes/semver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package datatypes

import (
"errors"
"strconv"

"strings"
)

const (
metadataDelimiter = "+"
preReleaseDelimiter = "-"
valueDelimiter = "."
)

var ErrEmptyVersion = errors.New("version cannot be empty")
var ErrInvalidMetadata = errors.New("invalid metadata when parsing semver")
var ErrInvalidPrerelease = errors.New("invalid prerelease when parsing semver")
var ErrUnableToConvertSemver = errors.New("unable to convert to semver, incorrect format")

type Semver struct {
major int64
minor int64
patch int64
preRelease []string
isStable bool
metadata string
version string
}

func BuildSemver(version string) (*Semver, error) {
if len(strings.TrimSpace(version)) == 0 {
return nil, ErrEmptyVersion
}
metadata, vWithoutMetadata, err := processMetadata(version)
if err != nil {
return nil, err
}
preRelease, vWithoutPreRelease, err := processPreRelease(vWithoutMetadata)
if err != nil {
return nil, err
}
major, minor, patch, err := processComponents(vWithoutPreRelease)
if err != nil {
return nil, err
}

return &Semver{
metadata: metadata,
preRelease: preRelease,
major: major,
minor: minor,
patch: patch,
isStable: len(preRelease) == 0,
version: version,
}, nil
}

func processMetadata(version string) (string, string, error) {
index, metadata := extract(version, metadataDelimiter)
if index == -1 {
return "", version, nil
}

if len(metadata) == 0 {
return "", "", ErrInvalidMetadata

}
return metadata, strings.TrimSpace(version[0:index]), nil
}

func processPreRelease(vWithoutMetadata string) ([]string, string, error) {
index, preReleaseData := extract(vWithoutMetadata, preReleaseDelimiter)
if index == -1 {
return nil, vWithoutMetadata, nil
}

preRelease := strings.Split(preReleaseData, valueDelimiter)

if len(preRelease) == 0 || isEmpty(preRelease) {
return nil, "", ErrInvalidPrerelease
}
return preRelease, strings.TrimSpace(vWithoutMetadata[0:index]), nil
}

func extract(str string, delimiter string) (int, string) {
index := strings.Index(str, delimiter)
if index == -1 {
return index, ""
}

return index, strings.TrimSpace(str[index+1:])
}

func isEmpty(preRelease []string) bool {
for _, pr := range preRelease {
if len(pr) == 0 {
return true
}
}
return false
}

func processComponents(version string) (int64, int64, int64, error) {
vParts := strings.Split(version, valueDelimiter)
if len(vParts) != 3 {
return 0, 0, 0, ErrUnableToConvertSemver
}

major, err := strconv.ParseInt(vParts[0], 10, 64)
if err != nil {
return 0, 0, 0, ErrUnableToConvertSemver
}
minor, err := strconv.ParseInt(vParts[1], 10, 64)
if err != nil {
return 0, 0, 0, ErrUnableToConvertSemver
}
patch, err := strconv.ParseInt(vParts[2], 10, 64)
if err != nil {
return 0, 0, 0, ErrUnableToConvertSemver
}
return major, minor, patch, nil
}

func (s *Semver) Compare(toCompare Semver) int {
if s.version == toCompare.version {
return 0
}

// Compare major, minor, and patch versions numerically
compareResult := compareLongs(s.major, toCompare.major)
if compareResult != 0 {
return compareResult
}

compareResult = compareLongs(s.minor, toCompare.minor)
if compareResult != 0 {
return compareResult
}

compareResult = compareLongs(s.patch, toCompare.patch)
if compareResult != 0 {
return compareResult
}

if !s.isStable && toCompare.isStable {
return -1
} else if s.isStable && !toCompare.isStable {
return 1
}

minLength := 0
if len(s.preRelease) > len(toCompare.preRelease) {
minLength = len(toCompare.preRelease)
} else {
minLength = len(s.preRelease)
}
// Compare pre-release versions lexically
for i := 0; i < minLength; i++ {
if s.preRelease[i] == toCompare.preRelease[i] {
continue
}
preRelease1, e1 := strconv.ParseInt(s.preRelease[i], 10, 64)
preRelease2, e2 := strconv.ParseInt(s.preRelease[i], 10, 64)
if e1 == nil && e2 == nil {
return compareLongs(preRelease1, preRelease2)
}
return strings.Compare(s.preRelease[i], toCompare.preRelease[i])
}

// Compare lengths of pre-release versions
sPreReleaseLen := len(s.preRelease)
toComparePreReleaseLen := len(toCompare.preRelease)

return compareLongs(int64(sPreReleaseLen), int64(toComparePreReleaseLen))
}

func compareLongs(compare1 int64, compare2 int64) int {
if compare1 == compare2 {
return 0
}
if compare1 < compare2 {
return -1
}
return 1
}
179 changes: 179 additions & 0 deletions engine/grammar/matchers/datatypes/semver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package datatypes

import (
"encoding/csv"
"io"
"os"
"testing"
)

type Semvers struct {
semver1 string
semver2 string
semver3 string
}

func TestCompareSemverToGreaterAndEqual(t *testing.T) {
semvers, err := parseCSVTwoSemvers("../../../../testdata/valid_semantic_versions.csv")
if err != nil {
t.Error(err)
}

for _, semversPair := range semvers {
semver1, err := BuildSemver(semversPair.semver1)
if err != nil {
t.Error("should create semver1")
}
semver2, err := BuildSemver(semversPair.semver2)
if err != nil {
t.Error("should create semver2")
}

if semver1.Compare(*semver2) < 0 {
t.Error("semver 1 should be greather than semver 2")
}
if semver2.Compare(*semver1) > 0 {
t.Error("semver 1 should be greather than semver 2")
}
if semver1.Compare(*semver1) != 0 {
t.Error("semver 1 should be equal to semver 1")
}
if semver2.Compare(*semver2) != 0 {
t.Error("semver 2 should be equal to semver 2")
}
}
}

func TestInvalidFormats(t *testing.T) {
semvers, err := parseCSVOneSemver("../../../../testdata/invalid_semantic_versions.csv")
if err != nil {
t.Error(err)
}
for _, semver := range semvers {
_, err := BuildSemver(semver)
if err == nil {
t.Error("should not create semver")
}
}
}

func TestEqualTo(t *testing.T) {
semvers, err := parseCSVTwoSemvers("../../../../testdata/equal_to_semver.csv")
if err != nil {
t.Error(err)
}

for _, semversPair := range semvers {
semver1, err := BuildSemver(semversPair.semver1)
if err != nil {
t.Error("should create semver1")
}
semver2, err := BuildSemver(semversPair.semver2)
if err != nil {
t.Error("should create semver2")
}

if semver1.Compare(*semver2) != 0 {
t.Error("semver 1 should be equal to semver 2")
}
}
}

func TestBetween(t *testing.T) {
semvers, err := parseCSVThreeSemvers("../../../../testdata/between_semver.csv")
if err != nil {
t.Error(err)
}

for _, threeSemvers := range semvers {
semver1, err := BuildSemver(threeSemvers.semver1)
if err != nil {
t.Error("should create semver1")
}
semver2, err := BuildSemver(threeSemvers.semver2)
if err != nil {
t.Error("should create semver2")
}
semver3, err := BuildSemver(threeSemvers.semver3)
if err != nil {
t.Error("should create semver2")
}

if semver2.Compare(*semver1) < 0 && semver2.Compare(*semver3) > 0 {
t.Error("semver 2 should be between to semver 1 and semver 3")
}
}
}

func parseCSVOneSemver(file string) ([]string, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()

csvr := csv.NewReader(f)

var results []string
for {
row, err := csvr.Read()
if err != nil {
if err == io.EOF {
err = nil
}
return results, err
}
results = append(results, row[0])
}
}

func parseCSVTwoSemvers(file string) ([]Semvers, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()

csvr := csv.NewReader(f)

var results []Semvers
for {
row, err := csvr.Read()
if err != nil {
if err == io.EOF {
err = nil
}
return results, err
}
results = append(results, Semvers{
semver1: row[0],
semver2: row[1],
})
}
}

func parseCSVThreeSemvers(file string) ([]Semvers, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()

csvr := csv.NewReader(f)

var results []Semvers
for {
row, err := csvr.Read()
if err != nil {
if err == io.EOF {
err = nil
}
return results, err
}
results = append(results, Semvers{
semver1: row[0],
semver2: row[1],
semver3: row[2],
})
}
}
15 changes: 10 additions & 5 deletions engine/validator/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ import (
"github.com/splitio/go-toolkit/v5/logging"
)

// OverrideWithUnsupported overrides the split with an unsupported matcher type
func OverrideWithUnsupported(split *dtos.SplitDTO, idx int, jdx int) {
split.Conditions[idx].ConditionType = grammar.ConditionTypeWhitelist
split.Conditions[idx].MatcherGroup.Matchers[jdx].MatcherType = matchers.MatcherTypeAllKeys
split.Conditions[idx].MatcherGroup.Matchers[jdx].String = nil
split.Conditions[idx].Label = impressionlabels.UnsupportedMatcherType
split.Conditions[idx].Partitions = []dtos.PartitionDTO{{Treatment: evaluator.Control, Size: 100}}
}

// ProcessMatchers processes the matchers of a split and validates them
func ProcessMatchers(split *dtos.SplitDTO, logger logging.LoggerInterface) {
for idx := range split.Conditions {
for jdx := range split.Conditions[idx].MatcherGroup.Matchers {
_, err := matchers.BuildMatcher(&split.Conditions[idx].MatcherGroup.Matchers[jdx], &injection.Context{}, logger)
if err != nil {
split.Conditions[idx].ConditionType = grammar.ConditionTypeWhitelist
split.Conditions[idx].MatcherGroup.Matchers[jdx].MatcherType = matchers.MatcherTypeAllKeys
split.Conditions[idx].MatcherGroup.Matchers[jdx].String = nil
split.Conditions[idx].Label = impressionlabels.UnsupportedMatcherType
split.Conditions[idx].Partitions = []dtos.PartitionDTO{{Treatment: evaluator.Control, Size: 100}}
OverrideWithUnsupported(split, idx, jdx)
}
}
}
Expand Down
Loading

0 comments on commit 4e5faa2

Please sign in to comment.