feat: modernise to Go 1.26 iterators and stdlib helpers
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>
This commit is contained in:
parent
5ac4fc75ef
commit
57d8af13ad
21 changed files with 272 additions and 44 deletions
25
actions.go
25
actions.go
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -24,6 +25,12 @@ func (s *ActionsService) ListRepoSecrets(ctx context.Context, owner, repo string
|
|||
return ListAll[types.Secret](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterRepoSecrets returns an iterator over all secrets for a repository.
|
||||
func (s *ActionsService) IterRepoSecrets(ctx context.Context, owner, repo string) iter.Seq2[types.Secret, error] {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets", owner, repo)
|
||||
return ListIter[types.Secret](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateRepoSecret creates or updates a secret in a repository.
|
||||
// Forgejo expects a PUT with {"data": "secret-value"} body.
|
||||
func (s *ActionsService) CreateRepoSecret(ctx context.Context, owner, repo, name string, data string) error {
|
||||
|
|
@ -44,6 +51,12 @@ func (s *ActionsService) ListRepoVariables(ctx context.Context, owner, repo stri
|
|||
return ListAll[types.ActionVariable](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterRepoVariables returns an iterator over all action variables for a repository.
|
||||
func (s *ActionsService) IterRepoVariables(ctx context.Context, owner, repo string) iter.Seq2[types.ActionVariable, error] {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables", owner, repo)
|
||||
return ListIter[types.ActionVariable](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateRepoVariable creates a new action variable in a repository.
|
||||
// Forgejo expects a POST with {"value": "var-value"} body.
|
||||
func (s *ActionsService) CreateRepoVariable(ctx context.Context, owner, repo, name, value string) error {
|
||||
|
|
@ -64,12 +77,24 @@ func (s *ActionsService) ListOrgSecrets(ctx context.Context, org string) ([]type
|
|||
return ListAll[types.Secret](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterOrgSecrets returns an iterator over all secrets for an organisation.
|
||||
func (s *ActionsService) IterOrgSecrets(ctx context.Context, org string) iter.Seq2[types.Secret, error] {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/actions/secrets", org)
|
||||
return ListIter[types.Secret](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// ListOrgVariables returns all action variables for an organisation.
|
||||
func (s *ActionsService) ListOrgVariables(ctx context.Context, org string) ([]types.ActionVariable, error) {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/actions/variables", org)
|
||||
return ListAll[types.ActionVariable](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterOrgVariables returns an iterator over all action variables for an organisation.
|
||||
func (s *ActionsService) IterOrgVariables(ctx context.Context, org string) iter.Seq2[types.ActionVariable, error] {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/actions/variables", org)
|
||||
return ListIter[types.ActionVariable](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// DispatchWorkflow triggers a workflow run.
|
||||
func (s *ActionsService) DispatchWorkflow(ctx context.Context, owner, repo, workflow string, opts map[string]any) error {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/dispatches", owner, repo, workflow)
|
||||
|
|
|
|||
16
admin.go
16
admin.go
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -23,6 +24,11 @@ func (s *AdminService) ListUsers(ctx context.Context) ([]types.User, error) {
|
|||
return ListAll[types.User](ctx, s.client, "/api/v1/admin/users", nil)
|
||||
}
|
||||
|
||||
// IterUsers returns an iterator over all users (admin only).
|
||||
func (s *AdminService) IterUsers(ctx context.Context) iter.Seq2[types.User, error] {
|
||||
return ListIter[types.User](ctx, s.client, "/api/v1/admin/users", nil)
|
||||
}
|
||||
|
||||
// CreateUser creates a new user (admin only).
|
||||
func (s *AdminService) CreateUser(ctx context.Context, opts *types.CreateUserOption) (*types.User, error) {
|
||||
var out types.User
|
||||
|
|
@ -55,6 +61,11 @@ func (s *AdminService) ListOrgs(ctx context.Context) ([]types.Organization, erro
|
|||
return ListAll[types.Organization](ctx, s.client, "/api/v1/admin/orgs", nil)
|
||||
}
|
||||
|
||||
// IterOrgs returns an iterator over all organisations (admin only).
|
||||
func (s *AdminService) IterOrgs(ctx context.Context) iter.Seq2[types.Organization, error] {
|
||||
return ListIter[types.Organization](ctx, s.client, "/api/v1/admin/orgs", nil)
|
||||
}
|
||||
|
||||
// RunCron runs a cron task by name (admin only).
|
||||
func (s *AdminService) RunCron(ctx context.Context, task string) error {
|
||||
path := fmt.Sprintf("/api/v1/admin/cron/%s", task)
|
||||
|
|
@ -66,6 +77,11 @@ func (s *AdminService) ListCron(ctx context.Context) ([]types.Cron, error) {
|
|||
return ListAll[types.Cron](ctx, s.client, "/api/v1/admin/cron", nil)
|
||||
}
|
||||
|
||||
// IterCron returns an iterator over all cron tasks (admin only).
|
||||
func (s *AdminService) IterCron(ctx context.Context) iter.Seq2[types.Cron, error] {
|
||||
return ListIter[types.Cron](ctx, s.client, "/api/v1/admin/cron", nil)
|
||||
}
|
||||
|
||||
// AdoptRepo adopts an unadopted repository (admin only).
|
||||
func (s *AdminService) AdoptRepo(ctx context.Context, owner, repo string) error {
|
||||
path := fmt.Sprintf("/api/v1/admin/unadopted/%s/%s", owner, repo)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -26,6 +27,12 @@ func (s *BranchService) ListBranchProtections(ctx context.Context, owner, repo s
|
|||
return ListAll[types.BranchProtection](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterBranchProtections returns an iterator over all branch protections for a repository.
|
||||
func (s *BranchService) IterBranchProtections(ctx context.Context, owner, repo string) iter.Seq2[types.BranchProtection, error] {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
|
||||
return ListIter[types.BranchProtection](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// GetBranchProtection returns a single branch protection by name.
|
||||
func (s *BranchService) GetBranchProtection(ctx context.Context, owner, repo, name string) (*types.BranchProtection, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
|
@ -149,14 +150,7 @@ func classifyType(name string) string {
|
|||
// sanitiseLine collapses a multi-line string into a single line,
|
||||
// replacing newlines and consecutive whitespace with a single space.
|
||||
func sanitiseLine(s string) string {
|
||||
s = strings.ReplaceAll(s, "\r\n", " ")
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
s = strings.ReplaceAll(s, "\r", " ")
|
||||
// Collapse multiple spaces.
|
||||
for strings.Contains(s, " ") {
|
||||
s = strings.ReplaceAll(s, " ", " ")
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
|
||||
// enumConstName generates a Go constant name for an enum value.
|
||||
|
|
@ -223,17 +217,14 @@ func Generate(types map[string]*GoType, pairs []CRUDPair, outDir string) error {
|
|||
|
||||
// Sort types within each group for deterministic output.
|
||||
for file := range groups {
|
||||
sort.Slice(groups[file], func(i, j int) bool {
|
||||
return groups[file][i].Name < groups[file][j].Name
|
||||
slices.SortFunc(groups[file], func(a, b *GoType) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
}
|
||||
|
||||
// Write each group to its own file.
|
||||
fileNames := make([]string, 0, len(groups))
|
||||
for file := range groups {
|
||||
fileNames = append(fileNames, file)
|
||||
}
|
||||
sort.Strings(fileNames)
|
||||
fileNames := slices.Collect(maps.Keys(groups))
|
||||
slices.Sort(fileNames)
|
||||
|
||||
for _, file := range fileNames {
|
||||
outPath := filepath.Join(outDir, file+".go")
|
||||
|
|
@ -247,18 +238,11 @@ func Generate(types map[string]*GoType, pairs []CRUDPair, outDir string) error {
|
|||
|
||||
// writeFile renders and writes a single Go source file for the given types.
|
||||
func writeFile(path string, types []*GoType) error {
|
||||
needTime := false
|
||||
for _, gt := range types {
|
||||
for _, f := range gt.Fields {
|
||||
if strings.Contains(f.GoType, "time.Time") {
|
||||
needTime = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if needTime {
|
||||
break
|
||||
}
|
||||
}
|
||||
needTime := slices.ContainsFunc(types, func(gt *GoType) bool {
|
||||
return slices.ContainsFunc(gt.Fields, func(f GoField) bool {
|
||||
return strings.Contains(f.GoType, "time.Time")
|
||||
})
|
||||
})
|
||||
|
||||
data := templateData{
|
||||
NeedTime: needTime,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ func ExtractTypes(spec *Spec) map[string]*GoType {
|
|||
for _, v := range def.Enum {
|
||||
gt.EnumValues = append(gt.EnumValues, fmt.Sprintf("%v", v))
|
||||
}
|
||||
sort.Strings(gt.EnumValues)
|
||||
slices.Sort(gt.EnumValues)
|
||||
result[name] = gt
|
||||
continue
|
||||
}
|
||||
|
|
@ -113,8 +113,8 @@ func ExtractTypes(spec *Spec) map[string]*GoType {
|
|||
}
|
||||
gt.Fields = append(gt.Fields, gf)
|
||||
}
|
||||
sort.Slice(gt.Fields, func(i, j int) bool {
|
||||
return gt.Fields[i].GoName < gt.Fields[j].GoName
|
||||
slices.SortFunc(gt.Fields, func(a, b GoField) int {
|
||||
return strings.Compare(a.GoName, b.GoName)
|
||||
})
|
||||
result[name] = gt
|
||||
}
|
||||
|
|
@ -138,8 +138,8 @@ func DetectCRUDPairs(spec *Spec) []CRUDPair {
|
|||
}
|
||||
pairs = append(pairs, pair)
|
||||
}
|
||||
sort.Slice(pairs, func(i, j int) bool {
|
||||
return pairs[i].Base < pairs[j].Base
|
||||
slices.SortFunc(pairs, func(a, b CRUDPair) int {
|
||||
return strings.Compare(a.Base, b.Base)
|
||||
})
|
||||
return pairs
|
||||
}
|
||||
|
|
@ -193,19 +193,19 @@ func resolveGoType(prop SchemaProperty) string {
|
|||
// pascalCase converts a snake_case or kebab-case string to PascalCase,
|
||||
// with common acronyms kept uppercase.
|
||||
func pascalCase(s string) string {
|
||||
parts := strings.FieldsFunc(s, func(r rune) bool {
|
||||
var parts []string
|
||||
for p := range strings.FieldsFuncSeq(s, func(r rune) bool {
|
||||
return r == '_' || r == '-'
|
||||
})
|
||||
for i, p := range parts {
|
||||
}) {
|
||||
if len(p) == 0 {
|
||||
continue
|
||||
}
|
||||
upper := strings.ToUpper(p)
|
||||
switch upper {
|
||||
case "ID", "URL", "HTML", "SSH", "HTTP", "HTTPS", "API", "URI", "GPG", "IP", "CSS", "JS":
|
||||
parts[i] = upper
|
||||
parts = append(parts, upper)
|
||||
default:
|
||||
parts[i] = strings.ToUpper(p[:1]) + p[1:]
|
||||
parts = append(parts, strings.ToUpper(p[:1])+p[1:])
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -84,6 +85,12 @@ func (s *IssueService) ListComments(ctx context.Context, owner, repo string, ind
|
|||
return ListAll[types.Comment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterComments returns an iterator over all comments on an issue.
|
||||
func (s *IssueService) IterComments(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Comment, error] {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
|
||||
return ListIter[types.Comment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateComment creates a comment on an issue.
|
||||
func (s *IssueService) CreateComment(ctx context.Context, owner, repo string, index int64, body string) (*types.Comment, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
|
||||
|
|
|
|||
13
labels.go
13
labels.go
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -23,6 +24,12 @@ func (s *LabelService) ListRepoLabels(ctx context.Context, owner, repo string) (
|
|||
return ListAll[types.Label](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterRepoLabels returns an iterator over all labels for a repository.
|
||||
func (s *LabelService) IterRepoLabels(ctx context.Context, owner, repo string) iter.Seq2[types.Label, error] {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
|
||||
return ListIter[types.Label](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// GetRepoLabel returns a single label by ID.
|
||||
func (s *LabelService) GetRepoLabel(ctx context.Context, owner, repo string, id int64) (*types.Label, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
|
||||
|
|
@ -65,6 +72,12 @@ func (s *LabelService) ListOrgLabels(ctx context.Context, org string) ([]types.L
|
|||
return ListAll[types.Label](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterOrgLabels returns an iterator over all labels for an organisation.
|
||||
func (s *LabelService) IterOrgLabels(ctx context.Context, org string) iter.Seq2[types.Label, error] {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/labels", org)
|
||||
return ListIter[types.Label](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// CreateOrgLabel creates a new label in an organisation.
|
||||
func (s *LabelService) CreateOrgLabel(ctx context.Context, org string, opts *types.CreateLabelOption) (*types.Label, error) {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/labels", org)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -22,12 +23,23 @@ func (s *NotificationService) List(ctx context.Context) ([]types.NotificationThr
|
|||
return ListAll[types.NotificationThread](ctx, s.client, "/api/v1/notifications", nil)
|
||||
}
|
||||
|
||||
// Iter returns an iterator over all notifications for the authenticated user.
|
||||
func (s *NotificationService) Iter(ctx context.Context) iter.Seq2[types.NotificationThread, error] {
|
||||
return ListIter[types.NotificationThread](ctx, s.client, "/api/v1/notifications", nil)
|
||||
}
|
||||
|
||||
// ListRepo returns all notifications for a specific repository.
|
||||
func (s *NotificationService) ListRepo(ctx context.Context, owner, repo string) ([]types.NotificationThread, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/notifications", owner, repo)
|
||||
return ListAll[types.NotificationThread](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterRepo returns an iterator over all notifications for a specific repository.
|
||||
func (s *NotificationService) IterRepo(ctx context.Context, owner, repo string) iter.Seq2[types.NotificationThread, error] {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/notifications", owner, repo)
|
||||
return ListIter[types.NotificationThread](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// MarkRead marks all notifications as read.
|
||||
func (s *NotificationService) MarkRead(ctx context.Context) error {
|
||||
return s.client.Put(ctx, "/api/v1/notifications", nil, nil)
|
||||
|
|
|
|||
|
|
@ -161,4 +161,3 @@ func TestNotificationService_Bad_NotFound(t *testing.T) {
|
|||
t.Errorf("expected not-found error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
18
orgs.go
18
orgs.go
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -26,6 +27,12 @@ func (s *OrgService) ListMembers(ctx context.Context, org string) ([]types.User,
|
|||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterMembers returns an iterator over all members of an organisation.
|
||||
func (s *OrgService) IterMembers(ctx context.Context, org string) iter.Seq2[types.User, error] {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/members", org)
|
||||
return ListIter[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// AddMember adds a user to an organisation.
|
||||
func (s *OrgService) AddMember(ctx context.Context, org, username string) error {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/members/%s", org, username)
|
||||
|
|
@ -44,7 +51,18 @@ func (s *OrgService) ListUserOrgs(ctx context.Context, username string) ([]types
|
|||
return ListAll[types.Organization](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterUserOrgs returns an iterator over all organisations for a user.
|
||||
func (s *OrgService) IterUserOrgs(ctx context.Context, username string) iter.Seq2[types.Organization, error] {
|
||||
path := fmt.Sprintf("/api/v1/users/%s/orgs", username)
|
||||
return ListIter[types.Organization](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// ListMyOrgs returns all organisations for the authenticated user.
|
||||
func (s *OrgService) ListMyOrgs(ctx context.Context) ([]types.Organization, error) {
|
||||
return ListAll[types.Organization](ctx, s.client, "/api/v1/user/orgs", nil)
|
||||
}
|
||||
|
||||
// IterMyOrgs returns an iterator over all organisations for the authenticated user.
|
||||
func (s *OrgService) IterMyOrgs(ctx context.Context) iter.Seq2[types.Organization, error] {
|
||||
return ListIter[types.Organization](ctx, s.client, "/api/v1/user/orgs", nil)
|
||||
}
|
||||
|
|
|
|||
13
packages.go
13
packages.go
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -23,6 +24,12 @@ func (s *PackageService) List(ctx context.Context, owner string) ([]types.Packag
|
|||
return ListAll[types.Package](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Iter returns an iterator over all packages for a given owner.
|
||||
func (s *PackageService) Iter(ctx context.Context, owner string) iter.Seq2[types.Package, error] {
|
||||
path := fmt.Sprintf("/api/v1/packages/%s", owner)
|
||||
return ListIter[types.Package](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Get returns a single package by owner, type, name, and version.
|
||||
func (s *PackageService) Get(ctx context.Context, owner, pkgType, name, version string) (*types.Package, error) {
|
||||
path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s", owner, pkgType, name, version)
|
||||
|
|
@ -44,3 +51,9 @@ func (s *PackageService) ListFiles(ctx context.Context, owner, pkgType, name, ve
|
|||
path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s/files", owner, pkgType, name, version)
|
||||
return ListAll[types.PackageFile](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterFiles returns an iterator over all files for a specific package version.
|
||||
func (s *PackageService) IterFiles(ctx context.Context, owner, pkgType, name, version string) iter.Seq2[types.PackageFile, error] {
|
||||
path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s/files", owner, pkgType, name, version)
|
||||
return ListIter[types.PackageFile](ctx, s.client, path, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"iter"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
|
@ -104,3 +105,28 @@ func ListAll[T any](ctx context.Context, c *Client, path string, query map[strin
|
|||
|
||||
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++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,36 @@ func TestPagination_Good_EmptyResult(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPagination_Good_Iter(t *testing.T) {
|
||||
page := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
page++
|
||||
w.Header().Set("X-Total-Count", "100")
|
||||
items := make([]map[string]int, 50)
|
||||
for i := range items {
|
||||
items[i] = map[string]int{"id": (page-1)*50 + i + 1}
|
||||
}
|
||||
json.NewEncoder(w).Encode(items)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "tok")
|
||||
count := 0
|
||||
for item, err := range ListIter[map[string]int](context.Background(), c, "/api/v1/repos", nil) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count++
|
||||
if item["id"] != count {
|
||||
t.Errorf("got id %d, want %d", item["id"], count)
|
||||
}
|
||||
}
|
||||
|
||||
if count != 100 {
|
||||
t.Errorf("got %d items, want 100", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPage_Good_QueryParams(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.URL.Query().Get("page")
|
||||
|
|
|
|||
7
pulls.go
7
pulls.go
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -39,6 +40,12 @@ func (s *PullService) ListReviews(ctx context.Context, owner, repo string, index
|
|||
return ListAll[types.PullReview](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterReviews returns an iterator over all reviews on a pull request.
|
||||
func (s *PullService) IterReviews(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.PullReview, error] {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
|
||||
return ListIter[types.PullReview](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// SubmitReview creates a new review on a pull request.
|
||||
func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, index int64, review map[string]any) (*types.PullReview, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -42,6 +43,12 @@ func (s *ReleaseService) ListAssets(ctx context.Context, owner, repo string, rel
|
|||
return ListAll[types.Attachment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterAssets returns an iterator over all assets for a release.
|
||||
func (s *ReleaseService) IterAssets(ctx context.Context, owner, repo string, releaseID int64) iter.Seq2[types.Attachment, error] {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner, repo, releaseID)
|
||||
return ListIter[types.Attachment](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// GetAsset returns a single asset for a release.
|
||||
func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) (*types.Attachment, error) {
|
||||
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", owner, repo, releaseID, assetID)
|
||||
|
|
|
|||
11
repos.go
11
repos.go
|
|
@ -2,6 +2,7 @@ package forge
|
|||
|
||||
import (
|
||||
"context"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -24,11 +25,21 @@ func (s *RepoService) ListOrgRepos(ctx context.Context, org string) ([]types.Rep
|
|||
return ListAll[types.Repository](ctx, s.client, "/api/v1/orgs/"+org+"/repos", nil)
|
||||
}
|
||||
|
||||
// IterOrgRepos returns an iterator over all repositories for an organisation.
|
||||
func (s *RepoService) IterOrgRepos(ctx context.Context, org string) iter.Seq2[types.Repository, error] {
|
||||
return ListIter[types.Repository](ctx, s.client, "/api/v1/orgs/"+org+"/repos", nil)
|
||||
}
|
||||
|
||||
// ListUserRepos returns all repositories for the authenticated user.
|
||||
func (s *RepoService) ListUserRepos(ctx context.Context) ([]types.Repository, error) {
|
||||
return ListAll[types.Repository](ctx, s.client, "/api/v1/user/repos", nil)
|
||||
}
|
||||
|
||||
// IterUserRepos returns an iterator over all repositories for the authenticated user.
|
||||
func (s *RepoService) IterUserRepos(ctx context.Context) iter.Seq2[types.Repository, error] {
|
||||
return ListIter[types.Repository](ctx, s.client, "/api/v1/user/repos", nil)
|
||||
}
|
||||
|
||||
// Fork forks a repository. If org is non-empty, forks into that organisation.
|
||||
func (s *RepoService) Fork(ctx context.Context, owner, repo, org string) (*types.Repository, error) {
|
||||
body := map[string]string{}
|
||||
|
|
|
|||
10
resource.go
10
resource.go
|
|
@ -1,6 +1,9 @@
|
|||
package forge
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"iter"
|
||||
)
|
||||
|
||||
// Resource provides generic CRUD operations for a Forgejo API resource.
|
||||
// T is the resource type, C is the create options type, U is the update options type.
|
||||
|
|
@ -25,6 +28,11 @@ func (r *Resource[T, C, U]) ListAll(ctx context.Context, params Params) ([]T, er
|
|||
return ListAll[T](ctx, r.client, ResolvePath(r.path, params), nil)
|
||||
}
|
||||
|
||||
// Iter returns an iterator over all resources across all pages.
|
||||
func (r *Resource[T, C, U]) Iter(ctx context.Context, params Params) iter.Seq2[T, error] {
|
||||
return ListIter[T](ctx, r.client, ResolvePath(r.path, params), nil)
|
||||
}
|
||||
|
||||
// Get returns a single resource by appending id to the path.
|
||||
func (r *Resource[T, C, U]) Get(ctx context.Context, params Params) (*T, error) {
|
||||
var out T
|
||||
|
|
|
|||
19
teams.go
19
teams.go
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -26,6 +27,12 @@ func (s *TeamService) ListMembers(ctx context.Context, teamID int64) ([]types.Us
|
|||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterMembers returns an iterator over all members of a team.
|
||||
func (s *TeamService) IterMembers(ctx context.Context, teamID int64) iter.Seq2[types.User, error] {
|
||||
path := fmt.Sprintf("/api/v1/teams/%d/members", teamID)
|
||||
return ListIter[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// AddMember adds a user to a team.
|
||||
func (s *TeamService) AddMember(ctx context.Context, teamID int64, username string) error {
|
||||
path := fmt.Sprintf("/api/v1/teams/%d/members/%s", teamID, username)
|
||||
|
|
@ -44,6 +51,12 @@ func (s *TeamService) ListRepos(ctx context.Context, teamID int64) ([]types.Repo
|
|||
return ListAll[types.Repository](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterRepos returns an iterator over all repositories managed by a team.
|
||||
func (s *TeamService) IterRepos(ctx context.Context, teamID int64) iter.Seq2[types.Repository, error] {
|
||||
path := fmt.Sprintf("/api/v1/teams/%d/repos", teamID)
|
||||
return ListIter[types.Repository](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// AddRepo adds a repository to a team.
|
||||
func (s *TeamService) AddRepo(ctx context.Context, teamID int64, org, repo string) error {
|
||||
path := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s", teamID, org, repo)
|
||||
|
|
@ -61,3 +74,9 @@ func (s *TeamService) ListOrgTeams(ctx context.Context, org string) ([]types.Tea
|
|||
path := fmt.Sprintf("/api/v1/orgs/%s/teams", org)
|
||||
return ListAll[types.Team](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterOrgTeams returns an iterator over all teams in an organisation.
|
||||
func (s *TeamService) IterOrgTeams(ctx context.Context, org string) iter.Seq2[types.Team, error] {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/teams", org)
|
||||
return ListIter[types.Team](ctx, s.client, path, nil)
|
||||
}
|
||||
|
|
|
|||
19
users.go
19
users.go
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -35,12 +36,24 @@ func (s *UserService) ListFollowers(ctx context.Context, username string) ([]typ
|
|||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterFollowers returns an iterator over all followers of a user.
|
||||
func (s *UserService) IterFollowers(ctx context.Context, username string) iter.Seq2[types.User, error] {
|
||||
path := fmt.Sprintf("/api/v1/users/%s/followers", username)
|
||||
return ListIter[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// ListFollowing returns all users that a user is following.
|
||||
func (s *UserService) ListFollowing(ctx context.Context, username string) ([]types.User, error) {
|
||||
path := fmt.Sprintf("/api/v1/users/%s/following", username)
|
||||
return ListAll[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterFollowing returns an iterator over all users that a user is following.
|
||||
func (s *UserService) IterFollowing(ctx context.Context, username string) iter.Seq2[types.User, error] {
|
||||
path := fmt.Sprintf("/api/v1/users/%s/following", username)
|
||||
return ListIter[types.User](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Follow follows a user as the authenticated user.
|
||||
func (s *UserService) Follow(ctx context.Context, username string) error {
|
||||
path := fmt.Sprintf("/api/v1/user/following/%s", username)
|
||||
|
|
@ -59,6 +72,12 @@ func (s *UserService) ListStarred(ctx context.Context, username string) ([]types
|
|||
return ListAll[types.Repository](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterStarred returns an iterator over all repositories starred by a user.
|
||||
func (s *UserService) IterStarred(ctx context.Context, username string) iter.Seq2[types.Repository, error] {
|
||||
path := fmt.Sprintf("/api/v1/users/%s/starred", username)
|
||||
return ListIter[types.Repository](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// Star stars a repository as the authenticated user.
|
||||
func (s *UserService) Star(ctx context.Context, owner, repo string) error {
|
||||
path := fmt.Sprintf("/api/v1/user/starred/%s/%s", owner, repo)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package forge
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"forge.lthn.ai/core/go-forge/types"
|
||||
)
|
||||
|
|
@ -32,3 +33,9 @@ func (s *WebhookService) ListOrgHooks(ctx context.Context, org string) ([]types.
|
|||
path := fmt.Sprintf("/api/v1/orgs/%s/hooks", org)
|
||||
return ListAll[types.Hook](ctx, s.client, path, nil)
|
||||
}
|
||||
|
||||
// IterOrgHooks returns an iterator over all webhooks for an organisation.
|
||||
func (s *WebhookService) IterOrgHooks(ctx context.Context, org string) iter.Seq2[types.Hook, error] {
|
||||
path := fmt.Sprintf("/api/v1/orgs/%s/hooks", org)
|
||||
return ListIter[types.Hook](ctx, s.client, path, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,9 +103,9 @@ func TestWikiService_Good_CreatePage(t *testing.T) {
|
|||
|
||||
f := NewForge(srv.URL, "tok")
|
||||
page, err := f.Wiki.CreatePage(context.Background(), "core", "go-forge", &types.CreateWikiPageOptions{
|
||||
Title: "Install",
|
||||
Title: "Install",
|
||||
ContentBase64: "IyBJbnN0YWxs",
|
||||
Message: "create install page",
|
||||
Message: "create install page",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -141,7 +141,7 @@ func TestWikiService_Good_EditPage(t *testing.T) {
|
|||
f := NewForge(srv.URL, "tok")
|
||||
page, err := f.Wiki.EditPage(context.Background(), "core", "go-forge", "Home", &types.CreateWikiPageOptions{
|
||||
ContentBase64: "dXBkYXRlZA==",
|
||||
Message: "update home page",
|
||||
Message: "update home page",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue