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:
commit
de4984d92a
40
issue.go
40
issue.go
@ -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 {
|
||||
|
@ -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
41
jira.go
@ -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
|
||||
}
|
||||
|
21
jira_test.go
21
jira_test.go
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user