Skip to content

Commit 4e82bf0

Browse files
Support V2-style pagination in V1
1 parent e3bf161 commit 4e82bf0

File tree

2 files changed

+158
-48
lines changed

2 files changed

+158
-48
lines changed

iter.go

Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -118,56 +118,51 @@ func GetIter(container ListParamsContainer, query Query) *Iter {
118118
return iter
119119
}
120120

121-
// V1List provides a convenient interface
122-
// for iterating over the elements
123-
// returned from paginated list API calls.
124-
// Successive calls to the Next method
125-
// will step through each item in the list,
126-
// fetching pages of items as needed.
127-
// Iterators are not thread-safe, so they should not be consumed
128-
// across multiple goroutines.
129-
type V1List[T any] struct {
121+
// v1List provides a convenient interface for iterating over the elements
122+
// returned from paginated list API calls. It is meant to be an improvement
123+
// over the Iter type, which was written before Go introduced generics and iter.Seq2.
124+
// Calling the `All` allows you to iterate over all items in the list,
125+
// with automatic pagination.
126+
type v1List[T any] struct {
130127
cur T
131-
listErr error
128+
err error
132129
formValues *form.Values
133130
listContainer ListContainer
134131
listParams ListParams
135132
listMeta *ListMeta
136-
query V1Query[T]
133+
query v1Query[T]
137134
values []T
138135
}
139136

137+
// All returns a Seq2 that will be evaluated on each item in a v1List.
138+
// The All function will continue to fetch pages of items as needed.
139+
func (it *v1List[T]) All() Seq2[T, error] {
140+
return func(yield func(T, error) bool) {
141+
for it.next() {
142+
if !yield(it.current(), nil) {
143+
return
144+
}
145+
}
146+
if it.err != nil {
147+
if !yield(*new(T), it.err) {
148+
return
149+
}
150+
}
151+
}
152+
}
153+
140154
// Current returns the most recent item
141155
// visited by a call to Next.
142-
func (it *V1List[T]) current() T {
156+
func (it *v1List[T]) current() T {
143157
return it.cur
144158
}
145159

146-
// Err returns the error, if any,
147-
// that caused the Iter to stop.
148-
// It must be inspected
149-
// after Next returns false.
150-
func (it *V1List[T]) err() error {
151-
return it.listErr
152-
}
153-
154-
// List returns the current list object which the iterator is currently using.
155-
// List objects will change as new API calls are made to continue pagination.
156-
func (it *V1List[T]) list() ListContainer {
157-
return it.listContainer
158-
}
159-
160-
// Meta returns the list metadata.
161-
func (it *V1List[T]) meta() *ListMeta {
162-
return it.listMeta
163-
}
164-
165-
// Next advances the Iter to the next item in the list,
160+
// next advances the V1List to the next item in the list,
166161
// which will then be available
167-
// through the Current method.
162+
// through the current method.
168163
// It returns false when the iterator stops
169164
// at the end of the list.
170-
func (it *V1List[T]) Next() bool {
165+
func (it *v1List[T]) next() bool {
171166
if len(it.values) == 0 && it.listMeta.HasMore && !it.listParams.Single {
172167
// determine if we're moving forward or backwards in paging
173168
if it.listParams.EndingBefore != nil {
@@ -187,8 +182,8 @@ func (it *V1List[T]) Next() bool {
187182
return true
188183
}
189184

190-
func (it *V1List[T]) getPage() {
191-
it.values, it.listContainer, it.listErr = it.query(it.listParams.GetParams(), it.formValues)
185+
func (it *v1List[T]) getPage() {
186+
it.values, it.listContainer, it.err = it.query(it.listParams.GetParams(), it.formValues)
192187
it.listMeta = it.listContainer.GetListMeta()
193188

194189
if it.listParams.EndingBefore != nil {
@@ -199,14 +194,10 @@ func (it *V1List[T]) getPage() {
199194
}
200195

201196
// Query is the function used to get a page listing.
202-
type V1Query[T any] func(*Params, *form.Values) ([]T, ListContainer, error)
203-
204-
//
205-
// Public functions
206-
//
197+
type v1Query[T any] func(*Params, *form.Values) ([]T, ListContainer, error)
207198

208-
// GetIter returns a new Iter for a given query and its options.
209-
func GetV1(container ListParamsContainer, query Query) *Iter {
199+
// getV1List returns a new v1List for a given query and its options.
200+
func getV1List[T any](container ListParamsContainer, query v1Query[T]) *v1List[T] {
210201
var listParams *ListParams
211202
formValues := &form.Values{}
212203

@@ -223,7 +214,7 @@ func GetV1(container ListParamsContainer, query Query) *Iter {
223214
if listParams == nil {
224215
listParams = &ListParams{}
225216
}
226-
iter := &Iter{
217+
iter := &v1List[T]{
227218
formValues: formValues,
228219
listParams: *listParams,
229220
query: query,
@@ -234,10 +225,6 @@ func GetV1(container ListParamsContainer, query Query) *Iter {
234225
return iter
235226
}
236227

237-
//
238-
// Private functions
239-
//
240-
241228
func listItemID[T any](x T) string {
242229
return reflect.ValueOf(x).Elem().FieldByName("ID").String()
243230
}

iter_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,97 @@ func TestIterListAndMeta(t *testing.T) {
143143
assert.Equal(t, listMeta, it.Meta())
144144
}
145145

146+
func TestV1ListEmpty(t *testing.T) {
147+
tq := testV1Query[*int]{{nil, &ListMeta{}, nil}}
148+
g, gerr := collectList(getV1List(nil, tq.query))
149+
assert.Equal(t, 0, len(tq))
150+
assert.Equal(t, 0, len(g))
151+
assert.NoError(t, gerr)
152+
}
153+
154+
func TestV1ListEmptyErr(t *testing.T) {
155+
tq := testV1Query[*int]{{nil, &ListMeta{}, errTest}}
156+
g, gerr := collectList(getV1List(nil, tq.query))
157+
assert.Equal(t, 0, len(tq))
158+
assert.Equal(t, 0, len(g))
159+
assert.Equal(t, errTest, gerr)
160+
}
161+
162+
func TestV1ListOne(t *testing.T) {
163+
tq := testV1Query[*int]{{[]*int{intPtr(1)}, &ListMeta{}, nil}}
164+
want := []*int{intPtr(1)}
165+
g, gerr := collectList(getV1List(nil, tq.query))
166+
assert.Equal(t, 0, len(tq))
167+
assert.Equal(t, want, g)
168+
assert.NoError(t, gerr)
169+
}
170+
171+
func TestV1ListOneErr(t *testing.T) {
172+
tq := testV1Query[*int]{{[]*int{intPtr(1)}, &ListMeta{}, errTest}}
173+
want := []*int{intPtr(1)}
174+
g, gerr := collectList(getV1List(nil, tq.query))
175+
assert.Equal(t, 0, len(tq))
176+
assert.Equal(t, want, g)
177+
assert.Equal(t, errTest, gerr)
178+
}
179+
180+
func TestV1ListPage2EmptyErr(t *testing.T) {
181+
tq := testV1Query[*item]{
182+
{[]*item{{"x"}}, &ListMeta{HasMore: true, TotalCount: 0, URL: ""}, nil},
183+
{nil, &ListMeta{}, errTest},
184+
}
185+
want := []*item{{"x"}}
186+
g, gerr := collectList(getV1List(nil, tq.query))
187+
assert.Equal(t, 0, len(tq))
188+
assert.Equal(t, want, g)
189+
assert.Equal(t, errTest, gerr)
190+
}
191+
192+
func TestV1ListTwoPages(t *testing.T) {
193+
tq := testV1Query[*item]{
194+
{[]*item{{"x"}}, &ListMeta{HasMore: true, TotalCount: 0, URL: ""}, nil},
195+
{[]*item{{"y"}}, &ListMeta{}, nil},
196+
}
197+
want := []*item{{"x"}, {"y"}}
198+
g, gerr := collectList(getV1List(nil, tq.query))
199+
assert.Equal(t, 0, len(tq))
200+
assert.Equal(t, want, g)
201+
assert.NoError(t, gerr)
202+
}
203+
204+
func TestV1ListTwoPagesErr(t *testing.T) {
205+
tq := testV1Query[*item]{
206+
{[]*item{{"x"}}, &ListMeta{HasMore: true, TotalCount: 0, URL: ""}, nil},
207+
{[]*item{{"y"}}, &ListMeta{}, errTest},
208+
}
209+
want := []*item{{"x"}, {"y"}}
210+
g, gerr := collectList(getV1List(nil, tq.query))
211+
assert.Equal(t, 0, len(tq))
212+
assert.Equal(t, want, g)
213+
assert.Equal(t, errTest, gerr)
214+
}
215+
216+
func TestV1ListReversed(t *testing.T) {
217+
tq := testV1Query[*int]{{[]*int{intPtr(1), intPtr(2)}, &ListMeta{}, nil}}
218+
want := []*int{intPtr(2), intPtr(1)}
219+
g, gerr := collectList(getV1List(&ListParams{EndingBefore: String("x")}, tq.query))
220+
assert.Equal(t, 0, len(tq))
221+
assert.Equal(t, want, g)
222+
assert.NoError(t, gerr)
223+
}
224+
225+
func TestV1ListReversedTwoPages(t *testing.T) {
226+
tq := testV1Query[*item]{
227+
{[]*item{&item{"3"}, &item{"4"}}, &ListMeta{HasMore: true, TotalCount: 0, URL: ""}, nil},
228+
{[]*item{&item{"1"}, &item{"2"}}, &ListMeta{}, nil},
229+
}
230+
want := []*item{&item{"4"}, &item{"3"}, &item{"2"}, &item{"1"}}
231+
g, gerr := collectList(getV1List(&ListParams{EndingBefore: String("x")}, tq.query))
232+
assert.Equal(t, 0, len(tq))
233+
assert.Equal(t, want, g)
234+
assert.NoError(t, gerr)
235+
}
236+
146237
//
147238
// ---
148239
//
@@ -165,6 +256,38 @@ func (tq *testQuery) query(*Params, *form.Values) ([]interface{}, ListContainer,
165256
return x.v, x.m, x.e
166257
}
167258

259+
type testV1Query[T any] []struct {
260+
v []T
261+
m ListContainer
262+
e error
263+
}
264+
265+
func (tq *testV1Query[T]) query(*Params, *form.Values) ([]T, ListContainer, error) {
266+
x := (*tq)[0]
267+
*tq = (*tq)[1:]
268+
return x.v, x.m, x.e
269+
}
270+
271+
func collectList[T any](it *v1List[*T]) ([]*T, error) {
272+
var tt []*T
273+
var err error
274+
it.All()(func(t *T, e error) bool {
275+
if e != nil {
276+
err = e
277+
return false
278+
}
279+
tt = append(tt, t)
280+
return true
281+
})
282+
return tt, err
283+
}
284+
285+
func intPtr(i int) *int {
286+
intPtr := new(int)
287+
*intPtr = i
288+
return intPtr
289+
}
290+
168291
type collectable interface {
169292
Next() bool
170293
Current() interface{}

0 commit comments

Comments
 (0)