Verified Commit f821c08c authored by Florian Forster's avatar Florian Forster
Browse files

feat(pagination): Add `ScanAndCollectN` to collect at most _n_ results.

Introduce `ScanAndCollectN`, which works like `ScanAndCollect` but stops
collecting once _n_ items have been gathered. Negative values of _n_ retain the
existing "collect all" behaviour, allowing `ScanAndCollect` to delegate to it.
This allows capping search results before they overwhelm callers.

Add an example function demonstrating how to use `ScanAndCollectN` with a note
directing users toward `Scan2` when iteration is preferred over collecting into
a slice.
parent 87321a15
Loading
Loading
Loading
Loading
+34 −7
Original line number Diff line number Diff line
@@ -3,7 +3,6 @@ package gitlab
import (
	"fmt"
	"iter"
	"slices"
)

type PaginationOptionFunc = RequestOptionFunc
@@ -154,7 +153,7 @@ func Must[T any](it iter.Seq2[T, error]) iter.Seq[T] {
	}
}

// ScanAndCollect is a convenience function that collects all results and returns them as slice as well as an error if one happens.
// ScanAndCollect is a convenience function that collects all results and returns them as a slice as well as an error if one happens.
//
//	opts := &ListProjectsOptions{}
//	projects, err := ScanAndCollect(func(p PaginationOptionFunc) ([]*Project, *Response, error) {
@@ -167,10 +166,38 @@ func Must[T any](it iter.Seq2[T, error]) iter.Seq[T] {
//
// Attention: This API is experimental and may be subject to breaking changes to improve the API in the future.
func ScanAndCollect[T any](f func(p PaginationOptionFunc) ([]T, *Response, error)) ([]T, error) {
	it, hasErr := Scan(f)
	allItems := slices.Collect(it)
	if err := hasErr(); err != nil {
	return ScanAndCollectN(f, -1)
}

// ScanAndCollectN is a convenience function that collects at most n results and
// returns them as a slice as well as an error if one happens.
//
// This is useful when you need a slice, e.g. for marshaling the data
// structures, passing the data to a function expecting a slice, or implementing
// custom sorting logic. If you want to iterate over all items, the iterator
// returned by [Scan2] is a more memory efficient alternative.
//
// n determines the number of items to collect:
//   - n > 0: at most n items are returned
//   - n == 0: the result is a nil slice (zero items)
//   - n < 0: all items  are returned (no limit)
//
// Attention: This API is experimental and may be subject to breaking changes to
// improve the API in the future.
func ScanAndCollectN[T any](f func(p PaginationOptionFunc) ([]T, *Response, error), n int) ([]T, error) {
	var items []T

	for item, err := range Scan2(f) {
		if err != nil {
			return nil, err
		}
	return allItems, nil

		if n >= 0 && len(items) >= n {
			break
		}

		items = append(items, item)
	}

	return items, nil
}
+42 −0
Original line number Diff line number Diff line
package gitlab_test

import (
	"encoding/json"
	"os"

	gitlab "gitlab.com/gitlab-org/api/client-go"
)

func ExampleScanAndCollectN() {
	// Create a client (this would normally use your GitLab instance URL and token)
	client, err := gitlab.NewAuthSourceClient(
		gitlab.AccessTokenAuthSource{"your-token"},
		gitlab.WithBaseURL("https://gitlab.example.com/api/v4"),
	)
	if err != nil {
		// Handle the error
		panic(err)
	}

	opts := &gitlab.ListProjectsOptions{}

	pager := func(pageOpt gitlab.PaginationOptionFunc) ([]*gitlab.Project, *gitlab.Response, error) {
		// Call ListProjects with pageOpt to retrieve the next page
		return client.Projects.ListProjects(opts, pageOpt)
	}

	// Retrieve at most 42 projects
	const limit = 42

	projects, err := gitlab.ScanAndCollectN(pager, limit)
	if err != nil {
		// Handle the error
		panic(err)
	}

	// Use the slice — here we serialize it to JSON, but you could sort it, pass it to another function, etc.
	// Note: if you want to iterate over items, use gitlab.Scan2() instead
	if err := json.NewEncoder(os.Stdout).Encode(projects); err != nil {
		panic(err)
	}
}
+29 −0
Original line number Diff line number Diff line
@@ -186,6 +186,35 @@ func TestPagination_ScanAndCollect_Error(t *testing.T) {
	require.Nil(t, projects)
}

func TestPagination_ScanAndCollectN(t *testing.T) {
	t.Parallel()

	mux, client := setup(t)
	handleTwoPagesSuccessfully(t, mux)

	opt := &ListProjectsOptions{}
	projects, err := ScanAndCollectN(func(p PaginationOptionFunc) ([]*Project, *Response, error) {
		return client.Projects.ListProjects(opt, p)
	}, 1)
	require.NoError(t, err)
	want := []*Project{{ID: 1}}
	assert.Equal(t, want, projects)
}

func TestPagination_ScanAndCollectN_Zero(t *testing.T) {
	t.Parallel()

	mux, client := setup(t)
	handleTwoPagesSuccessfully(t, mux)

	opt := &ListProjectsOptions{}
	projects, err := ScanAndCollectN(func(p PaginationOptionFunc) ([]*Project, *Response, error) {
		return client.Projects.ListProjects(opt, p)
	}, 0)
	require.NoError(t, err)
	assert.Nil(t, projects)
}

func handleTwoPagesSuccessfully(t *testing.T, mux *http.ServeMux) {
	mux.HandleFunc("GET /api/v4/projects", func(w http.ResponseWriter, r *http.Request) {
		page := r.URL.Query().Get("page")