Skip to content

Commit

Permalink
refactor: remove non-rangefunc variant; add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
costela committed Sep 28, 2024
1 parent 16265ec commit 944529b
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 66 deletions.
59 changes: 46 additions & 13 deletions iterator.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
//go:build go1.22 && goexperiment.rangefunc
// +build go1.22,goexperiment.rangefunc
//go:build go1.23
// +build go1.23

package gitlab

import (
"iter"
)

// PageIterator is an EXPERIMENTAL iterator as defined in the "rangefunc" experiment for go 1.22.
// See https://go.dev/wiki/RangefuncExperiment for more details.
//
// It can be used as:
// Paginatable is the type implemented by list functions that return paginated
// content (e.g. [UsersService.ListUsers]).
// It works for top-level entities (e.g. users). See [PaginatableForID] for
// entities that require a parent ID (e.g. tags).
type Paginatable[O, T any] func(*O, ...RequestOptionFunc) ([]*T, *Response, error)

// AllPages is a [iter.Seq2] iterator to be used with any paginated resource.
// E.g. [UsersService.ListUsers]
//
// for user, err := range gitlab.PageIterator(gl.Users.List, nil) {
// for user, err := range gitlab.AllPages(gl.Users.ListUsers, nil) {
// if err != nil {
// // handle error
// }
// // process individual user
// }
func PageIterator[O, T any](f Paginatable[O, T], opt *O, optFunc ...RequestOptionFunc) iter.Seq2[*T, error] {
//
// It is also possible to specify additional pagination parameters:
//
// for mr, err := range gitlab.AllPages(
// gl.MergeRequests.ListMergeRequests,
// &gitlab.ListMergeRequestsOptions{
// ListOptions: gitlab.ListOptions{
// PerPage: 100,
// Pagination: "keyset",
// OrderBy: "created_at",
// },
// },
// gitlab.WithContext(ctx),
// ) {
// // ...
// }
//
// Errors while fetching pages are returned as the second value of the iterator.
// It is the responsibility of the caller to handle them appropriately, e.g. by
// breaking the loop. The iteration will otherwise continue indefinitely,
// retrying to retrieve the erroring page on each iteration.
func AllPages[O, T any](f Paginatable[O, T], opt *O, optFunc ...RequestOptionFunc) iter.Seq2[*T, error] {
return func(yield func(*T, error) bool) {
nextLink := ""
for {
page, resp, err := f(opt, append(optFunc, WithKeysetPaginationParameters(nextLink))...)
if err != nil {
yield(nil, err)
return
if !yield(nil, err) {
return
}
continue
}
for _, p := range page {
if !yield(p, nil) {
Expand All @@ -40,10 +67,16 @@ func PageIterator[O, T any](f Paginatable[O, T], opt *O, optFunc ...RequestOptio
}
}

// PageIteratorForID is similar to [PageIterator] but for paginated resources that require a parent ID (e.g. tags of a project).
func PageIteratorForID[O, T any](id any, f PaginatableForID[O, T], opt *O, optFunc ...RequestOptionFunc) iter.Seq2[*T, error] {
// PaginatableForID is the type implemented by list functions that return
// paginated content for sub-entities (e.g. [TagsService.ListTags]).
// See also [Paginatable] for top-level entities (e.g. users).
type PaginatableForID[O, T any] func(any, *O, ...RequestOptionFunc) ([]*T, *Response, error)

// AllPagesForID is similar to [AllPages] but for paginated resources that
// require a parent ID (e.g. tags of a project).
func AllPagesForID[O, T any](id any, f PaginatableForID[O, T], opt *O, optFunc ...RequestOptionFunc) iter.Seq2[*T, error] {
idFunc := func(opt *O, optFunc ...RequestOptionFunc) ([]*T, *Response, error) {
return f(id, opt, optFunc...)
}
return PageIterator(idFunc, opt, optFunc...)
return AllPages(idFunc, opt, optFunc...)
}
105 changes: 105 additions & 0 deletions iterator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//go:build go1.23
// +build go1.23

package gitlab

import (
"errors"
"iter"
"testing"

"github.com/stretchr/testify/assert"
)

func TestAllPages(t *testing.T) {
type foo struct{ string }
type listFooOpt struct{}

type iteration struct {
foo *foo
err error
}

sentinelError := errors.New("sentinel error")

// assertSeq is a helper function to assert the sequence of iterations.
// It is necessary because the iteration may be endless (e.g. in the error
// case).
assertSeq := func(t *testing.T, expected []iteration, actual iter.Seq2[*foo, error]) {
t.Helper()
i := 0
for actualFoo, actualErr := range actual {
if i >= len(expected) {
t.Errorf("unexpected iteration: %v, %v", actualFoo, actualErr)
break
}
assert.Equal(t, expected[i].foo, actualFoo)
assert.Equal(t, expected[i].err, actualErr)
i++
}

if i < len(expected) {
t.Errorf("expected %d more iterations", len(expected)-i)
}
}

type args struct {
f Paginatable[listFooOpt, foo]
opt *listFooOpt
optFunc []RequestOptionFunc
}
tests := []struct {
name string
args args
want []iteration
}{
{
name: "empty",
args: args{
f: func() Paginatable[listFooOpt, foo] {
return func(*listFooOpt, ...RequestOptionFunc) ([]*foo, *Response, error) {
return []*foo{}, &Response{}, nil
}
}(),
},
want: []iteration{},
},
{
name: "single element, no errors",
args: args{
f: func() Paginatable[listFooOpt, foo] {
return func(*listFooOpt, ...RequestOptionFunc) ([]*foo, *Response, error) {
return []*foo{{"foo"}}, &Response{}, nil
}
}(),
},
want: []iteration{
{foo: &foo{"foo"}, err: nil},
},
},
{
name: "one error than success",
args: args{
f: func() Paginatable[listFooOpt, foo] {
called := false
return func(*listFooOpt, ...RequestOptionFunc) ([]*foo, *Response, error) {
if !called {
called = true
return []*foo{}, &Response{}, sentinelError
}
return []*foo{{"foo"}}, &Response{}, nil
}
}(),
},
want: []iteration{
{foo: nil, err: sentinelError},
{foo: &foo{"foo"}, err: nil},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assertSeq(t, tt.want, AllPages(tt.args.f, tt.args.opt, tt.args.optFunc...))
})
}
}
53 changes: 0 additions & 53 deletions pagination.go

This file was deleted.

0 comments on commit 944529b

Please sign in to comment.