199 lines
4.4 KiB
Go
199 lines
4.4 KiB
Go
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++
|
|
}
|
|
}
|
|
}
|