feat: modernise to Go 1.26 iterators and stdlib helpers
All checks were successful
Security Scan / security (push) Successful in 14s
Test / test (push) Successful in 2m1s

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:
Snider 2026-02-23 05:39:07 +00:00
parent 5ac4fc75ef
commit 57d8af13ad
21 changed files with 272 additions and 44 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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,

View file

@ -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, "")

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -161,4 +161,3 @@ func TestNotificationService_Bad_NotFound(t *testing.T) {
t.Errorf("expected not-found error, got %v", err)
}
}

18
orgs.go
View file

@ -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)
}

View file

@ -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)
}

View file

@ -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++
}
}
}

View file

@ -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")

View file

@ -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)

View file

@ -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)

View file

@ -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{}

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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)