Add ListIter in pagination + generic Resource.Iter for streaming paginated results as iter.Seq2[T, error]. Add Iter* methods across all service files (actions, admin, branches, issues, labels, notifs, orgs, packages, pulls, releases, repos, teams, users, webhooks). Modernise cmd/forgegen with slices.Sort, maps.Keys, strings.FieldsFuncSeq. Co-Authored-By: Gemini <noreply@google.com> Co-Authored-By: Virgil <virgil@lethean.io>
132 lines
3 KiB
Go
132 lines
3 KiB
Go
package forge
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"iter"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
)
|
|
|
|
// ListOptions controls pagination.
|
|
type ListOptions struct {
|
|
Page int // 1-based page number
|
|
Limit int // items per page (default 50)
|
|
}
|
|
|
|
// DefaultList returns sensible default pagination.
|
|
var DefaultList = ListOptions{Page: 1, Limit: 50}
|
|
|
|
// PagedResult holds a single page of results with metadata.
|
|
type PagedResult[T any] struct {
|
|
Items []T
|
|
TotalCount int
|
|
Page int
|
|
HasMore bool
|
|
}
|
|
|
|
// ListPage fetches a single page of results.
|
|
// Extra query params can be passed via the query map.
|
|
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 = 50
|
|
}
|
|
|
|
u, err := url.Parse(c.baseURL + path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("forge: parse url: %w", 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()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("forge: create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Authorization", "token "+c.token)
|
|
req.Header.Set("Accept", "application/json")
|
|
if c.userAgent != "" {
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("forge: request GET %s: %w", path, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return nil, c.parseError(resp, path)
|
|
}
|
|
|
|
var items []T
|
|
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
|
|
return nil, fmt.Errorf("forge: decode response: %w", err)
|
|
}
|
|
|
|
totalCount, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
|
|
|
|
return &PagedResult[T]{
|
|
Items: items,
|
|
TotalCount: totalCount,
|
|
Page: opts.Page,
|
|
HasMore: len(items) >= opts.Limit && opts.Page*opts.Limit < totalCount,
|
|
}, nil
|
|
}
|
|
|
|
// ListAll fetches all pages of results.
|
|
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: 50})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
all = append(all, result.Items...)
|
|
if len(result.Items) == 0 || len(all) >= result.TotalCount {
|
|
break
|
|
}
|
|
page++
|
|
}
|
|
|
|
return all, nil
|
|
}
|
|
|
|
// ListIter returns an iterator over all resources across all pages.
|
|
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
|
|
count := 0
|
|
for {
|
|
result, err := ListPage[T](ctx, c, path, query, ListOptions{Page: page, Limit: 50})
|
|
if err != nil {
|
|
yield(*new(T), err)
|
|
return
|
|
}
|
|
for _, item := range result.Items {
|
|
if !yield(item, nil) {
|
|
return
|
|
}
|
|
count++
|
|
}
|
|
if len(result.Items) == 0 || count >= result.TotalCount {
|
|
break
|
|
}
|
|
page++
|
|
}
|
|
}
|
|
}
|