2015-09-03 12:25:21 +02:00
package jira
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
2016-06-15 11:20:37 +02:00
"reflect"
"github.com/google/go-querystring/query"
2015-09-03 12:25:21 +02:00
)
// A Client manages communication with the JIRA API.
type Client struct {
// HTTP client used to communicate with the API.
client * http . Client
// Base URL for API requests.
baseURL * url . URL
// Session storage if the user authentificate with a Session cookie
session * Session
// Services used for talking to different parts of the JIRA API.
Authentication * AuthenticationService
Issue * IssueService
2016-05-29 18:42:38 +02:00
Project * ProjectService
2016-06-15 19:15:12 +02:00
Board * BoardService
2016-06-24 10:43:32 +02:00
Sprint * SprintService
2016-10-31 14:24:30 +02:00
User * UserService
2015-09-03 12:25:21 +02:00
}
// NewClient returns a new JIRA API client.
// If a nil httpClient is provided, http.DefaultClient will be used.
2016-03-26 22:24:23 +02:00
// To use API methods which require authentication you can follow the preferred solution and
2015-09-03 12:25:21 +02:00
// provide an http.Client that will perform the authentication for you with OAuth and HTTP Basic (such as that provided by the golang.org/x/oauth2 library).
// As an alternative you can use Session Cookie based authentication provided by this package as well.
// See https://docs.atlassian.com/jira/REST/latest/#authentication
// baseURL is the HTTP endpoint of your JIRA instance and should always be specified with a trailing slash.
func NewClient ( httpClient * http . Client , baseURL string ) ( * Client , error ) {
if httpClient == nil {
httpClient = http . DefaultClient
}
parsedBaseURL , err := url . Parse ( baseURL )
if err != nil {
return nil , err
}
c := & Client {
client : httpClient ,
baseURL : parsedBaseURL ,
}
c . Authentication = & AuthenticationService { client : c }
c . Issue = & IssueService { client : c }
2016-05-29 18:42:38 +02:00
c . Project = & ProjectService { client : c }
2016-06-15 19:08:15 +02:00
c . Board = & BoardService { client : c }
2016-06-24 10:43:32 +02:00
c . Sprint = & SprintService { client : c }
2016-10-31 14:24:30 +02:00
c . User = & UserService { client : c }
2015-09-03 12:25:21 +02:00
return c , nil
}
2016-10-10 09:46:21 +02:00
// NewRawRequest creates an API request.
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// Relative URLs should always be specified without a preceding slash.
// Allows using an optional native io.Reader for sourcing the request body.
func ( c * Client ) NewRawRequest ( method , urlStr string , body io . Reader ) ( * http . Request , error ) {
rel , err := url . Parse ( urlStr )
if err != nil {
return nil , err
}
u := c . baseURL . ResolveReference ( rel )
req , err := http . NewRequest ( method , u . String ( ) , body )
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
// Set session cookie if there is one
if c . session != nil {
for _ , cookie := range c . session . Cookies {
req . AddCookie ( cookie )
}
}
return req , nil
}
2015-09-03 12:25:21 +02:00
// NewRequest creates an API request.
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// Relative URLs should always be specified without a preceding slash.
// If specified, the value pointed to by body is JSON encoded and included as the request body.
func ( c * Client ) NewRequest ( method , urlStr string , body interface { } ) ( * http . Request , error ) {
rel , err := url . Parse ( urlStr )
if err != nil {
return nil , err
}
u := c . baseURL . ResolveReference ( rel )
var buf io . ReadWriter
if body != nil {
buf = new ( bytes . Buffer )
2016-10-23 14:51:29 +02:00
err = json . NewEncoder ( buf ) . Encode ( body )
2015-09-03 12:25:21 +02:00
if err != nil {
return nil , err
}
}
req , err := http . NewRequest ( method , u . String ( ) , buf )
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
// Set session cookie if there is one
if c . session != nil {
2016-06-03 22:43:41 +02:00
for _ , cookie := range c . session . Cookies {
2016-05-27 12:01:54 +02:00
req . AddCookie ( cookie )
}
2015-09-03 12:25:21 +02:00
}
return req , nil
}
2016-06-15 11:20:37 +02:00
// addOptions adds the parameters in opt as URL query parameters to s. opt
// must be a struct whose fields may contain "url" tags.
func addOptions ( s string , opt interface { } ) ( string , error ) {
v := reflect . ValueOf ( opt )
if v . Kind ( ) == reflect . Ptr && v . IsNil ( ) {
return s , nil
}
u , err := url . Parse ( s )
if err != nil {
return s , err
}
qs , err := query . Values ( opt )
if err != nil {
return s , err
}
u . RawQuery = qs . Encode ( )
return u . String ( ) , nil
}
2016-05-27 14:14:09 +02:00
// NewMultiPartRequest creates an API request including a multi-part file.
2016-05-19 23:11:21 +02:00
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// Relative URLs should always be specified without a preceding slash.
2016-05-27 14:14:09 +02:00
// If specified, the value pointed to by buf is a multipart form.
2016-05-19 23:11:21 +02:00
func ( c * Client ) NewMultiPartRequest ( method , urlStr string , buf * bytes . Buffer ) ( * http . Request , error ) {
rel , err := url . Parse ( urlStr )
if err != nil {
return nil , err
}
u := c . baseURL . ResolveReference ( rel )
req , err := http . NewRequest ( method , u . String ( ) , buf )
if err != nil {
return nil , err
}
// Set required headers
req . Header . Set ( "X-Atlassian-Token" , "nocheck" )
// Set session cookie if there is one
2016-07-27 12:21:09 +02:00
if c . session != nil {
for _ , cookie := range c . session . Cookies {
req . AddCookie ( cookie )
}
2016-05-19 23:11:21 +02:00
}
return req , nil
}
2015-09-03 12:25:21 +02:00
// 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.
2016-06-15 17:09:13 +02:00
func ( c * Client ) Do ( req * http . Request , v interface { } ) ( * Response , error ) {
httpResp , err := c . client . Do ( req )
2015-09-03 12:25:21 +02:00
if err != nil {
return nil , err
}
2016-06-15 17:09:13 +02:00
err = CheckResponse ( httpResp )
2015-09-03 12:25:21 +02:00
if err != nil {
// Even though there was an error, we still return the response
// in case the caller wants to inspect it further
2016-06-15 17:09:13 +02:00
return newResponse ( httpResp , nil ) , err
2015-09-03 12:25:21 +02:00
}
if v != nil {
2016-05-24 07:48:04 +02:00
// Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to
2016-06-15 17:09:13 +02:00
defer httpResp . Body . Close ( )
err = json . NewDecoder ( httpResp . Body ) . Decode ( v )
2015-09-03 12:25:21 +02:00
}
2016-06-15 17:09:13 +02:00
resp := newResponse ( httpResp , v )
2015-09-03 12:25:21 +02:00
return resp , err
}
// CheckResponse checks the API response for errors, and returns them if present.
// A response is considered an error if it has a status code outside the 200 range.
2016-05-28 20:15:17 +02:00
// The caller is responsible to analyze the response body.
// The body can contain JSON (if the error is intended) or xml (sometimes JIRA just failes).
2015-09-03 12:25:21 +02:00
func CheckResponse ( r * http . Response ) error {
if c := r . StatusCode ; 200 <= c && c <= 299 {
return nil
}
2016-05-28 20:15:17 +02:00
err := fmt . Errorf ( "Request failed. Please analyze the request body for more details. Status code: %d" , r . StatusCode )
return err
2015-09-03 12:25:21 +02:00
}
2016-06-03 18:18:01 +02:00
// GetBaseURL will return you the Base URL.
// This is the same URL as in the NewClient constructor
func ( c * Client ) GetBaseURL ( ) url . URL {
return * c . baseURL
}
2016-06-15 17:09:13 +02:00
// 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
}