-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'semver-matcher' of github.com:splitio/go-split-commons …
…into semver-matcher
- Loading branch information
Showing
7 changed files
with
447 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.