Skip to content

Commit f7dca42

Browse files
authored
Merge pull request #53 from snyk/feat/version-slice
feat: VersionSlice supporting sort and search
2 parents 113d6b9 + 8b50886 commit f7dca42

6 files changed

+206
-38
lines changed

resource.go

+6-14
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const (
2626
type Resource struct {
2727
*Document
2828
Name string
29-
Version *Version
29+
Version Version
3030
sourcePrefix string
3131
}
3232

@@ -58,8 +58,8 @@ func (e *ResourceVersions) Name() string {
5858
}
5959

6060
// Versions returns a slice containing each Version defined for this endpoint.
61-
func (e *ResourceVersions) Versions() []*Version {
62-
result := make([]*Version, len(e.versions))
61+
func (e *ResourceVersions) Versions() []Version {
62+
result := make([]Version, len(e.versions))
6363
for i := range e.versions {
6464
result[i] = e.versions[i].Version
6565
}
@@ -84,7 +84,7 @@ func (e *ResourceVersions) At(vs string) (*Resource, error) {
8484
}
8585
for i := len(e.versions) - 1; i >= 0; i-- {
8686
ev := e.versions[i].Version
87-
if (ev.Date.Before(v.Date) || ev.Date.Equal(v.Date)) && v.Stability.Compare(ev.Stability) <= 0 {
87+
if dateCmp, stabilityCmp := ev.compareDateStability(v); dateCmp <= 0 && stabilityCmp >= 0 {
8888
return e.versions[i], nil
8989
}
9090
}
@@ -94,19 +94,11 @@ func (e *ResourceVersions) At(vs string) (*Resource, error) {
9494
type resourceVersionSlice []*Resource
9595

9696
func (e resourceVersionSlice) Less(i, j int) bool {
97-
return e[i].Version.Compare(e[j].Version) < 0
97+
return e[i].Version.Compare(&e[j].Version) < 0
9898
}
9999
func (e resourceVersionSlice) Len() int { return len(e) }
100100
func (e resourceVersionSlice) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
101101

102-
type versionSlice []*Version
103-
104-
func (vs versionSlice) Less(i, j int) bool {
105-
return vs[i].Compare(vs[j]) < 0
106-
}
107-
func (vs versionSlice) Len() int { return len(vs) }
108-
func (vs versionSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
109-
110102
// LoadResourceVersions returns a ResourceVersions slice parsed from a
111103
// directory structure of resource specs. This directory will be of the form:
112104
//
@@ -209,7 +201,7 @@ func loadResource(specPath string, versionStr string) (*Resource, error) {
209201
return nil, fmt.Errorf("failed to localize refs: %w", err)
210202
}
211203

212-
ep := &Resource{Name: name, Document: doc, Version: version}
204+
ep := &Resource{Name: name, Document: doc, Version: *version}
213205
for path := range doc.T.Paths {
214206
doc.T.Paths[path].ExtensionProps.Extensions[ExtSnykApiVersion] = version.String()
215207
}

resource_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func TestResource(t *testing.T) {
1515
c := qt.New(t)
1616
eps, err := LoadResourceVersions(testdata.Path("resources/_examples/hello-world"))
1717
c.Assert(err, qt.IsNil)
18-
c.Assert(eps.Versions(), qt.DeepEquals, []*Version{{
18+
c.Assert(eps.Versions(), qt.DeepEquals, []Version{{
1919
Date: time.Date(2021, time.June, 1, 0, 0, 0, 0, time.UTC),
2020
Stability: StabilityGA,
2121
}, {

spec.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,21 @@ func (s *SpecVersions) Resources() []*ResourceVersions {
9393

9494
// Versions returns a slice containing each Version defined by an Resource in
9595
// this specification. Versions are sorted in ascending order.
96-
func (s *SpecVersions) Versions() []*Version {
96+
func (s *SpecVersions) Versions() []Version {
9797
vset := map[Version]bool{}
9898
for _, eps := range s.resources {
9999
for i := range eps.versions {
100-
vset[*eps.versions[i].Version] = true
100+
vset[eps.versions[i].Version] = true
101101
}
102102
}
103-
versions := make([]*Version, len(vset))
103+
versions := make([]Version, len(vset))
104104
i := 0
105105
for k := range vset {
106106
v := k
107-
versions[i] = &v
107+
versions[i] = v
108108
i++
109109
}
110-
sort.Sort(versionSlice(versions))
110+
sort.Sort(VersionSlice(versions))
111111
return versions
112112
}
113113

spec_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func TestSpecs(t *testing.T) {
1515
c.Assert(err, qt.IsNil)
1616
versions := specs.Versions()
1717
c.Assert(versions, qt.HasLen, 4)
18-
c.Assert(versions, qt.ContentEquals, []*Version{
18+
c.Assert(versions, qt.ContentEquals, []Version{
1919
mustParseVersion("2021-06-01"),
2020
mustParseVersion("2021-06-04~experimental"),
2121
mustParseVersion("2021-06-07"),

version.go

+68-7
Original file line numberDiff line numberDiff line change
@@ -115,19 +115,30 @@ func (s Stability) Compare(sr Stability) int {
115115
// Compare returns -1 if the given version is less than, 0 if equal to, and 1
116116
// if greater than the caller target version.
117117
func (v *Version) Compare(vr *Version) int {
118-
if v.Date.Before(vr.Date) {
119-
return -1
118+
dateCmp, stabilityCmp := v.compareDateStability(vr)
119+
if dateCmp != 0 {
120+
return dateCmp
120121
}
121-
if v.Date.After(vr.Date) {
122-
return 1
122+
return stabilityCmp
123+
}
124+
125+
// compareDateStability returns the comparison of both the date and stability
126+
// between two versions. Used internally where these need to be evaluated
127+
// independently, such as when searching for the best matching version.
128+
func (v *Version) compareDateStability(vr *Version) (int, int) {
129+
dateCmp := 0
130+
if v.Date.Before(vr.Date) {
131+
dateCmp = -1
132+
} else if v.Date.After(vr.Date) {
133+
dateCmp = 1
123134
}
124-
// Dates are equal
125-
return 0 - v.Stability.Compare(vr.Stability)
135+
stabilityCmp := v.Stability.Compare(vr.Stability)
136+
return dateCmp, stabilityCmp
126137
}
127138

128139
// VersionDateStrings returns a slice of distinct version date strings for a
129140
// slice of Versions. Consecutive duplicate dates are removed.
130-
func VersionDateStrings(vs []*Version) []string {
141+
func VersionDateStrings(vs []Version) []string {
131142
var result []string
132143
for i := range vs {
133144
ds := vs[i].DateString()
@@ -137,3 +148,53 @@ func VersionDateStrings(vs []*Version) []string {
137148
}
138149
return result
139150
}
151+
152+
// VersionSlice is a sortable, searchable slice of Versions.
153+
type VersionSlice []Version
154+
155+
// Resolve returns the most recent Version in the slice with equal or greater
156+
// stability.
157+
//
158+
// This method requires that the VersionSlice has already been sorted with
159+
// sort.Sort, otherwise behavior is undefined.
160+
func (vs VersionSlice) Resolve(q Version) (*Version, error) {
161+
lower, curr, upper := 0, len(vs)/2, len(vs)
162+
if upper == 0 {
163+
// Nothing matches an empty slice.
164+
return nil, ErrNoMatchingVersion
165+
}
166+
for curr < upper && lower != upper-1 {
167+
dateCmp, stabilityCmp := vs[curr].compareDateStability(&q)
168+
if dateCmp > 0 {
169+
// Current version is more recent than the query, so it's our new
170+
// upper (exclusive) range limit to search.
171+
upper = curr
172+
curr = lower + (upper-lower)/2
173+
} else if dateCmp <= 0 {
174+
if stabilityCmp >= 0 {
175+
// Matching version found, so it's our new lower (inclusive)
176+
// range limit to search.
177+
lower = curr
178+
}
179+
// The edge is somewhere between here and the upper limit.
180+
curr = curr + (upper-curr)/2 + (upper-curr)%2
181+
}
182+
}
183+
// Did we find a match?
184+
dateCmp, stabilityCmp := vs[lower].compareDateStability(&q)
185+
if dateCmp <= 0 && stabilityCmp >= 0 {
186+
return &vs[lower], nil
187+
}
188+
return nil, ErrNoMatchingVersion
189+
}
190+
191+
// Len implements sort.Interface.
192+
func (vs VersionSlice) Len() int { return len(vs) }
193+
194+
// Less implements sort.Interface.
195+
func (vs VersionSlice) Less(i, j int) bool {
196+
return vs[i].Compare(&vs[j]) < 0
197+
}
198+
199+
// Swap implements sort.Interface.
200+
func (vs VersionSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }

version_test.go

+125-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package vervet_test
22

33
import (
4+
"sort"
45
"testing"
56

67
qt "github.com/frankban/quicktest"
@@ -43,12 +44,12 @@ func TestParseVersion(t *testing.T) {
4344
}
4445
}
4546

46-
func mustParseVersion(s string) *Version {
47+
func mustParseVersion(s string) Version {
4748
v, err := ParseVersion(s)
4849
if err != nil {
4950
panic(err)
5051
}
51-
return v
52+
return *v
5253
}
5354

5455
func TestVersionOrder(t *testing.T) {
@@ -64,25 +65,25 @@ func TestVersionOrder(t *testing.T) {
6465
}, {
6566
l: "2021-01-01", r: "2021-01-01", cmp: 0,
6667
}, {
67-
l: "9999-12-31", r: "9999-12-31~beta", cmp: -1,
68+
l: "9999-12-31", r: "9999-12-31~beta", cmp: 1,
6869
}, {
6970
// Compare date versions and special tags
70-
l: "9999-12-31~beta", r: "9999-12-31", cmp: 1,
71+
l: "9999-12-31~beta", r: "9999-12-31", cmp: -1,
7172
}, {
72-
l: "9999-12-31", r: "9999-12-31~experimental", cmp: -1,
73+
l: "9999-12-31", r: "9999-12-31~experimental", cmp: 1,
7374
}, {
74-
l: "9999-12-31~experimental", r: "9999-12-31", cmp: 1,
75+
l: "9999-12-31~experimental", r: "9999-12-31", cmp: -1,
7576
// Compare special tags
7677
}, {
77-
l: "2021-08-01~beta", r: "2021-08-01~experimental", cmp: -1,
78+
l: "2021-08-01~beta", r: "2021-08-01~experimental", cmp: 1,
7879
}, {
79-
l: "2021-08-01~experimental", r: "2021-08-01~beta", cmp: 1,
80+
l: "2021-08-01~experimental", r: "2021-08-01~beta", cmp: -1,
8081
}, {
8182
l: "2021-08-01~beta", r: "2021-08-01~beta", cmp: 0,
8283
}, {
8384
l: "2021-08-01~experimental", r: "2021-08-01~experimental", cmp: 0,
8485
}, {
85-
l: "2021-08-01~wip", r: "2021-08-01~experimental", cmp: 1,
86+
l: "2021-08-01~wip", r: "2021-08-01~experimental", cmp: -1,
8687
}}
8788
for i := range tests {
8889
c.Logf("test %d %#v", i, tests[i])
@@ -96,7 +97,7 @@ func TestVersionOrder(t *testing.T) {
9697

9798
func TestVersionDateStrings(t *testing.T) {
9899
c := qt.New(t)
99-
c.Assert(VersionDateStrings([]*Version{
100+
c.Assert(VersionDateStrings([]Version{
100101
mustParseVersion("2021-06-01~wip"),
101102
mustParseVersion("2021-06-01~beta"),
102103
mustParseVersion("2021-06-10~beta"),
@@ -106,3 +107,117 @@ func TestVersionDateStrings(t *testing.T) {
106107
mustParseVersion("2021-07-12~beta"),
107108
}), qt.ContentEquals, []string{"2021-06-01", "2021-06-10", "2021-07-12"})
108109
}
110+
111+
func TestVersionSlice(t *testing.T) {
112+
type matchTest struct {
113+
match string
114+
result string
115+
err string
116+
}
117+
tests := []struct {
118+
versions VersionSlice
119+
first string
120+
last string
121+
matchTests []matchTest
122+
}{{
123+
versions: VersionSlice{
124+
mustParseVersion("2021-07-12~experimental"),
125+
mustParseVersion("2021-06-01~beta"),
126+
mustParseVersion("2021-07-12~beta"),
127+
mustParseVersion("2021-06-10"),
128+
mustParseVersion("2021-06-01~wip"),
129+
mustParseVersion("2021-07-12~wip"),
130+
mustParseVersion("2021-06-10~beta"),
131+
},
132+
first: "2021-06-01~wip",
133+
last: "2021-07-12~beta",
134+
matchTests: []matchTest{{
135+
match: "2021-06-10",
136+
result: "2021-06-10",
137+
}, {
138+
match: "2021-06-10~beta",
139+
result: "2021-06-10",
140+
}, {
141+
match: "2021-06-10~experimental",
142+
result: "2021-06-10",
143+
}, {
144+
match: "2021-06-11~experimental",
145+
result: "2021-06-10",
146+
}, {
147+
match: "2021-01-01",
148+
err: "no matching version",
149+
}, {
150+
match: "2022-01-01",
151+
result: "2021-06-10",
152+
}, {
153+
match: "2022-01-01~experimental",
154+
result: "2021-07-12~beta",
155+
}},
156+
}, {
157+
versions: VersionSlice{
158+
mustParseVersion("2021-06-10~beta"),
159+
},
160+
first: "2021-06-10~beta",
161+
last: "2021-06-10~beta",
162+
matchTests: []matchTest{{
163+
match: "2021-06-10",
164+
err: "no matching version",
165+
}, {
166+
match: "2021-06-10~beta",
167+
result: "2021-06-10~beta",
168+
}, {
169+
match: "2021-06-10~experimental",
170+
result: "2021-06-10~beta",
171+
}, {
172+
match: "2021-06-11~wip",
173+
result: "2021-06-10~beta",
174+
}, {
175+
match: "2021-01-01",
176+
err: "no matching version",
177+
}, {
178+
match: "2022-01-01~wip",
179+
result: "2021-06-10~beta",
180+
}},
181+
}, {
182+
versions: VersionSlice{
183+
mustParseVersion("2021-06-10~beta"),
184+
mustParseVersion("2022-01-10~experimental"),
185+
},
186+
first: "2021-06-10~beta",
187+
last: "2022-01-10~experimental",
188+
matchTests: []matchTest{{
189+
match: "2021-04-10",
190+
err: "no matching version",
191+
}, {
192+
match: "2021-08-10~beta",
193+
result: "2021-06-10~beta",
194+
}, {
195+
match: "2022-02-10~wip",
196+
result: "2022-01-10~experimental",
197+
}, {
198+
match: "2021-01-30",
199+
err: "no matching version",
200+
}},
201+
}}
202+
c := qt.New(t)
203+
for _, t := range tests {
204+
sort.Sort(t.versions)
205+
c.Assert(t.versions[0].String(), qt.Equals, t.first)
206+
c.Assert(t.versions[len(t.versions)-1].String(), qt.Equals, t.last)
207+
for _, mt := range t.matchTests {
208+
match := mustParseVersion(mt.match)
209+
result, err := t.versions.Resolve(match)
210+
if err != nil {
211+
c.Assert(err, qt.ErrorMatches, mt.err)
212+
} else {
213+
c.Assert(result.String(), qt.Equals, mt.result)
214+
}
215+
}
216+
}
217+
}
218+
219+
func TestVersionSliceResolveEmpty(t *testing.T) {
220+
c := qt.New(t)
221+
_, err := VersionSlice{}.Resolve(mustParseVersion("2021-10-31"))
222+
c.Assert(err, qt.ErrorMatches, "no matching version")
223+
}

0 commit comments

Comments
 (0)