Skip to content

Commit 6a9244f

Browse files
authored
Semantic Versioning (#14)
1 parent f775c12 commit 6a9244f

10 files changed

+1003
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This is single repository that stores many, independent small subpackages. This
2424
- [randstr](https://go.rtnl.ai/x/randstr): generate random strings using the crypto/rand package as efficiently as possible
2525
- [api](https://go.rtnl.ai/x/api): common utilities and responses for our JSON/REST APIs that our services run.
2626
- [dsn](https://go.rtnl.ai/x/dsn): parses data source names in order to connect to both server and embedded databases easily.
27+
- [semver](https://go.rtnl.ai/x/semver): allows parsing and comparison of semantic versioning numbers.
2728

2829
## About
2930

assert/assert.go

+14
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ func Regexp(tb testing.TB, rx *regexp.Regexp, str string, msgAndArgs ...interfac
101101
Assert(tb, rx.MatchString(str), msg)
102102
}
103103

104+
// Nil asserts that the specified object is nil.
105+
func Nil(tb testing.TB, object interface{}, msgAndArgs ...interface{}) {
106+
tb.Helper()
107+
msg := makeMessage("expected nil, but got non-nil", msgAndArgs...)
108+
Assert(tb, object == nil || reflect.ValueOf(object).IsNil(), msg)
109+
}
110+
111+
// NotNil asserts that the specified object is not nil.
112+
func NotNil(tb testing.TB, object interface{}, msgAndArgs ...interface{}) {
113+
tb.Helper()
114+
msg := makeMessage("expected non-nil, but got nil", msgAndArgs...)
115+
Assert(tb, object != nil && !reflect.ValueOf(object).IsNil(), msg)
116+
}
117+
104118
func makeMessage(msg string, msgAndArgs ...interface{}) string {
105119
switch len(msgAndArgs) {
106120
case 0:

semver/README.md

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Semantic Versioning
2+
3+
Utilities for identifying, parsing, and comparing semantic version strings as
4+
specified by [Semantic Versioning 2.0.0](https://semver.org/).
5+
6+
To test if a string is a valid semantic version:
7+
8+
```go
9+
semver.Valid("1.3.1")
10+
// true
11+
12+
semver.Valid("1.foo")
13+
// false
14+
```
15+
16+
To parse a string into its semantic version components:
17+
18+
```go
19+
vers, err := semver.Parse("1.3.1-alpha")
20+
```
21+
22+
To compare the precedence of two versions:
23+
24+
```go
25+
semver.Compare(a, b)
26+
// or
27+
a.Compare(b)
28+
```
29+
30+
[Precedence](https://semver.org/#spec-item-11) defines how versions are compared to each other when ordered (e.g. which is the later version number). The output of compare is:
31+
32+
- `-1`: a < b or b has the higher precedence
33+
- `0`: a == b or a and b have the same precedence (but not necessarily equal strings)
34+
- `1`: a > b or a has the higher precedence
35+
36+
A `Range` is a a specification of semantic version boundaries, and are used to check if a semantic version is part of a range or not. For example:
37+
38+
```go
39+
spec := semver.Range(">=1.3.1")
40+
spec("1.4.8")
41+
// true
42+
spec("1.2.9")
43+
// false
44+
```
45+
46+
Alternatively, you can check to see if a version satisifies a range:
47+
48+
```go
49+
v := semver.MustParse("1.3.0")
50+
v.Satisfies(spec)
51+
// false
52+
```
53+
54+
> **NOTE**: The range functionality described below is currently only partially implemented.
55+
56+
Ranges are denoted by an operator:
57+
58+
| Operator | Definition | Example | Notes |
59+
|---|---|---|---|
60+
| = | Match exact version | =1.2.3 | No operator is interpreted as = |
61+
| > | Match higher precedence versions | >1.2.3 | |
62+
| < | Match lower precedence versions | <1.2.3 | |
63+
| >= | Match exact or higher precedence versions | >=1.2.3 | |
64+
| <= | Match exact or lower precedence versions | <=1.2.3 | |
65+
| ~ | Match "reasonably close to" | ~1.2.3 | is >=1.2.3 && <1.3.0 |
66+
| ^ | Match "compatible with" | ^1.2.3 | is >=1.2.3 && <2.0.0 |
67+
| x or X | Any version | 1.x.x | is >=1.0.0 && <2.0.0 |
68+
| * | Any version | 1.2.* | is >=1.2.0 && <1.3.0 |
69+
| - | Hyphenated range | 1.2.3 - 2.3.4 | is >=1.2.3 && <=2.3.4 |
70+
71+
Ranges can be combined with `&&` (AND) and `||` (OR) operators for more complex range definitions.

semver/errors.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package semver
2+
3+
import "errors"
4+
5+
var (
6+
ErrInvalidSemVer = errors.New("invalid semantic version")
7+
ErrInvalidRange = errors.New("invalid semantic range")
8+
ErrScanValue = errors.New("could not scan source value")
9+
)

semver/range.go

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package semver
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"unicode"
7+
)
8+
9+
func Range(s string) (_ Specification, err error) {
10+
var fn Specifies
11+
if fn, err = parseRange(s); err != nil {
12+
return nil, err
13+
}
14+
15+
return func(t any) bool {
16+
switch v := t.(type) {
17+
case string:
18+
ver, err := Parse(v)
19+
if err != nil {
20+
return false
21+
}
22+
return fn(ver)
23+
case Version:
24+
return fn(v)
25+
case *Version:
26+
return fn(*v)
27+
default:
28+
return false
29+
}
30+
}, nil
31+
}
32+
33+
type Specification func(any) bool
34+
35+
type Specifies func(Version) bool
36+
37+
func (s Specifies) Or(o Specifies) Specifies {
38+
return func(v Version) bool {
39+
return s(v) || o(v)
40+
}
41+
}
42+
43+
func (s Specifies) And(o Specifies) Specifies {
44+
return func(v Version) bool {
45+
return s(v) && o(v)
46+
}
47+
}
48+
49+
//===========================================================================
50+
// Comparison Operators
51+
//===========================================================================
52+
53+
type operator rune
54+
55+
const (
56+
EQ operator = '='
57+
GT operator = '>'
58+
GTE operator = '≥'
59+
LT operator = '<'
60+
LTE operator = '≤'
61+
)
62+
63+
func compare(v Version, op operator) Specifies {
64+
switch op {
65+
case EQ:
66+
return func(o Version) bool {
67+
return Compare(o, v) == 0
68+
}
69+
case GT:
70+
return func(o Version) bool {
71+
return Compare(o, v) > 0
72+
}
73+
case GTE:
74+
return func(o Version) bool {
75+
return Compare(o, v) >= 0
76+
}
77+
case LT:
78+
return func(o Version) bool {
79+
return Compare(o, v) < 0
80+
}
81+
case LTE:
82+
return func(o Version) bool {
83+
return Compare(o, v) <= 0
84+
}
85+
default:
86+
panic(fmt.Errorf("unknown operator %q", op))
87+
}
88+
}
89+
90+
//===========================================================================
91+
// Parsing
92+
//===========================================================================
93+
94+
func parseRange(s string) (_ Specifies, err error) {
95+
parts := split(s)
96+
orParts, err := splitOR(parts)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
// TODO: expand wildcards
102+
103+
var orFn Specifies
104+
for _, part := range orParts {
105+
var andFn Specifies
106+
for _, and := range part {
107+
op, vers, err := parseComparitor(and)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
// TODO: build range functions
113+
114+
// Set function
115+
if andFn == nil {
116+
andFn = compare(vers, op)
117+
} else {
118+
andFn = andFn.And(compare(vers, op))
119+
}
120+
}
121+
122+
if orFn == nil {
123+
orFn = andFn
124+
} else {
125+
orFn = orFn.Or(andFn)
126+
}
127+
}
128+
129+
return orFn, nil
130+
}
131+
132+
func parseComparitor(s string) (op operator, v Version, err error) {
133+
i := strings.IndexFunc(s, unicode.IsDigit)
134+
if i == -1 {
135+
return 0, Version{}, ErrInvalidRange
136+
}
137+
138+
// Split the operator from the version
139+
ops, vers := s[0:i], s[i:]
140+
141+
// Parse the version number
142+
if v, err = Parse(vers); err != nil {
143+
return 0, Version{}, err
144+
}
145+
146+
// Parse the operator
147+
switch ops {
148+
case "=", "==":
149+
return EQ, v, nil
150+
case ">":
151+
return GT, v, nil
152+
case ">=":
153+
return GTE, v, nil
154+
case "<":
155+
return LT, v, nil
156+
case "<=":
157+
return LTE, v, nil
158+
default:
159+
return 0, Version{}, ErrInvalidRange
160+
}
161+
}
162+
163+
func split(s string) (result []string) {
164+
last := 0
165+
var lastChar byte
166+
exclude := []byte{'>', '<', '='}
167+
168+
for i := 0; i < len(s); i++ {
169+
if s[i] == ' ' && !inArray(lastChar, exclude) {
170+
if last < i-1 {
171+
result = append(result, s[last:i])
172+
}
173+
last = i + 1
174+
} else if s[i] != ' ' {
175+
lastChar = s[i]
176+
}
177+
}
178+
179+
if last < len(s)-1 {
180+
result = append(result, s[last:])
181+
}
182+
183+
for i, v := range result {
184+
result[i] = strings.Replace(v, " ", "", -1)
185+
}
186+
187+
return result
188+
}
189+
190+
func splitOR(parts []string) (result [][]string, err error) {
191+
last := 0
192+
for i, part := range parts {
193+
if part == "||" {
194+
if i == 0 {
195+
return nil, ErrInvalidRange
196+
}
197+
198+
result = append(result, parts[last:i])
199+
last = i + 1
200+
}
201+
}
202+
203+
if last == len(parts) {
204+
return nil, ErrInvalidRange
205+
}
206+
207+
result = append(result, parts[last:])
208+
return result, nil
209+
}
210+
211+
func inArray(s byte, list []byte) bool {
212+
for _, el := range list {
213+
if el == s {
214+
return true
215+
}
216+
}
217+
return false
218+
}

0 commit comments

Comments
 (0)