From b9962f2412d131a3ac268eddad21e1e99c8e504f Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 00:30:36 +0000 Subject: [PATCH] feat: add milestone list filters Co-Authored-By: Virgil --- docs/api-contract.md | 2 +- milestones.go | 52 +++++++++++++++++++++++++++++++++++++++----- milestones_test.go | 40 ++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/docs/api-contract.md b/docs/api-contract.md index 231d64e..b02b74e 100644 --- a/docs/api-contract.md +++ b/docs/api-contract.md @@ -138,7 +138,7 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r | method | LabelService.ListRepoLabels | `func (s *LabelService) ListRepoLabels(ctx context.Context, owner, repo string) ([]types.Label, error)` | ListRepoLabels returns all labels for a repository. | `TestLabelService_Good_ListRepoLabels` | | method | MilestoneService.Create | `func (s *MilestoneService) Create(ctx context.Context, owner, repo string, opts *types.CreateMilestoneOption) (*types.Milestone, error)` | Create creates a new milestone. | No direct tests. | | method | MilestoneService.Get | `func (s *MilestoneService) Get(ctx context.Context, owner, repo string, id int64) (*types.Milestone, error)` | Get returns a single milestone by ID. | No direct tests. | -| method | MilestoneService.ListAll | `func (s *MilestoneService) ListAll(ctx context.Context, params Params) ([]types.Milestone, error)` | ListAll returns all milestones for a repository. | No direct tests. | +| method | MilestoneService.ListAll | `func (s *MilestoneService) ListAll(ctx context.Context, params Params, filters ...MilestoneListOptions) ([]types.Milestone, error)` | ListAll returns all milestones for a repository. | No direct tests. | | method | MiscService.GetGitignoreTemplate | `func (s *MiscService) GetGitignoreTemplate(ctx context.Context, name string) (*types.GitignoreTemplateInfo, error)` | GetGitignoreTemplate returns a single gitignore template by name. | `TestMiscService_Good_GetGitignoreTemplate` | | method | MiscService.GetAPISettings | `func (s *MiscService) GetAPISettings(ctx context.Context) (*types.GeneralAPISettings, error)` | GetAPISettings returns the instance's global API settings. | `TestMiscService_GetAPISettings_Good` | | method | MiscService.GetAttachmentSettings | `func (s *MiscService) GetAttachmentSettings(ctx context.Context) (*types.GeneralAttachmentSettings, error)` | GetAttachmentSettings returns the instance's global attachment settings. | `TestMiscService_GetAttachmentSettings_Good` | diff --git a/milestones.go b/milestones.go index f5df222..27658a8 100644 --- a/milestones.go +++ b/milestones.go @@ -7,6 +7,26 @@ import ( "dappco.re/go/core/forge/types" ) +// MilestoneListOptions controls filtering for repository milestone listings. +type MilestoneListOptions struct { + State string + Name string +} + +func (o MilestoneListOptions) queryParams() map[string]string { + query := make(map[string]string, 2) + if o.State != "" { + query["state"] = o.State + } + if o.Name != "" { + query["name"] = o.Name + } + if len(query) == 0 { + return nil + } + return query +} + // MilestoneService handles repository milestones. // // Usage: @@ -22,21 +42,21 @@ func newMilestoneService(c *Client) *MilestoneService { } // List returns a single page of milestones for a repository. -func (s *MilestoneService) List(ctx context.Context, params Params, opts ListOptions) (*PagedResult[types.Milestone], error) { +func (s *MilestoneService) List(ctx context.Context, params Params, opts ListOptions, filters ...MilestoneListOptions) (*PagedResult[types.Milestone], error) { path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones", params) - return ListPage[types.Milestone](ctx, s.client, path, nil, opts) + return ListPage[types.Milestone](ctx, s.client, path, milestoneQuery(filters...), opts) } // Iter returns an iterator over all milestones for a repository. -func (s *MilestoneService) Iter(ctx context.Context, params Params) iter.Seq2[types.Milestone, error] { +func (s *MilestoneService) Iter(ctx context.Context, params Params, filters ...MilestoneListOptions) iter.Seq2[types.Milestone, error] { path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones", params) - return ListIter[types.Milestone](ctx, s.client, path, nil) + return ListIter[types.Milestone](ctx, s.client, path, milestoneQuery(filters...)) } // ListAll returns all milestones for a repository. -func (s *MilestoneService) ListAll(ctx context.Context, params Params) ([]types.Milestone, error) { +func (s *MilestoneService) ListAll(ctx context.Context, params Params, filters ...MilestoneListOptions) ([]types.Milestone, error) { path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones", params) - return ListAll[types.Milestone](ctx, s.client, path, nil) + return ListAll[types.Milestone](ctx, s.client, path, milestoneQuery(filters...)) } // Get returns a single milestone by ID. @@ -74,3 +94,23 @@ func (s *MilestoneService) Delete(ctx context.Context, owner, repo string, id in path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id))) return s.client.Delete(ctx, path) } + +func milestoneQuery(filters ...MilestoneListOptions) map[string]string { + if len(filters) == 0 { + return nil + } + + query := make(map[string]string, 2) + for _, filter := range filters { + if filter.State != "" { + query["state"] = filter.State + } + if filter.Name != "" { + query["name"] = filter.Name + } + } + if len(query) == 0 { + return nil + } + return query +} diff --git a/milestones_test.go b/milestones_test.go index f225d57..8def945 100644 --- a/milestones_test.go +++ b/milestones_test.go @@ -49,6 +49,46 @@ func TestMilestoneService_List_Good(t *testing.T) { } } +func TestMilestoneService_ListWithFilters_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/repos/core/go-forge/milestones" { + t.Errorf("wrong path: %s", r.URL.Path) + } + if got := r.URL.Query().Get("state"); got != "all" { + t.Errorf("got state=%q, want %q", got, "all") + } + if got := r.URL.Query().Get("name"); got != "v1.0" { + t.Errorf("got name=%q, want %q", got, "v1.0") + } + if got := r.URL.Query().Get("page"); got != "1" { + t.Errorf("got page=%q, want %q", got, "1") + } + if got := r.URL.Query().Get("limit"); got != "1" { + t.Errorf("got limit=%q, want %q", got, "1") + } + w.Header().Set("X-Total-Count", "1") + json.NewEncoder(w).Encode([]types.Milestone{{ID: 1, Title: "v1.0"}}) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + page, err := f.Milestones.List( + context.Background(), + Params{"owner": "core", "repo": "go-forge"}, + ListOptions{Page: 1, Limit: 1}, + MilestoneListOptions{State: "all", Name: "v1.0"}, + ) + if err != nil { + t.Fatal(err) + } + if len(page.Items) != 1 || page.Items[0].Title != "v1.0" { + t.Fatalf("unexpected items: %+v", page.Items) + } +} + func TestMilestoneService_Iter_Good(t *testing.T) { requests := 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {