From 57d8af13adb87d38cbff2e5b8afbbb7752e12535 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 23 Feb 2026 05:39:07 +0000 Subject: [PATCH] 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 Co-Authored-By: Virgil --- actions.go | 25 ++++++++++++++++++++++++ admin.go | 16 ++++++++++++++++ branches.go | 7 +++++++ cmd/forgegen/generator.go | 40 ++++++++++++--------------------------- cmd/forgegen/parser.go | 22 ++++++++++----------- issues.go | 7 +++++++ labels.go | 13 +++++++++++++ notifications.go | 12 ++++++++++++ notifications_test.go | 1 - orgs.go | 18 ++++++++++++++++++ packages.go | 13 +++++++++++++ pagination.go | 26 +++++++++++++++++++++++++ pagination_test.go | 30 +++++++++++++++++++++++++++++ pulls.go | 7 +++++++ releases.go | 7 +++++++ repos.go | 11 +++++++++++ resource.go | 10 +++++++++- teams.go | 19 +++++++++++++++++++ users.go | 19 +++++++++++++++++++ webhooks.go | 7 +++++++ wiki_test.go | 6 +++--- 21 files changed, 272 insertions(+), 44 deletions(-) diff --git a/actions.go b/actions.go index cc11abc..dad82c0 100644 --- a/actions.go +++ b/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) diff --git a/admin.go b/admin.go index 214fa89..637d201 100644 --- a/admin.go +++ b/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) diff --git a/branches.go b/branches.go index 116c346..ab2e903 100644 --- a/branches.go +++ b/branches.go @@ -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) diff --git a/cmd/forgegen/generator.go b/cmd/forgegen/generator.go index 60f27bf..e8aa786 100644 --- a/cmd/forgegen/generator.go +++ b/cmd/forgegen/generator.go @@ -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, diff --git a/cmd/forgegen/parser.go b/cmd/forgegen/parser.go index 3d4874a..37e512e 100644 --- a/cmd/forgegen/parser.go +++ b/cmd/forgegen/parser.go @@ -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, "") diff --git a/issues.go b/issues.go index 5e0972c..11dc441 100644 --- a/issues.go +++ b/issues.go @@ -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) diff --git a/labels.go b/labels.go index 6a61260..5f8d4a8 100644 --- a/labels.go +++ b/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) diff --git a/notifications.go b/notifications.go index 10906b3..ccefaae 100644 --- a/notifications.go +++ b/notifications.go @@ -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) diff --git a/notifications_test.go b/notifications_test.go index 2a87421..e461996 100644 --- a/notifications_test.go +++ b/notifications_test.go @@ -161,4 +161,3 @@ func TestNotificationService_Bad_NotFound(t *testing.T) { t.Errorf("expected not-found error, got %v", err) } } - diff --git a/orgs.go b/orgs.go index 0c3f180..7c4ed27 100644 --- a/orgs.go +++ b/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) +} diff --git a/packages.go b/packages.go index a0f4c02..4fc9133 100644 --- a/packages.go +++ b/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) +} diff --git a/pagination.go b/pagination.go index 5bb530a..d5f9cc6 100644 --- a/pagination.go +++ b/pagination.go @@ -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++ + } + } +} diff --git a/pagination_test.go b/pagination_test.go index f25415b..61e047e 100644 --- a/pagination_test.go +++ b/pagination_test.go @@ -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") diff --git a/pulls.go b/pulls.go index e83d076..1bdf582 100644 --- a/pulls.go +++ b/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) diff --git a/releases.go b/releases.go index bfaeb1f..ef3287c 100644 --- a/releases.go +++ b/releases.go @@ -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) diff --git a/repos.go b/repos.go index f8610e7..8bfb39e 100644 --- a/repos.go +++ b/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{} diff --git a/resource.go b/resource.go index 5c222fb..04f073c 100644 --- a/resource.go +++ b/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 diff --git a/teams.go b/teams.go index e677495..cdd81f3 100644 --- a/teams.go +++ b/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) +} diff --git a/users.go b/users.go index 7da6db4..efaa13b 100644 --- a/users.go +++ b/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) diff --git a/webhooks.go b/webhooks.go index 4c802a3..8a55026 100644 --- a/webhooks.go +++ b/webhooks.go @@ -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) +} diff --git a/wiki_test.go b/wiki_test.go index 758e0fd..58138e2 100644 --- a/wiki_test.go +++ b/wiki_test.go @@ -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)