Skip to content

Commit 2549937

Browse files
committed
feat: 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 2549937

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)