Skip to content

Commit 41f1c1e

Browse files
committed
Add Search (POST) support.
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.
1 parent dcc7f65 commit 41f1c1e

File tree

2 files changed

+71
-27
lines changed

2 files changed

+71
-27
lines changed

issue.go

+32-27
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"net/http"
1212
"net/url"
1313
"reflect"
14-
"strconv"
1514
"strings"
1615
"time"
1716

@@ -517,14 +516,23 @@ type CommentVisibility struct {
517516
// Default Pagination options
518517
type SearchOptions struct {
519518
// StartAt: The starting index of the returned projects. Base index: 0.
520-
StartAt int `url:"startAt,omitempty"`
519+
StartAt int `url:"startAt,omitempty" json:"startAt,omitempty"`
521520
// MaxResults: The maximum number of projects to return per page. Default: 50.
522-
MaxResults int `url:"maxResults,omitempty"`
521+
MaxResults int `url:"maxResults,omitempty" json:"maxResults,omitempty"`
523522
// Expand: Expand specific sections in the returned issues
524-
Expand string `url:"expand,omitempty"`
525-
Fields []string
523+
Expand string `url:"expand,omitempty" json:"-"`
524+
Fields []string `url:"fields,comma,omitempty" json:"fields,omitempty"`
526525
// ValidateQuery: The validateQuery param offers control over whether to validate and how strictly to treat the validation. Default: strict.
527-
ValidateQuery string `url:"validateQuery,omitempty"`
526+
ValidateQuery string `url:"validateQuery,omitempty" json:"validateQuery,omitempty"`
527+
}
528+
529+
// searchRequest is for the Search (with JQL) method to encode its inputs into a request whether it's POST or GET.
530+
type searchRequest struct {
531+
// JQL is the query that the user passed.
532+
JQL string `url:"jql,omitempty" json:"jql,omitempty"`
533+
// ExpandArray is the array form of SearchOptions.Expand used for the POST/JSON version of the search request.
534+
ExpandArray []string `url:"-" json:"expand,omitempty"`
535+
*SearchOptions `url:",omitempty" json:",omitempty"`
528536
}
529537

530538
// searchResult is only a small wrapper around the Search (with JQL) method
@@ -1091,32 +1099,29 @@ func (s *IssueService) SearchWithContext(ctx context.Context, jql string, option
10911099
u := url.URL{
10921100
Path: "rest/api/2/search",
10931101
}
1094-
uv := url.Values{}
1095-
if jql != "" {
1096-
uv.Add("jql", jql)
1102+
r := searchRequest{
1103+
JQL: jql,
1104+
SearchOptions: options,
10971105
}
1098-
1099-
if options != nil {
1100-
if options.StartAt != 0 {
1101-
uv.Add("startAt", strconv.Itoa(options.StartAt))
1102-
}
1103-
if options.MaxResults != 0 {
1104-
uv.Add("maxResults", strconv.Itoa(options.MaxResults))
1105-
}
1106-
if options.Expand != "" {
1107-
uv.Add("expand", options.Expand)
1108-
}
1109-
if strings.Join(options.Fields, ",") != "" {
1110-
uv.Add("fields", strings.Join(options.Fields, ","))
1111-
}
1112-
if options.ValidateQuery != "" {
1113-
uv.Add("validateQuery", options.ValidateQuery)
1114-
}
1106+
if options != nil && options.Expand != "" {
1107+
r.ExpandArray = strings.Split(options.Expand, ",")
11151108
}
11161109

1110+
uv, err := query.Values(r)
1111+
if err != nil {
1112+
return []Issue{}, nil, err
1113+
}
11171114
u.RawQuery = uv.Encode()
11181115

1119-
req, err := s.client.NewRequestWithContext(ctx, "GET", u.String(), nil)
1116+
var req *http.Request
1117+
if len(u.String()) > 2000 {
1118+
// If the JQL is too long, switch to the post method instead.
1119+
u.RawQuery = ""
1120+
req, err = s.client.NewRequestWithContext(ctx, "POST", u.String(), r)
1121+
} else {
1122+
req, err = s.client.NewRequestWithContext(ctx, "GET", u.String(), nil)
1123+
}
1124+
11201125
if err != nil {
11211126
return []Issue{}, nil, err
11221127
}

issue_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,45 @@ func TestIssueService_Search(t *testing.T) {
647647
}
648648
}
649649

650+
func TestIssueService_Search_Long_Query(t *testing.T) {
651+
setup()
652+
defer teardown()
653+
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
654+
testMethod(t, r, "POST")
655+
testRequestURL(t, r, "/rest/api/2/search")
656+
w.WriteHeader(http.StatusOK)
657+
fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 40,"total": 0,"issues": []}`)
658+
})
659+
660+
opt := &SearchOptions{StartAt: 1, MaxResults: 40, Expand: "foo"}
661+
662+
// LONGKEY- is 8 characters; 250 x 8 is 2000, which is the limit for URL length.
663+
keys := make([]string, 250)
664+
for n := range keys {
665+
keys[n] = fmt.Sprintf("LONGKEY-%d", n+1)
666+
}
667+
668+
query := fmt.Sprintf("type = Bug and Key IN (%s)", strings.Join(keys, ","))
669+
_, resp, err := testClient.Issue.Search(query, opt)
670+
671+
if resp == nil {
672+
t.Errorf("Response given: %+v", resp)
673+
}
674+
if err != nil {
675+
t.Errorf("Error given: %s", err)
676+
}
677+
678+
if resp.StartAt != 1 {
679+
t.Errorf("StartAt should populate with 1, %v given", resp.StartAt)
680+
}
681+
if resp.MaxResults != 40 {
682+
t.Errorf("MaxResults should populate with 40, %v given", resp.MaxResults)
683+
}
684+
if resp.Total != 0 {
685+
t.Errorf("Total should populate with 0, %v given", resp.Total)
686+
}
687+
}
688+
650689
func TestIssueService_SearchEmptyJQL(t *testing.T) {
651690
setup()
652691
defer teardown()

0 commit comments

Comments
 (0)