1
0
mirror of https://github.com/interviewstreet/go-jira.git synced 2024-11-24 08:22:42 +02:00

Merge pull request #18 from nebril/paging

Wrap http.Response in Response struct to provide more information about paging
This commit is contained in:
Andy Grunwald 2016-06-19 14:33:53 +02:00 committed by GitHub
commit de4984d92a
5 changed files with 131 additions and 25 deletions

View File

@ -5,7 +5,6 @@ import (
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"strings"
"time"
@ -278,10 +277,18 @@ type CommentVisibility struct {
Value string `json:"value,omitempty"`
}
type SearchOptions struct {
StartAt int
MaxResults int
}
// searchResult is only a small wrapper arround the Search (with JQL) method
// to be able to parse the results
type searchResult struct {
Issues []Issue `json:"issues"`
Issues []Issue `json:"issues"`
StartAt int `json:"startAt"`
MaxResults int `json:"maxResults"`
Total int `json:"total"`
}
// CustomFields represents custom fields of JIRA
@ -294,7 +301,7 @@ type CustomFields map[string]string
// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue
func (s *IssueService) Get(issueID string) (*Issue, *http.Response, error) {
func (s *IssueService) Get(issueID string) (*Issue, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
if err != nil {
@ -310,11 +317,11 @@ func (s *IssueService) Get(issueID string) (*Issue, *http.Response, error) {
return issue, resp, nil
}
// DownloadAttachment returns a http.Response of an attachment for a given attachmentID.
// The attachment is in the http.Response.Body of the response.
// DownloadAttachment returns a Response of an attachment for a given attachmentID.
// The attachment is in the Response.Body of the response.
// This is an io.ReadCloser.
// The caller should close the resp.Body.
func (s *IssueService) DownloadAttachment(attachmentID string) (*http.Response, error) {
func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error) {
apiEndpoint := fmt.Sprintf("secure/attachment/%s/", attachmentID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
if err != nil {
@ -330,7 +337,7 @@ func (s *IssueService) DownloadAttachment(attachmentID string) (*http.Response,
}
// PostAttachment uploads r (io.Reader) as an attachment to a given attachmentID
func (s *IssueService) PostAttachment(attachmentID string, r io.Reader, attachmentName string) (*[]Attachment, *http.Response, error) {
func (s *IssueService) PostAttachment(attachmentID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", attachmentID)
b := new(bytes.Buffer)
@ -371,7 +378,7 @@ func (s *IssueService) PostAttachment(attachmentID string, r io.Reader, attachme
// The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues
func (s *IssueService) Create(issue *Issue) (*Issue, *http.Response, error) {
func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) {
apiEndpoint := "rest/api/2/issue/"
req, err := s.client.NewRequest("POST", apiEndpoint, issue)
if err != nil {
@ -390,7 +397,7 @@ func (s *IssueService) Create(issue *Issue) (*Issue, *http.Response, error) {
// AddComment adds a new comment to issueID.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addComment
func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *http.Response, error) {
func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment", issueID)
req, err := s.client.NewRequest("POST", apiEndpoint, comment)
if err != nil {
@ -409,7 +416,7 @@ func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *
// AddLink adds a link between two issues.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink
func (s *IssueService) AddLink(issueLink *IssueLink) (*http.Response, error) {
func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issueLink")
req, err := s.client.NewRequest("POST", apiEndpoint, issueLink)
if err != nil {
@ -423,8 +430,15 @@ func (s *IssueService) AddLink(issueLink *IssueLink) (*http.Response, error) {
// Search will search for tickets according to the jql
//
// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues
func (s *IssueService) Search(jql string) ([]Issue, *http.Response, error) {
u := fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql))
func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Response, error) {
var u string
if options == nil {
u = fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql))
} else {
u = fmt.Sprintf("rest/api/2/search?jql=%s&startAt=%d&maxResults=%d", url.QueryEscape(jql),
options.StartAt, options.MaxResults)
}
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return []Issue{}, nil, err
@ -436,7 +450,7 @@ func (s *IssueService) Search(jql string) ([]Issue, *http.Response, error) {
}
// GetCustomFields returns a map of customfield_* keys with string values
func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *http.Response, error) {
func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
if err != nil {

View File

@ -316,11 +316,13 @@ func TestIssue_Search(t *testing.T) {
defer teardown()
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, "/rest/api/2/search?jql=something")
testRequestURL(t, r, "/rest/api/2/search?jql=something&startAt=1&maxResults=40")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"expand": "schema,names","startAt": 0,"maxResults": 50,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 40,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
})
_, resp, err := testClient.Issue.Search("something")
opt := &SearchOptions{StartAt: 1, MaxResults: 40}
_, resp, err := testClient.Issue.Search("something", opt)
if resp == nil {
t.Errorf("Response given: %+v", resp)
@ -328,6 +330,47 @@ func TestIssue_Search(t *testing.T) {
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("StartAt should populate with 40, %v given", resp.MaxResults)
}
if resp.Total != 6 {
t.Errorf("StartAt should populate with 6, %v given", resp.Total)
}
}
func TestIssue_SearchWithoutPaging(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, "/rest/api/2/search?jql=something")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"expand": "schema,names","startAt": 0,"maxResults": 50,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
})
_, resp, err := testClient.Issue.Search("something", nil)
if resp == nil {
t.Errorf("Response given: %+v", resp)
}
if err != nil {
t.Errorf("Error given: %s", err)
}
if resp.StartAt != 0 {
t.Errorf("StartAt should populate with 0, %v given", resp.StartAt)
}
if resp.MaxResults != 50 {
t.Errorf("StartAt should populate with 50, %v given", resp.MaxResults)
}
if resp.Total != 6 {
t.Errorf("StartAt should populate with 6, %v given", resp.Total)
}
}
func Test_CustomFields(t *testing.T) {

41
jira.go
View File

@ -122,25 +122,26 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (
// Do sends an API request and returns the API response.
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
resp, err := c.client.Do(req)
func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
httpResp, err := c.client.Do(req)
if err != nil {
return nil, err
}
err = CheckResponse(resp)
err = CheckResponse(httpResp)
if err != nil {
// Even though there was an error, we still return the response
// in case the caller wants to inspect it further
return resp, err
return newResponse(httpResp, nil), err
}
if v != nil {
// Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(v)
defer httpResp.Body.Close()
err = json.NewDecoder(httpResp.Body).Decode(v)
}
resp := newResponse(httpResp, v)
return resp, err
}
@ -162,3 +163,31 @@ func CheckResponse(r *http.Response) error {
func (c *Client) GetBaseURL() url.URL {
return *c.baseURL
}
// Response represents JIRA API response. It wraps http.Response returned from
// API and provides information about paging.
type Response struct {
*http.Response
StartAt int
MaxResults int
Total int
}
func newResponse(r *http.Response, v interface{}) *Response {
resp := &Response{Response: r}
resp.populatePageValues(v)
return resp
}
// Sets paging values if response json was parsed to searchResult type
// (can be extended with other types if they also need paging info)
func (r *Response) populatePageValues(v interface{}) {
switch value := v.(type) {
case *searchResult:
r.StartAt = value.StartAt
r.MaxResults = value.MaxResults
r.Total = value.Total
}
return
}

View File

@ -296,3 +296,24 @@ func TestGetBaseURL_WithURL(t *testing.T) {
t.Errorf("Base URLs are not equal. Expected %+v, got %+v", *u, b)
}
}
func TestPagingInfoEmptyByDefault(t *testing.T) {
c, _ := NewClient(nil, testJIRAInstanceURL)
req, _ := c.NewRequest("GET", "/", nil)
type foo struct {
A string
}
body := new(foo)
resp, _ := c.Do(req, body)
if resp.StartAt != 0 {
t.Errorf("StartAt not equal to 0")
}
if resp.MaxResults != 0 {
t.Errorf("StartAt not equal to 0")
}
if resp.Total != 0 {
t.Errorf("StartAt not equal to 0")
}
}

View File

@ -2,7 +2,6 @@ package jira
import (
"fmt"
"net/http"
)
// ProjectService handles projects for the JIRA instance / API.
@ -73,7 +72,7 @@ type ProjectComponent struct {
// GetList gets all projects form JIRA
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
func (s *ProjectService) GetList() (*ProjectList, *http.Response, error) {
func (s *ProjectService) GetList() (*ProjectList, *Response, error) {
apiEndpoint := "rest/api/2/project"
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
if err != nil {
@ -93,7 +92,7 @@ func (s *ProjectService) GetList() (*ProjectList, *http.Response, error) {
// This can be an project id, or an project key.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
func (s *ProjectService) Get(projectID string) (*Project, *http.Response, error) {
func (s *ProjectService) Get(projectID string) (*Project, *Response, error) {
apiEndpoint := fmt.Sprintf("/rest/api/2/project/%s", projectID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
if err != nil {