Skip to content

Commit a3eb209

Browse files
committed
Add new constructor that strictly adheres to semver specs
1 parent b5a281d commit a3eb209

File tree

2 files changed

+112
-8
lines changed

2 files changed

+112
-8
lines changed

version.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,25 @@ import (
1010
)
1111

1212
// The compiled regular expression used to test the validity of a version.
13-
var versionRegexp *regexp.Regexp
13+
var (
14+
versionRegexp *regexp.Regexp
15+
semverRegexp *regexp.Regexp
16+
)
1417

1518
// The raw regular expression string used for testing the validity
1619
// of a version.
17-
const VersionRegexpRaw string = `v?([0-9]+(\.[0-9]+)*?)` +
18-
`(-([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)|(-?([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?` +
19-
`(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` +
20-
`?`
20+
const (
21+
VersionRegexpRaw string = `v?([0-9]+(\.[0-9]+)*?)` +
22+
`(-([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)|(-?([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?` +
23+
`(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` +
24+
`?`
25+
26+
// SemverRegexpRaw requires a separator between version and prerelease
27+
SemverRegexpRaw string = `v?([0-9]+(\.[0-9]+)*?)` +
28+
`(-([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)|(-([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?` +
29+
`(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` +
30+
`?`
31+
)
2132

2233
// Version represents a single version.
2334
type Version struct {
@@ -30,12 +41,24 @@ type Version struct {
3041

3142
func init() {
3243
versionRegexp = regexp.MustCompile("^" + VersionRegexpRaw + "$")
44+
semverRegexp = regexp.MustCompile("^" + SemverRegexpRaw + "$")
3345
}
3446

3547
// NewVersion parses the given version and returns a new
3648
// Version.
3749
func NewVersion(v string) (*Version, error) {
38-
matches := versionRegexp.FindStringSubmatch(v)
50+
return newVersion(v, versionRegexp)
51+
}
52+
53+
// NewSemver parses the given version and returns a new
54+
// Version that adheres strictly to SemVer specs
55+
// https://semver.org/
56+
func NewSemver(v string) (*Version, error) {
57+
return newVersion(v, semverRegexp)
58+
}
59+
60+
func newVersion(v string, pattern *regexp.Regexp) (*Version, error) {
61+
matches := pattern.FindStringSubmatch(v)
3962
if matches == nil {
4063
return nil, fmt.Errorf("Malformed version: %s", v)
4164
}

version_test.go

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ func TestNewVersion(t *testing.T) {
1010
version string
1111
err bool
1212
}{
13+
{"", true},
1314
{"1.2.3", false},
1415
{"1.0", false},
1516
{"1", false},
@@ -32,14 +33,56 @@ func TestNewVersion(t *testing.T) {
3233
{"foo1.2.3", true},
3334
{"1.7rc2", false},
3435
{"v1.7rc2", false},
36+
{"1.0-", false},
3537
}
3638

3739
for _, tc := range cases {
3840
_, err := NewVersion(tc.version)
3941
if tc.err && err == nil {
40-
t.Fatalf("expected error for version: %s", tc.version)
42+
t.Fatalf("expected error for version: %q", tc.version)
4143
} else if !tc.err && err != nil {
42-
t.Fatalf("error for version %s: %s", tc.version, err)
44+
t.Fatalf("error for version %q: %s", tc.version, err)
45+
}
46+
}
47+
}
48+
49+
func TestNewSemver(t *testing.T) {
50+
cases := []struct {
51+
version string
52+
err bool
53+
}{
54+
{"", true},
55+
{"1.2.3", false},
56+
{"1.0", false},
57+
{"1", false},
58+
{"1.2.beta", true},
59+
{"1.21.beta", true},
60+
{"foo", true},
61+
{"1.2-5", false},
62+
{"1.2-beta.5", false},
63+
{"\n1.2", true},
64+
{"1.2.0-x.Y.0+metadata", false},
65+
{"1.2.0-x.Y.0+metadata-width-hypen", false},
66+
{"1.2.3-rc1-with-hypen", false},
67+
{"1.2.3.4", false},
68+
{"1.2.0.4-x.Y.0+metadata", false},
69+
{"1.2.0.4-x.Y.0+metadata-width-hypen", false},
70+
{"1.2.0-X-1.2.0+metadata~dist", false},
71+
{"1.2.3.4-rc1-with-hypen", false},
72+
{"1.2.3.4", false},
73+
{"v1.2.3", false},
74+
{"foo1.2.3", true},
75+
{"1.7rc2", true},
76+
{"v1.7rc2", true},
77+
{"1.0-", true},
78+
}
79+
80+
for _, tc := range cases {
81+
_, err := NewSemver(tc.version)
82+
if tc.err && err == nil {
83+
t.Fatalf("expected error for version: %q", tc.version)
84+
} else if !tc.err && err != nil {
85+
t.Fatalf("error for version %q: %s", tc.version, err)
4386
}
4487
}
4588
}
@@ -91,6 +134,44 @@ func TestVersionCompare(t *testing.T) {
91134
}
92135
}
93136

137+
func TestVersionCompare_versionAndSemver(t *testing.T) {
138+
cases := []struct {
139+
versionRaw string
140+
semverRaw string
141+
expected int
142+
}{
143+
{"0.0.2", "0.0.2", 0},
144+
{"1.0.2alpha", "1.0.2-alpha", 0},
145+
{"v1.2+foo", "v1.2+beta", 0},
146+
{"v1.2", "v1.2+meta", 0},
147+
{"1.2", "1.2-beta", 1},
148+
{"v1.2", "v1.2-beta", 1},
149+
{"1.2.3", "1.4.5", -1},
150+
{"v1.2", "v1.2.0.0.1", -1},
151+
{"v1.0.3-", "v1.0.3", -1},
152+
}
153+
154+
for _, tc := range cases {
155+
ver, err := NewVersion(tc.versionRaw)
156+
if err != nil {
157+
t.Fatalf("err: %s", err)
158+
}
159+
160+
semver, err := NewSemver(tc.semverRaw)
161+
if err != nil {
162+
t.Fatalf("err: %s", err)
163+
}
164+
165+
actual := ver.Compare(semver)
166+
if actual != tc.expected {
167+
t.Fatalf(
168+
"%s <=> %s\nexpected: %d\n actual: %d",
169+
tc.versionRaw, tc.semverRaw, tc.expected, actual,
170+
)
171+
}
172+
}
173+
}
174+
94175
func TestComparePreReleases(t *testing.T) {
95176
cases := []struct {
96177
v1 string

0 commit comments

Comments
 (0)