Skip to content

Commit d171750

Browse files
committed
Go: Add SemVer type to track valid semantic versions
1 parent 964b3f2 commit d171750

File tree

3 files changed

+178
-2
lines changed

3 files changed

+178
-2
lines changed

go/extractor/util/BUILD.bazel

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go/extractor/util/semver.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package util
2+
3+
import (
4+
"log"
5+
"strings"
6+
7+
"golang.org/x/mod/semver"
8+
)
9+
10+
// A type used to represent values known to be valid semantic versions.
11+
type SemVer interface {
12+
String() string
13+
// Compares this semantic version against the `other`. Returns the following values:
14+
//
15+
// 0 if both versions are equal.
16+
//
17+
// -1 if this version is older than the `other`.
18+
//
19+
// 1 if this version is newer than the `other`.
20+
Compare(other SemVer) int
21+
// Returns true if this version is newer than the `other`, or false otherwise.
22+
IsNewerThan(other SemVer) bool
23+
// Returns true if this version is equal to `other` or newer, or false otherwise.
24+
IsAtLeast(other SemVer) bool
25+
// Returns true if this version is older than the `other`, or false otherwise.
26+
IsOlderThan(other SemVer) bool
27+
// Returns true if this version is equal to `other` or older, or false otherwise.
28+
IsAtMost(other SemVer) bool
29+
// Returns the `major.minor` version prefix of the semantic version. For example, "v1.2.3" becomes "v1.2".
30+
MajorMinor() SemVer
31+
}
32+
33+
// The internal representation used for values known to be valid semantic versions.
34+
//
35+
// NOTE: Not exported to prevent invalid values from being constructed.
36+
type semVer string
37+
38+
// Converts the semantic version to a string representation.
39+
func (ver semVer) String() string {
40+
return string(ver)
41+
}
42+
43+
// Represents `v0.0.0`.
44+
func Zero() SemVer {
45+
return semVer("v0.0.0")
46+
}
47+
48+
// Constructs a [SemVer] from the given `version` string. The input can be any valid version string
49+
// that we commonly deal with. This includes ordinary version strings such as "1.2.3", ones with
50+
// the "go" prefix, and ones with the "v" prefix. Go's non-semver-compliant release candidate
51+
// versions are also automatically corrected from e.g. "go1.20rc1" to "v1.20-rc1". If given
52+
// the empty string, this function return `nil`. Otherwise, for invalid version strings, the function
53+
// prints a message to the log and exits the process.
54+
func NewSemVer(version string) SemVer {
55+
// If the input is the empty string, return nil f
56+
if version == "" {
57+
return nil
58+
}
59+
60+
// Drop a "go" prefix, if there is one.
61+
version = strings.TrimPrefix(version, "go")
62+
63+
// Go versions don't follow the SemVer format, but the only exception we normally care about
64+
// is release candidates; so this is a horrible hack to convert e.g. `1.22rc1` into `1.22-rc1`
65+
// which is compatible with the SemVer specification.
66+
rcIndex := strings.Index(version, "rc")
67+
if rcIndex != -1 {
68+
version = semver.Canonical("v"+version[:rcIndex]) + "-" + version[rcIndex:]
69+
}
70+
71+
// Add the "v" prefix that is required by the `semver` package.
72+
if !strings.HasPrefix(version, "v") {
73+
version = "v" + version
74+
}
75+
76+
// Convert the remaining version string to a canonical semantic version,
77+
// and check that this was successful.
78+
canonical := semver.Canonical(version)
79+
80+
if canonical == "" {
81+
log.Fatalf("%s is not a valid version string\n", version)
82+
}
83+
84+
return semVer(canonical)
85+
}
86+
87+
func (ver semVer) Compare(other SemVer) int {
88+
return semver.Compare(string(ver), string(other.String()))
89+
}
90+
91+
func (ver semVer) IsNewerThan(other SemVer) bool {
92+
return ver.Compare(other) > 0
93+
}
94+
95+
func (ver semVer) IsAtLeast(other SemVer) bool {
96+
return ver.Compare(other) >= 0
97+
}
98+
99+
func (ver semVer) IsOlderThan(other SemVer) bool {
100+
return ver.Compare(other) < 0
101+
}
102+
103+
func (ver semVer) IsAtMost(other SemVer) bool {
104+
return ver.Compare(other) <= 0
105+
}
106+
107+
func (ver semVer) MajorMinor() SemVer {
108+
return semVer(semver.MajorMinor(string(ver)))
109+
}

go/extractor/util/semver_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package util
2+
3+
import "testing"
4+
5+
func TestNewSemVer(t *testing.T) {
6+
type TestPair struct {
7+
Input string
8+
Expected string
9+
}
10+
11+
// Check the special case for the empty string.
12+
result := NewSemVer("")
13+
if result != nil {
14+
t.Errorf("Expected NewSemVer(\"\") to return nil, but got \"%s\".", result)
15+
}
16+
17+
testData := []TestPair{
18+
{"0", "v0.0.0"},
19+
{"1.0", "v1.0.0"},
20+
{"1.0.2", "v1.0.2"},
21+
{"1.20", "v1.20.0"},
22+
{"1.22.3", "v1.22.3"},
23+
}
24+
25+
// Check that we get what we expect for each of the test cases.
26+
for _, pair := range testData {
27+
result := NewSemVer(pair.Input)
28+
29+
if result.String() != pair.Expected {
30+
t.Errorf("Expected NewSemVer(\"%s\") to return \"%s\", but got \"%s\".", pair.Input, pair.Expected, result)
31+
}
32+
}
33+
34+
// And again, but this time prefixed with "v"
35+
for _, pair := range testData {
36+
result := NewSemVer("v" + pair.Input)
37+
38+
if result.String() != pair.Expected {
39+
t.Errorf("Expected NewSemVer(\"v%s\") to return \"%s\", but got \"%s\".", pair.Input, pair.Expected, result)
40+
}
41+
}
42+
43+
// And again, but this time prefixed with "go"
44+
for _, pair := range testData {
45+
result := NewSemVer("go" + pair.Input)
46+
47+
if result.String() != pair.Expected {
48+
t.Errorf("Expected NewSemVer(\"go%s\") to return \"%s\", but got \"%s\".", pair.Input, pair.Expected, result)
49+
}
50+
}
51+
52+
// And again, but this time with an "rc1" suffix.
53+
for _, pair := range testData {
54+
result := NewSemVer(pair.Input + "rc1")
55+
56+
if result.String() != pair.Expected+"-rc1" {
57+
t.Errorf("Expected NewSemVer(\"%src1\") to return \"%s\", but got \"%s\".", pair.Input, pair.Expected+"-rc1", result)
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)