Repository: xanzy/go-gitlab
Pull request #1278 (2021-10-19 17:42:23)

Add ApprovedByIDs field to ListMergeRequestsOptions
Certain subscription levels have the option to retrieve a list of merge requests that have been approved by certain users. This MR adds this field which, sadly, is one of the integer array values that can also be a string.

I hope this approach doesn't go completely against the conventions here but I couldn't find any relevant examples in other fields.

Relevant GitLab API docs: https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests


Patch:
diff --git a/merge_requests.go b/merge_requests.go
index bdd0fd8..42132b9 100644
--- a/merge_requests.go
+++ b/merge_requests.go
@@ -140,32 +140,33 @@ func (m MergeRequestDiffVersion) String() string {
 // https://docs.gitlab.com/ce/api/merge_requests.html#list-merge-requests
 type ListMergeRequestsOptions struct {
 	ListOptions
-	State                  *string    `url:"state,omitempty" json:"state,omitempty"`
-	OrderBy                *string    `url:"order_by,omitempty" json:"order_by,omitempty"`
-	Sort                   *string    `url:"sort,omitempty" json:"sort,omitempty"`
-	Milestone              *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
-	View                   *string    `url:"view,omitempty" json:"view,omitempty"`
-	Labels                 Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
-	NotLabels              Labels     `url:"not[labels],comma,omitempty" json:"not[labels],omitempty"`
-	WithLabelsDetails      *bool      `url:"with_labels_details,omitempty" json:"with_labels_details,omitempty"`
-	WithMergeStatusRecheck *bool      `url:"with_merge_status_recheck,omitempty" json:"with_merge_status_recheck,omitempty"`
-	CreatedAfter           *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
-	CreatedBefore          *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
-	UpdatedAfter           *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
-	UpdatedBefore          *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"`
-	Scope                  *string    `url:"scope,omitempty" json:"scope,omitempty"`
-	AuthorID               *int       `url:"author_id,omitempty" json:"author_id,omitempty"`
-	AuthorUsername         *string    `url:"author_username,omitempty" json:"author_username,omitempty"`
-	AssigneeID             *int       `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
-	ReviewerID             *int       `url:"reviewer_id,omitempty" json:"reviewer_id,omitempty"`
-	ReviewerUsername       *string    `url:"reviewer_username,omitempty" json:"reviewer_username,omitempty"`
-	MyReactionEmoji        *string    `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
-	SourceBranch           *string    `url:"source_branch,omitempty" json:"source_branch,omitempty"`
-	TargetBranch           *string    `url:"target_branch,omitempty" json:"target_branch,omitempty"`
-	Search                 *string    `url:"search,omitempty" json:"search,omitempty"`
-	In                     *string    `url:"in,omitempty" json:"in,omitempty"`
-	Draft                  *bool      `url:"draft,omitempty" json:"draft,omitempty"`
-	WIP                    *string    `url:"wip,omitempty" json:"wip,omitempty"`
+	State                  *string           `url:"state,omitempty" json:"state,omitempty"`
+	OrderBy                *string           `url:"order_by,omitempty" json:"order_by,omitempty"`
+	Sort                   *string           `url:"sort,omitempty" json:"sort,omitempty"`
+	Milestone              *string           `url:"milestone,omitempty" json:"milestone,omitempty"`
+	View                   *string           `url:"view,omitempty" json:"view,omitempty"`
+	Labels                 Labels            `url:"labels,comma,omitempty" json:"labels,omitempty"`
+	NotLabels              Labels            `url:"not[labels],comma,omitempty" json:"not[labels],omitempty"`
+	WithLabelsDetails      *bool             `url:"with_labels_details,omitempty" json:"with_labels_details,omitempty"`
+	WithMergeStatusRecheck *bool             `url:"with_merge_status_recheck,omitempty" json:"with_merge_status_recheck,omitempty"`
+	CreatedAfter           *time.Time        `url:"created_after,omitempty" json:"created_after,omitempty"`
+	CreatedBefore          *time.Time        `url:"created_before,omitempty" json:"created_before,omitempty"`
+	UpdatedAfter           *time.Time        `url:"updated_after,omitempty" json:"updated_after,omitempty"`
+	UpdatedBefore          *time.Time        `url:"updated_before,omitempty" json:"updated_before,omitempty"`
+	Scope                  *string           `url:"scope,omitempty" json:"scope,omitempty"`
+	AuthorID               *int              `url:"author_id,omitempty" json:"author_id,omitempty"`
+	AuthorUsername         *string           `url:"author_username,omitempty" json:"author_username,omitempty"`
+	AssigneeID             *int              `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
+	ApprovedByIDs          *ApproverIDsValue `url:"approved_by_ids,omitempty" json:"approved_by_ids,omitempty"`
+	ReviewerID             *int              `url:"reviewer_id,omitempty" json:"reviewer_id,omitempty"`
+	ReviewerUsername       *string           `url:"reviewer_username,omitempty" json:"reviewer_username,omitempty"`
+	MyReactionEmoji        *string           `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
+	SourceBranch           *string           `url:"source_branch,omitempty" json:"source_branch,omitempty"`
+	TargetBranch           *string           `url:"target_branch,omitempty" json:"target_branch,omitempty"`
+	Search                 *string           `url:"search,omitempty" json:"search,omitempty"`
+	In                     *string           `url:"in,omitempty" json:"in,omitempty"`
+	Draft                  *bool             `url:"draft,omitempty" json:"draft,omitempty"`
+	WIP                    *string           `url:"wip,omitempty" json:"wip,omitempty"`
 }
 
 // ListMergeRequests gets all merge requests. The state parameter can be used
diff --git a/types.go b/types.go
index 679cb9e..2d90c41 100644
--- a/types.go
+++ b/types.go
@@ -21,6 +21,7 @@ import (
 	"errors"
 	"fmt"
 	"net/url"
+	"strconv"
 	"time"
 )
 
@@ -78,6 +79,52 @@ func AccessLevel(v AccessLevelValue) *AccessLevelValue {
 	return p
 }
 
+// ApproverIDValue represents an approver ID value within GitLab.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests
+type ApproverIDValue string
+
+// List of available approver ID values.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests
+const (
+	ApproverIDAny  ApproverIDValue = "Any"
+	ApproverIDNone ApproverIDValue = "None"
+)
+
+// ApproverIDsValue represents an approvers ID value within GitLab.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests
+type ApproverIDsValue struct {
+	value interface{}
+}
+
+func ApproverIDs(v interface{}) *ApproverIDsValue {
+	switch v.(type) {
+	case ApproverIDValue, []int:
+		return &ApproverIDsValue{value: v}
+	default:
+		panic("Unsupported value passed as approver ID")
+	}
+}
+
+func (a *ApproverIDsValue) EncodeValues(key string, v *url.Values) error {
+	switch value := a.value.(type) {
+	case ApproverIDValue:
+		v.Set(key, string(value))
+	case []int:
+		v.Del(key)
+		v.Del(key + "[]")
+		for _, id := range value {
+			v.Add(key+"[]", strconv.Itoa(id))
+		}
+	}
+	return nil
+}
+
 // AvailabilityValue represents an availability value within GitLab.
 type AvailabilityValue string
 


Test patch:
diff --git a/merge_requests_test.go b/merge_requests_test.go
index e0fcb46..e2caa0e 100644
--- a/merge_requests_test.go
+++ b/merge_requests_test.go
@@ -24,6 +24,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/google/go-querystring/query"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -245,3 +246,28 @@ func TestGetIssuesClosedOnMerge_Jira(t *testing.T) {
 	assert.Equal(t, "PROJECT-123", issues[0].ExternalID)
 	assert.Equal(t, "Title of this issue", issues[0].Title)
 }
+
+func TestIntSliceOrString(t *testing.T) {
+	t.Run("any", func(t *testing.T) {
+		opts := &ListMergeRequestsOptions{}
+		opts.ApprovedByIDs = ApproverIDs(ApproverIDAny)
+		q, err := query.Values(opts)
+		assert.NoError(t, err)
+		assert.Equal(t, "Any", q.Get("approved_by_ids"))
+	})
+	t.Run("none", func(t *testing.T) {
+		opts := &ListMergeRequestsOptions{}
+		opts.ApprovedByIDs = ApproverIDs(ApproverIDNone)
+		q, err := query.Values(opts)
+		assert.NoError(t, err)
+		assert.Equal(t, "None", q.Get("approved_by_ids"))
+	})
+	t.Run("ids", func(t *testing.T) {
+		opts := &ListMergeRequestsOptions{}
+		opts.ApprovedByIDs = ApproverIDs([]int{1, 2, 3})
+		q, err := query.Values(opts)
+		assert.NoError(t, err)
+		includedIDs := q["approved_by_ids[]"]
+		assert.Equal(t, []string{"1", "2", "3"}, includedIDs)
+	})
+}