package jira import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "reflect" "github.com/google/go-querystring/query" ) // 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 Project *ProjectService Board *BoardService Sprint *SprintService User *UserService Group *GroupService } // NewClient returns a new JIRA API client. // If a nil httpClient is provided, http.DefaultClient will be used. // To use API methods which require authentication you can follow the preferred solution and // 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} c.Project = &ProjectService{client: c} c.Board = &BoardService{client: c} c.Sprint = &SprintService{client: c} c.User = &UserService{client: c} c.Group = &GroupService{client: c} return c, nil } // 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 authentication information if c.Authentication.authType == authTypeSession { // Set session cookie if there is one if c.session != nil { for _, cookie := range c.session.Cookies { req.AddCookie(cookie) } } } else if c.Authentication.authType == authTypeBasic { // Set basic auth information if c.Authentication.username != "" { req.SetBasicAuth(c.Authentication.username, c.Authentication.password) } } return req, nil } // 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) err = json.NewEncoder(buf).Encode(body) 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 authentication information if c.Authentication.authType == authTypeSession { // Set session cookie if there is one if c.session != nil { for _, cookie := range c.session.Cookies { req.AddCookie(cookie) } } } else if c.Authentication.authType == authTypeBasic { // Set basic auth information if c.Authentication.username != "" { req.SetBasicAuth(c.Authentication.username, c.Authentication.password) } } return req, nil } // 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 } // NewMultiPartRequest creates an API request including a multi-part file. // 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 buf is a multipart form. 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 authentication information if c.Authentication.authType == authTypeSession { // Set session cookie if there is one if c.session != nil { for _, cookie := range c.session.Cookies { req.AddCookie(cookie) } } } else if c.Authentication.authType == authTypeBasic { // Set basic auth information if c.Authentication.username != "" { req.SetBasicAuth(c.Authentication.username, c.Authentication.password) } } return req, nil } // 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{}) (*Response, error) { httpResp, err := c.client.Do(req) if err != nil { return nil, err } 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 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 httpResp.Body.Close() err = json.NewDecoder(httpResp.Body).Decode(v) } resp := newResponse(httpResp, v) 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. // 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). func CheckResponse(r *http.Response) error { if c := r.StatusCode; 200 <= c && c <= 299 { return nil } err := fmt.Errorf("Request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode) return err } // 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 } // 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 }