Skip to content

Commit

Permalink
feat: Add Search (POST) support.
Browse files Browse the repository at this point in the history
This adds support for the POST search endpoint for long queries on Jira.
The client interface does not change; the request to the server will be
upgraded to a POST request behind the scenes if the URL would be more
than 2000 characters long.
  • Loading branch information
mari-crl committed Jul 14, 2022
1 parent dcc7f65 commit 2549937
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 27 deletions.
59 changes: 32 additions & 27 deletions issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -517,14 +516,23 @@ type CommentVisibility struct {
// Default Pagination options
type SearchOptions struct {
// StartAt: The starting index of the returned projects. Base index: 0.
StartAt int `url:"startAt,omitempty"`
StartAt int `url:"startAt,omitempty" json:"startAt,omitempty"`
// MaxResults: The maximum number of projects to return per page. Default: 50.
MaxResults int `url:"maxResults,omitempty"`
MaxResults int `url:"maxResults,omitempty" json:"maxResults,omitempty"`
// Expand: Expand specific sections in the returned issues
Expand string `url:"expand,omitempty"`
Fields []string
Expand string `url:"expand,omitempty" json:"-"`
Fields []string `url:"fields,comma,omitempty" json:"fields,omitempty"`
// ValidateQuery: The validateQuery param offers control over whether to validate and how strictly to treat the validation. Default: strict.
ValidateQuery string `url:"validateQuery,omitempty"`
ValidateQuery string `url:"validateQuery,omitempty" json:"validateQuery,omitempty"`
}

// searchRequest is for the Search (with JQL) method to encode its inputs into a request whether it's POST or GET.
type searchRequest struct {
// JQL is the query that the user passed.
JQL string `url:"jql,omitempty" json:"jql,omitempty"`
// ExpandArray is the array form of SearchOptions.Expand used for the POST/JSON version of the search request.
ExpandArray []string `url:"-" json:"expand,omitempty"`
*SearchOptions `url:",omitempty" json:",omitempty"`
}

// searchResult is only a small wrapper around the Search (with JQL) method
Expand Down Expand Up @@ -1091,32 +1099,29 @@ func (s *IssueService) SearchWithContext(ctx context.Context, jql string, option
u := url.URL{
Path: "rest/api/2/search",
}
uv := url.Values{}
if jql != "" {
uv.Add("jql", jql)
r := searchRequest{
JQL: jql,
SearchOptions: options,
}

if options != nil {
if options.StartAt != 0 {
uv.Add("startAt", strconv.Itoa(options.StartAt))
}
if options.MaxResults != 0 {
uv.Add("maxResults", strconv.Itoa(options.MaxResults))
}
if options.Expand != "" {
uv.Add("expand", options.Expand)
}
if strings.Join(options.Fields, ",") != "" {
uv.Add("fields", strings.Join(options.Fields, ","))
}
if options.ValidateQuery != "" {
uv.Add("validateQuery", options.ValidateQuery)
}
if options != nil && options.Expand != "" {
r.ExpandArray = strings.Split(options.Expand, ",")
}

uv, err := query.Values(r)
if err != nil {
return []Issue{}, nil, err
}
u.RawQuery = uv.Encode()

req, err := s.client.NewRequestWithContext(ctx, "GET", u.String(), nil)
var req *http.Request
if len(u.String()) > 2000 {
// If the JQL is too long, switch to the post method instead.
u.RawQuery = ""
req, err = s.client.NewRequestWithContext(ctx, "POST", u.String(), r)
} else {
req, err = s.client.NewRequestWithContext(ctx, "GET", u.String(), nil)
}

if err != nil {
return []Issue{}, nil, err
}
Expand Down
39 changes: 39 additions & 0 deletions issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,45 @@ func TestIssueService_Search(t *testing.T) {
}
}

func TestIssueService_Search_Long_Query(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/api/2/search")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 40,"total": 0,"issues": []}`)
})

opt := &SearchOptions{StartAt: 1, MaxResults: 40, Expand: "foo"}

// LONGKEY- is 8 characters; 250 x 8 is 2000, which is the limit for URL length.
keys := make([]string, 250)
for n := range keys {
keys[n] = fmt.Sprintf("LONGKEY-%d", n+1)
}

query := fmt.Sprintf("type = Bug and Key IN (%s)", strings.Join(keys, ","))
_, resp, err := testClient.Issue.Search(query, opt)

if resp == nil {
t.Errorf("Response given: %+v", resp)
}
if err != nil {
t.Errorf("Error given: %s", err)
}

if resp.StartAt != 1 {
t.Errorf("StartAt should populate with 1, %v given", resp.StartAt)
}
if resp.MaxResults != 40 {
t.Errorf("MaxResults should populate with 40, %v given", resp.MaxResults)
}
if resp.Total != 0 {
t.Errorf("Total should populate with 0, %v given", resp.Total)
}
}

func TestIssueService_SearchEmptyJQL(t *testing.T) {
setup()
defer teardown()
Expand Down

0 comments on commit 2549937

Please sign in to comment.