From db31014f5fc3dfbebaa85d640e4c6353b58dcef9 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 06:24:47 +0000 Subject: [PATCH] feat(forge): add iterator variants for list endpoints Co-Authored-By: Virgil --- admin.go | 32 ++++++++++++++++++++++++ admin_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ commits.go | 16 ++++++++++++ commits_test.go | 33 +++++++++++++++++++++++++ repos.go | 16 ++++++++++++ repos_test.go | 43 ++++++++++++++++++++++++++++++++ 6 files changed, 206 insertions(+) diff --git a/admin.go b/admin.go index ab35e03..3af3560 100644 --- a/admin.go +++ b/admin.go @@ -217,6 +217,22 @@ func (s *AdminService) ListQuotaGroups(ctx context.Context) ([]types.QuotaGroup, return ListAll[types.QuotaGroup](ctx, s.client, "/api/v1/admin/quota/groups", nil) } +// IterQuotaGroups returns an iterator over all available quota groups. +func (s *AdminService) IterQuotaGroups(ctx context.Context) iter.Seq2[types.QuotaGroup, error] { + return func(yield func(types.QuotaGroup, error) bool) { + groups, err := s.ListQuotaGroups(ctx) + if err != nil { + yield(*new(types.QuotaGroup), err) + return + } + for _, group := range groups { + if !yield(group, nil) { + return + } + } + } +} + // CreateQuotaGroup creates a new quota group. func (s *AdminService) CreateQuotaGroup(ctx context.Context, opts *types.CreateQuotaGroupOptions) (*types.QuotaGroup, error) { var out types.QuotaGroup @@ -283,6 +299,22 @@ func (s *AdminService) ListQuotaRules(ctx context.Context) ([]types.QuotaRuleInf return ListAll[types.QuotaRuleInfo](ctx, s.client, "/api/v1/admin/quota/rules", nil) } +// IterQuotaRules returns an iterator over all available quota rules. +func (s *AdminService) IterQuotaRules(ctx context.Context) iter.Seq2[types.QuotaRuleInfo, error] { + return func(yield func(types.QuotaRuleInfo, error) bool) { + rules, err := s.ListQuotaRules(ctx) + if err != nil { + yield(*new(types.QuotaRuleInfo), err) + return + } + for _, rule := range rules { + if !yield(rule, nil) { + return + } + } + } +} + // CreateQuotaRule creates a new quota rule. func (s *AdminService) CreateQuotaRule(ctx context.Context, opts *types.CreateQuotaRuleOptions) (*types.QuotaRuleInfo, error) { var out types.QuotaRuleInfo diff --git a/admin_test.go b/admin_test.go index fdae775..b6bc11f 100644 --- a/admin_test.go +++ b/admin_test.go @@ -411,6 +411,39 @@ func TestAdminService_ListQuotaGroups_Good(t *testing.T) { } } +func TestAdminService_IterQuotaGroups_Good(t *testing.T) { + var requests int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/admin/quota/groups" { + t.Errorf("wrong path: %s", r.URL.Path) + } + json.NewEncoder(w).Encode([]types.QuotaGroup{ + {Name: "default"}, + {Name: "premium"}, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + var got []string + for group, err := range f.Admin.IterQuotaGroups(context.Background()) { + if err != nil { + t.Fatal(err) + } + got = append(got, group.Name) + } + if requests != 1 { + t.Fatalf("expected 1 request, got %d", requests) + } + if len(got) != 2 || got[0] != "default" || got[1] != "premium" { + t.Fatalf("got %#v", got) + } +} + func TestAdminService_CreateQuotaGroup_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -606,6 +639,39 @@ func TestAdminService_ListQuotaRules_Good(t *testing.T) { } } +func TestAdminService_IterQuotaRules_Good(t *testing.T) { + var requests int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/admin/quota/rules" { + t.Errorf("wrong path: %s", r.URL.Path) + } + json.NewEncoder(w).Encode([]types.QuotaRuleInfo{ + {Name: "git"}, + {Name: "artifacts"}, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + var got []string + for rule, err := range f.Admin.IterQuotaRules(context.Background()) { + if err != nil { + t.Fatal(err) + } + got = append(got, rule.Name) + } + if requests != 1 { + t.Fatalf("expected 1 request, got %d", requests) + } + if len(got) != 2 || got[0] != "git" || got[1] != "artifacts" { + t.Fatalf("got %#v", got) + } +} + func TestAdminService_CreateQuotaRule_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/commits.go b/commits.go index 499d745..a7fd518 100644 --- a/commits.go +++ b/commits.go @@ -94,6 +94,22 @@ func (s *CommitService) ListStatuses(ctx context.Context, owner, repo, ref strin return out, nil } +// IterStatuses returns an iterator over all commit statuses for a given ref. +func (s *CommitService) IterStatuses(ctx context.Context, owner, repo, ref string) iter.Seq2[types.CommitStatus, error] { + return func(yield func(types.CommitStatus, error) bool) { + statuses, err := s.ListStatuses(ctx, owner, repo, ref) + if err != nil { + yield(*new(types.CommitStatus), err) + return + } + for _, status := range statuses { + if !yield(status, nil) { + return + } + } + } +} + // CreateStatus creates a new commit status for the given SHA. func (s *CommitService) CreateStatus(ctx context.Context, owner, repo, sha string, opts *types.CreateStatusOption) (*types.CommitStatus, error) { path := ResolvePath("/api/v1/repos/{owner}/{repo}/statuses/{sha}", pathParams("owner", owner, "repo", repo, "sha", sha)) diff --git a/commits_test.go b/commits_test.go index 96c54ef..ef9c09d 100644 --- a/commits_test.go +++ b/commits_test.go @@ -185,6 +185,39 @@ func TestCommitService_ListStatuses_Good(t *testing.T) { } } +func TestCommitService_IterStatuses_Good(t *testing.T) { + var requests int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/repos/core/go-forge/commits/abc123/statuses" { + t.Errorf("wrong path: %s", r.URL.Path) + } + json.NewEncoder(w).Encode([]types.CommitStatus{ + {ID: 1, Context: "ci/build"}, + {ID: 2, Context: "ci/test"}, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + var got []string + for status, err := range f.Commits.IterStatuses(context.Background(), "core", "go-forge", "abc123") { + if err != nil { + t.Fatal(err) + } + got = append(got, status.Context) + } + if requests != 1 { + t.Fatalf("expected 1 request, got %d", requests) + } + if len(got) != 2 || got[0] != "ci/build" || got[1] != "ci/test" { + t.Fatalf("got %#v", got) + } +} + func TestCommitService_CreateStatus_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/repos.go b/repos.go index bb94868..c283119 100644 --- a/repos.go +++ b/repos.go @@ -497,6 +497,22 @@ func (s *RepoService) ListIssueTemplates(ctx context.Context, owner, repo string return ListAll[types.IssueTemplate](ctx, s.client, path, nil) } +// IterIssueTemplates returns an iterator over all issue templates available for a repository. +func (s *RepoService) IterIssueTemplates(ctx context.Context, owner, repo string) iter.Seq2[types.IssueTemplate, error] { + return func(yield func(types.IssueTemplate, error) bool) { + templates, err := s.ListIssueTemplates(ctx, owner, repo) + if err != nil { + yield(*new(types.IssueTemplate), err) + return + } + for _, template := range templates { + if !yield(template, nil) { + return + } + } + } +} + // GetIssueConfig returns the issue config for a repository. func (s *RepoService) GetIssueConfig(ctx context.Context, owner, repo string) (*types.IssueConfig, error) { path := ResolvePath("/api/v1/repos/{owner}/{repo}/issue_config", pathParams("owner", owner, "repo", repo)) diff --git a/repos_test.go b/repos_test.go index 6fca0ce..1a32fdd 100644 --- a/repos_test.go +++ b/repos_test.go @@ -1009,6 +1009,49 @@ func TestRepoService_ListIssueTemplates_Good(t *testing.T) { } } +func TestRepoService_IterIssueTemplates_Good(t *testing.T) { + var requests int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/repos/core/go-forge/issue_templates" { + t.Errorf("wrong path: %s", r.URL.Path) + http.NotFound(w, r) + return + } + json.NewEncoder(w).Encode([]types.IssueTemplate{ + { + Name: "bug report", + Title: "Bug report", + Content: "Describe the problem", + }, + { + Name: "feature request", + Title: "Feature request", + Content: "Describe the idea", + }, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + var got []string + for template, err := range f.Repos.IterIssueTemplates(context.Background(), "core", "go-forge") { + if err != nil { + t.Fatal(err) + } + got = append(got, template.Name) + } + if requests != 1 { + t.Fatalf("expected 1 request, got %d", requests) + } + if len(got) != 2 || got[0] != "bug report" || got[1] != "feature request" { + t.Fatalf("got %#v", got) + } +} + func TestRepoService_GetIssueConfig_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet {