Skip to content
This repository was archived by the owner on Dec 10, 2024. It is now read-only.

Commit 944529b

Browse files
committed
refactor: remove non-rangefunc variant; add tests
1 parent 16265ec commit 944529b

File tree

3 files changed

+151
-66
lines changed

3 files changed

+151
-66
lines changed

iterator.go

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,58 @@
1-
//go:build go1.22 && goexperiment.rangefunc
2-
// +build go1.22,goexperiment.rangefunc
1+
//go:build go1.23
2+
// +build go1.23
33

44
package gitlab
55

66
import (
77
"iter"
88
)
99

10-
// PageIterator is an EXPERIMENTAL iterator as defined in the "rangefunc" experiment for go 1.22.
11-
// See https://go.dev/wiki/RangefuncExperiment for more details.
12-
//
13-
// It can be used as:
10+
// Paginatable is the type implemented by list functions that return paginated
11+
// content (e.g. [UsersService.ListUsers]).
12+
// It works for top-level entities (e.g. users). See [PaginatableForID] for
13+
// entities that require a parent ID (e.g. tags).
14+
type Paginatable[O, T any] func(*O, ...RequestOptionFunc) ([]*T, *Response, error)
15+
16+
// AllPages is a [iter.Seq2] iterator to be used with any paginated resource.
17+
// E.g. [UsersService.ListUsers]
1418
//
15-
// for user, err := range gitlab.PageIterator(gl.Users.List, nil) {
19+
// for user, err := range gitlab.AllPages(gl.Users.ListUsers, nil) {
1620
// if err != nil {
1721
// // handle error
1822
// }
1923
// // process individual user
2024
// }
21-
func PageIterator[O, T any](f Paginatable[O, T], opt *O, optFunc ...RequestOptionFunc) iter.Seq2[*T, error] {
25+
//
26+
// It is also possible to specify additional pagination parameters:
27+
//
28+
// for mr, err := range gitlab.AllPages(
29+
// gl.MergeRequests.ListMergeRequests,
30+
// &gitlab.ListMergeRequestsOptions{
31+
// ListOptions: gitlab.ListOptions{
32+
// PerPage: 100,
33+
// Pagination: "keyset",
34+
// OrderBy: "created_at",
35+
// },
36+
// },
37+
// gitlab.WithContext(ctx),
38+
// ) {
39+
// // ...
40+
// }
41+
//
42+
// Errors while fetching pages are returned as the second value of the iterator.
43+
// It is the responsibility of the caller to handle them appropriately, e.g. by
44+
// breaking the loop. The iteration will otherwise continue indefinitely,
45+
// retrying to retrieve the erroring page on each iteration.
46+
func AllPages[O, T any](f Paginatable[O, T], opt *O, optFunc ...RequestOptionFunc) iter.Seq2[*T, error] {
2247
return func(yield func(*T, error) bool) {
2348
nextLink := ""
2449
for {
2550
page, resp, err := f(opt, append(optFunc, WithKeysetPaginationParameters(nextLink))...)
2651
if err != nil {
27-
yield(nil, err)
28-
return
52+
if !yield(nil, err) {
53+
return
54+
}
55+
continue
2956
}
3057
for _, p := range page {
3158
if !yield(p, nil) {
@@ -40,10 +67,16 @@ func PageIterator[O, T any](f Paginatable[O, T], opt *O, optFunc ...RequestOptio
4067
}
4168
}
4269

43-
// PageIteratorForID is similar to [PageIterator] but for paginated resources that require a parent ID (e.g. tags of a project).
44-
func PageIteratorForID[O, T any](id any, f PaginatableForID[O, T], opt *O, optFunc ...RequestOptionFunc) iter.Seq2[*T, error] {
70+
// PaginatableForID is the type implemented by list functions that return
71+
// paginated content for sub-entities (e.g. [TagsService.ListTags]).
72+
// See also [Paginatable] for top-level entities (e.g. users).
73+
type PaginatableForID[O, T any] func(any, *O, ...RequestOptionFunc) ([]*T, *Response, error)
74+
75+
// AllPagesForID is similar to [AllPages] but for paginated resources that
76+
// require a parent ID (e.g. tags of a project).
77+
func AllPagesForID[O, T any](id any, f PaginatableForID[O, T], opt *O, optFunc ...RequestOptionFunc) iter.Seq2[*T, error] {
4578
idFunc := func(opt *O, optFunc ...RequestOptionFunc) ([]*T, *Response, error) {
4679
return f(id, opt, optFunc...)
4780
}
48-
return PageIterator(idFunc, opt, optFunc...)
81+
return AllPages(idFunc, opt, optFunc...)
4982
}

iterator_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//go:build go1.23
2+
// +build go1.23
3+
4+
package gitlab
5+
6+
import (
7+
"errors"
8+
"iter"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestAllPages(t *testing.T) {
15+
type foo struct{ string }
16+
type listFooOpt struct{}
17+
18+
type iteration struct {
19+
foo *foo
20+
err error
21+
}
22+
23+
sentinelError := errors.New("sentinel error")
24+
25+
// assertSeq is a helper function to assert the sequence of iterations.
26+
// It is necessary because the iteration may be endless (e.g. in the error
27+
// case).
28+
assertSeq := func(t *testing.T, expected []iteration, actual iter.Seq2[*foo, error]) {
29+
t.Helper()
30+
i := 0
31+
for actualFoo, actualErr := range actual {
32+
if i >= len(expected) {
33+
t.Errorf("unexpected iteration: %v, %v", actualFoo, actualErr)
34+
break
35+
}
36+
assert.Equal(t, expected[i].foo, actualFoo)
37+
assert.Equal(t, expected[i].err, actualErr)
38+
i++
39+
}
40+
41+
if i < len(expected) {
42+
t.Errorf("expected %d more iterations", len(expected)-i)
43+
}
44+
}
45+
46+
type args struct {
47+
f Paginatable[listFooOpt, foo]
48+
opt *listFooOpt
49+
optFunc []RequestOptionFunc
50+
}
51+
tests := []struct {
52+
name string
53+
args args
54+
want []iteration
55+
}{
56+
{
57+
name: "empty",
58+
args: args{
59+
f: func() Paginatable[listFooOpt, foo] {
60+
return func(*listFooOpt, ...RequestOptionFunc) ([]*foo, *Response, error) {
61+
return []*foo{}, &Response{}, nil
62+
}
63+
}(),
64+
},
65+
want: []iteration{},
66+
},
67+
{
68+
name: "single element, no errors",
69+
args: args{
70+
f: func() Paginatable[listFooOpt, foo] {
71+
return func(*listFooOpt, ...RequestOptionFunc) ([]*foo, *Response, error) {
72+
return []*foo{{"foo"}}, &Response{}, nil
73+
}
74+
}(),
75+
},
76+
want: []iteration{
77+
{foo: &foo{"foo"}, err: nil},
78+
},
79+
},
80+
{
81+
name: "one error than success",
82+
args: args{
83+
f: func() Paginatable[listFooOpt, foo] {
84+
called := false
85+
return func(*listFooOpt, ...RequestOptionFunc) ([]*foo, *Response, error) {
86+
if !called {
87+
called = true
88+
return []*foo{}, &Response{}, sentinelError
89+
}
90+
return []*foo{{"foo"}}, &Response{}, nil
91+
}
92+
}(),
93+
},
94+
want: []iteration{
95+
{foo: nil, err: sentinelError},
96+
{foo: &foo{"foo"}, err: nil},
97+
},
98+
},
99+
}
100+
for _, tt := range tests {
101+
t.Run(tt.name, func(t *testing.T) {
102+
assertSeq(t, tt.want, AllPages(tt.args.f, tt.args.opt, tt.args.optFunc...))
103+
})
104+
}
105+
}

pagination.go

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)