Skip to content

Commit 6acbc9f

Browse files
authored
Add some iterator utilities to go with the v1.23.0 upgrade. (#4238)
* Add some iterator utilities to go with the v1.23.0 upgrade. * Ditch the unused var. * Update from PR and add index and delete functions to sliceutil.
1 parent b134da1 commit 6acbc9f

File tree

9 files changed

+305
-0
lines changed

9 files changed

+305
-0
lines changed

util/iterutil/doc.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Package iterators provides a set of iterators for various data structures.
3+
4+
# Elements of slices as pointers
5+
6+
The main motivation is making it easier to work with slices of large structures, where people often choose an iterator
7+
pattern, which makes a shallow copy of each struct in the slice:
8+
9+
var myStructs []MyStruct
10+
for _, myStruct := range myStructs {
11+
// myStruct is a shallow *COPY* of each element - any changes in the loop have no effect on the slice element.
12+
}
13+
14+
In order to improve that, and also not have to use the throw-away `_` variable, [SlicePointerValues] may be used to
15+
iterate across pointers to each element in the slice:
16+
17+
var myStructs []MyStruct
18+
for myStructPtr := range iterators.SlicePointerValues(myStructs) {
19+
// myStructPtr is a pointer to each element - any changes in the loop will be reflected in the slice element.
20+
}
21+
22+
If you still need the index, you can use [SlicePointers]:
23+
24+
var myStructs []MyStruct
25+
for i, myStructPtr := range iterators.SlicePointers(myStructs) {
26+
// myStructPtr is a pointer to each element - any changes in the loop will be reflected in the slice element.
27+
// i is the index of the element.
28+
}
29+
30+
# Walking json
31+
32+
The https://github.com/tidwall/gjson library is already included as a dependency. If you need to walk a json document,
33+
the [WalkGjsonLeaves] iterator is available, which will yield the json paths to all leaves in the document along with
34+
the [gjson.Result] for each leaf.
35+
*/
36+
package iterators

util/iterutil/gjson.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package iterators
2+
3+
import (
4+
"iter"
5+
6+
"github.com/tidwall/gjson"
7+
)
8+
9+
type (
10+
GjsonElem struct {
11+
path string
12+
result gjson.Result
13+
}
14+
)
15+
16+
// WalkGjsonLeaves returns an iterator of (path, result) from a gjson result; only leaves are yielded.
17+
func WalkGjsonLeaves(result gjson.Result) iter.Seq2[string, gjson.Result] {
18+
return func(yield func(string, gjson.Result) bool) {
19+
stack := []GjsonElem{{path: "", result: result}}
20+
for stackLen := len(stack); stackLen > 0; stackLen = len(stack) {
21+
elem := stack[stackLen-1]
22+
stack = stack[:stackLen-1]
23+
if elem.result.Type == gjson.JSON {
24+
if elem.path != "" {
25+
elem.result.ForEach(func(key, value gjson.Result) bool {
26+
stack = append(stack, GjsonElem{path: elem.path + "." + key.String(), result: value})
27+
return true
28+
})
29+
} else {
30+
elem.result.ForEach(func(key, value gjson.Result) bool {
31+
stack = append(stack, GjsonElem{path: key.String(), result: value})
32+
return true
33+
})
34+
}
35+
} else if !yield(elem.path, elem.result) {
36+
return
37+
}
38+
}
39+
}
40+
}

util/iterutil/gjson_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package iterators
2+
3+
import (
4+
"slices"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/tidwall/gjson"
9+
)
10+
11+
func TestWalkGjsonLeaves(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
json string
15+
wantPaths []string
16+
}{
17+
{
18+
name: "true",
19+
json: "true",
20+
wantPaths: []string{""},
21+
},
22+
{
23+
name: "three fields",
24+
json: `{"foo": "bar", "num": 42, "bool": true}`,
25+
wantPaths: []string{"bool", "foo", "num"},
26+
},
27+
{
28+
name: "deep struct",
29+
json: `{"foo": {"bar": {"baz": "qux"}}, "num": 42, "bool": true}`,
30+
wantPaths: []string{"bool", "foo.bar.baz", "num"},
31+
},
32+
{
33+
name: "deep struct with array",
34+
json: `{"foo": {"bar": {"baz": "qux", "biz": [1,2,3,4]}}, "num": 42, "bool": true}`,
35+
wantPaths: []string{
36+
"bool",
37+
"foo.bar.baz",
38+
"foo.bar.biz.0",
39+
"foo.bar.biz.1",
40+
"foo.bar.biz.2",
41+
"foo.bar.biz.3",
42+
"num",
43+
},
44+
},
45+
}
46+
for _, tt := range tests {
47+
t.Run(tt.name, func(t *testing.T) {
48+
gjsonResult := gjson.Parse(tt.json)
49+
gotPaths := slices.Collect(Firsts(WalkGjsonLeaves(gjsonResult)))
50+
slices.Sort(gotPaths)
51+
assert.Equal(t, tt.wantPaths, gotPaths)
52+
})
53+
}
54+
}

util/iterutil/iter.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package iterators
2+
3+
import "iter"
4+
5+
// Firsts returns an iterator that yields the first elements of a sequence of pairs.
6+
func Firsts[T, U any](seq2 iter.Seq2[T, U]) iter.Seq[T] {
7+
return func(yield func(T) bool) {
8+
for t := range seq2 {
9+
if !yield(t) {
10+
return
11+
}
12+
}
13+
}
14+
}
15+
16+
// Seconds returns an iterator that yields the second elements of a sequence of pairs.
17+
func Seconds[T, U any](seq2 iter.Seq2[T, U]) iter.Seq[U] {
18+
return func(yield func(U) bool) {
19+
for _, u := range seq2 {
20+
if !yield(u) {
21+
return
22+
}
23+
}
24+
}
25+
}

util/iterutil/iter_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package iterators
2+
3+
import (
4+
"iter"
5+
"slices"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func testIter() iter.Seq2[int, string] {
12+
return func(yield func(int, string) bool) {
13+
if !yield(1, "one") {
14+
return
15+
}
16+
if !yield(2, "two") {
17+
return
18+
}
19+
if !yield(3, "three") {
20+
return
21+
}
22+
}
23+
}
24+
25+
func TestFirsts(t *testing.T) {
26+
firsts := Firsts(testIter())
27+
want := []int{1, 2, 3}
28+
got := slices.Collect(firsts)
29+
assert.Equal(t, want, got)
30+
}
31+
32+
func TestSeconds(t *testing.T) {
33+
seconds := Seconds(testIter())
34+
want := []string{"one", "two", "three"}
35+
got := slices.Collect(seconds)
36+
assert.Equal(t, want, got)
37+
}

util/iterutil/slices.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package iterators
2+
3+
import "iter"
4+
5+
// SlicePointers returns an iterator that yields indices and pointers to the elements of a slice.
6+
func SlicePointers[Slice ~[]T, T any](s Slice) iter.Seq2[int, *T] {
7+
return func(yield func(int, *T) bool) {
8+
for i := range s {
9+
if !yield(i, &s[i]) {
10+
return
11+
}
12+
}
13+
}
14+
}
15+
16+
// SlicePointerValues returns an iterator that yields pointers to the elements of a slice.
17+
func SlicePointerValues[Slice ~[]T, T any](s Slice) iter.Seq[*T] {
18+
return func(yield func(*T) bool) {
19+
for i := range s {
20+
if !yield(&s[i]) {
21+
return
22+
}
23+
}
24+
}
25+
}

util/iterutil/slices_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package iterators
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
type Simple[T any] struct {
10+
Value T
11+
}
12+
13+
func TestSlicePointers(t *testing.T) {
14+
s := []Simple[int]{{1}, {2}, {3}}
15+
for i, v := range SlicePointers(s) {
16+
v.Value = 99999
17+
assert.EqualValues(t, 99999, s[i].Value)
18+
}
19+
for _, v := range s {
20+
assert.EqualValues(t, 99999, v.Value)
21+
}
22+
}
23+
24+
func TestSlicePointerValues(t *testing.T) {
25+
s := []Simple[int]{{1}, {2}, {3}}
26+
for v := range SlicePointerValues(s) {
27+
v.Value = 99999
28+
}
29+
for _, v := range s {
30+
assert.EqualValues(t, 99999, v.Value)
31+
}
32+
}

util/sliceutil/slices.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package sliceutil
2+
3+
// IndexPointerFunc returns the index of the first element in the slice for which the function f returns true.
4+
func IndexPointerFunc[Slice ~[]T, T any](s Slice, f func(*T) bool) int {
5+
for i := range s {
6+
if f(&s[i]) {
7+
return i
8+
}
9+
}
10+
return -1
11+
}
12+
13+
// DeletePointerFunc deletes all elements from the slice for which the function f returns true.
14+
func DeletePointerFunc[Slice ~[]T, T any](s Slice, f func(*T) bool) Slice {
15+
i := IndexPointerFunc(s, f)
16+
if i == -1 {
17+
return s
18+
}
19+
for j := i + 1; j < len(s); j++ {
20+
if v := &s[j]; !f(v) {
21+
s[i] = *v
22+
i++
23+
}
24+
}
25+
clear(s[i:]) // zero/nil out the obsolete elements, for GC
26+
return s[:i]
27+
}

util/sliceutil/slices_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package sliceutil
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
type Simple[T any] struct {
10+
Value T
11+
}
12+
13+
func TestIndexPointerFunc(t *testing.T) {
14+
s := []Simple[int]{{1}, {2}, {3}}
15+
assert.Equal(t, 1, IndexPointerFunc(s, func(v *Simple[int]) bool { return v.Value == 2 }))
16+
assert.Equal(t, -1, IndexPointerFunc(s, func(v *Simple[int]) bool { return v.Value == 4 }))
17+
}
18+
19+
func TestDeletePointerFunc(t *testing.T) {
20+
s := []Simple[int]{{1}, {2}, {3}, {4}, {5}}
21+
s = DeletePointerFunc(s, func(v *Simple[int]) bool { return v.Value%2 == 0 })
22+
assert.Equal(t, []Simple[int]{{1}, {3}, {5}}, s)
23+
24+
s = DeletePointerFunc(s, func(v *Simple[int]) bool { return v.Value > 10 })
25+
assert.Equal(t, []Simple[int]{{1}, {3}, {5}}, s)
26+
27+
s = DeletePointerFunc(s, func(v *Simple[int]) bool { return true })
28+
assert.Empty(t, s)
29+
}

0 commit comments

Comments
 (0)