go-forge/pagination.go

200 lines
4.4 KiB
Go
Raw Permalink Normal View History

package forge
import (
"context"
"iter"
"net/http"
"net/url"
"strconv"
core "dappco.re/go/core"
)
const defaultPageLimit = 50
// ListOptions controls pagination.
//
// Usage:
//
// opts := forge.ListOptions{Page: 1, Limit: 50}
// _ = opts
type ListOptions struct {
Page int // 1-based page number
Limit int // items per page (default 50)
}
// String returns a safe summary of the pagination options.
//
// Usage:
//
// _ = forge.DefaultList.String()
func (o ListOptions) String() string {
return core.Concat(
"forge.ListOptions{page=",
strconv.Itoa(o.Page),
", limit=",
strconv.Itoa(o.Limit),
"}",
)
}
// GoString returns a safe Go-syntax summary of the pagination options.
//
// Usage:
//
// _ = fmt.Sprintf("%#v", forge.DefaultList)
func (o ListOptions) GoString() string { return o.String() }
// DefaultList provides sensible default pagination.
//
// Usage:
//
// page, err := forge.ListPage[types.Repository](ctx, client, path, nil, forge.DefaultList)
// _ = page
var DefaultList = ListOptions{Page: 1, Limit: defaultPageLimit}
// PagedResult holds a single page of results with metadata.
//
// Usage:
//
// page, err := forge.ListPage[types.Repository](ctx, client, path, nil, forge.DefaultList)
// _ = page
type PagedResult[T any] struct {
Items []T
TotalCount int
Page int
HasMore bool
}
// String returns a safe summary of a page of results.
//
// Usage:
//
// page, _ := forge.ListPage[types.Repository](...)
// _ = page.String()
func (r PagedResult[T]) String() string {
items := 0
if r.Items != nil {
items = len(r.Items)
}
return core.Concat(
"forge.PagedResult{items=",
strconv.Itoa(items),
", totalCount=",
strconv.Itoa(r.TotalCount),
", page=",
strconv.Itoa(r.Page),
", hasMore=",
strconv.FormatBool(r.HasMore),
"}",
)
}
// GoString returns a safe Go-syntax summary of a page of results.
//
// Usage:
//
// _ = fmt.Sprintf("%#v", page)
func (r PagedResult[T]) GoString() string { return r.String() }
// ListPage fetches a single page of results.
// Extra query params can be passed via the query map.
//
// Usage:
//
// page, err := forge.ListPage[types.Repository](ctx, client, "/api/v1/user/repos", nil, forge.DefaultList)
// _ = page
func ListPage[T any](ctx context.Context, c *Client, path string, query map[string]string, opts ListOptions) (*PagedResult[T], error) {
if opts.Page < 1 {
opts.Page = 1
}
if opts.Limit < 1 {
opts.Limit = defaultPageLimit
}
u, err := url.Parse(path)
if err != nil {
return nil, core.E("ListPage", "forge: parse path", err)
}
q := u.Query()
q.Set("page", strconv.Itoa(opts.Page))
q.Set("limit", strconv.Itoa(opts.Limit))
for k, v := range query {
q.Set(k, v)
}
u.RawQuery = q.Encode()
var items []T
resp, err := c.doJSON(ctx, http.MethodGet, u.String(), nil, &items)
if err != nil {
return nil, err
}
totalCount, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
return &PagedResult[T]{
Items: items,
TotalCount: totalCount,
Page: opts.Page,
// If totalCount is provided, use it to determine if there are more items.
// Otherwise, assume there are more if we got a full page.
HasMore: (totalCount > 0 && (opts.Page-1)*opts.Limit+len(items) < totalCount) ||
(totalCount == 0 && len(items) >= opts.Limit),
}, nil
}
// ListAll fetches all pages of results.
//
// Usage:
//
// items, err := forge.ListAll[types.Repository](ctx, client, "/api/v1/user/repos", nil)
// _ = items
func ListAll[T any](ctx context.Context, c *Client, path string, query map[string]string) ([]T, error) {
var all []T
page := 1
for {
result, err := ListPage[T](ctx, c, path, query, ListOptions{Page: page, Limit: defaultPageLimit})
if err != nil {
return nil, err
}
all = append(all, result.Items...)
if !result.HasMore {
break
}
page++
}
return all, nil
}
// ListIter returns an iterator over all resources across all pages.
//
// Usage:
//
// for item, err := range forge.ListIter[types.Repository](ctx, client, "/api/v1/user/repos", nil) {
// _, _ = item, err
// }
func ListIter[T any](ctx context.Context, c *Client, path string, query map[string]string) iter.Seq2[T, error] {
return func(yield func(T, error) bool) {
page := 1
for {
result, err := ListPage[T](ctx, c, path, query, ListOptions{Page: page, Limit: defaultPageLimit})
if err != nil {
yield(*new(T), err)
return
}
for _, item := range result.Items {
if !yield(item, nil) {
return
}
}
if !result.HasMore {
break
}
page++
}
}
}