mirror of
https://github.com/interviewstreet/go-jira.git
synced 2025-03-19 20:57:47 +02:00
Added the first go code
This commit is contained in:
parent
76e73f3252
commit
e848968f58
66
authentication.go
Normal file
66
authentication.go
Normal file
@ -0,0 +1,66 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// AuthenticationService handles authentication for the JIRA instance / API.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication
|
||||
type AuthenticationService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Session represents a Session JSON response by the JIRA API.
|
||||
type Session struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Session struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
} `json:"session,omitempty"`
|
||||
LoginInfo struct {
|
||||
FailedLoginCount int `json:"failedLoginCount"`
|
||||
LoginCount int `json:"loginCount"`
|
||||
LastFailedLoginTime string `json:"lastFailedLoginTime"`
|
||||
PreviousLoginTime string `json:"previousLoginTime"`
|
||||
} `json:"loginInfo"`
|
||||
}
|
||||
|
||||
// AcquireSessionCookie creates a new session for a user in JIRA.
|
||||
// Once a session has been successfully created it can be used to access any of JIRA's remote APIs and also the web UI by passing the appropriate HTTP Cookie header.
|
||||
// The header will by automatically applied to every API request.
|
||||
// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API.
|
||||
// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#d2e459
|
||||
func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) {
|
||||
apiEndpoint := "rest/auth/1/session"
|
||||
body := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}{
|
||||
username,
|
||||
password,
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest("POST", apiEndpoint, body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
session := new(Session)
|
||||
resp, err := s.client.Do(req, session)
|
||||
if resp.StatusCode != 200 || err != nil {
|
||||
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
|
||||
}
|
||||
|
||||
s.client.session = session
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TODO Missing API Call GET (Returns information about the currently authenticated user's session)
|
||||
// See https://docs.atlassian.com/jira/REST/latest/#d2e456
|
||||
// TODO Missing API Call DELETE (Logs the current user out of JIRA, destroying the existing session, if any.)
|
||||
// https://docs.atlassian.com/jira/REST/latest/#d2e456
|
19
errors.go
Normal file
19
errors.go
Normal file
@ -0,0 +1,19 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ErrorResponse reports one or more errors caused by an API request.
|
||||
type ErrorResponse struct {
|
||||
Response *http.Response // HTTP response that caused this error
|
||||
ErrorMessages []string `json:"errorMessages,omitempty"`
|
||||
Errors map[string]string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
func (r *ErrorResponse) Error() string {
|
||||
return fmt.Sprintf("%v %v: %d %v %+v",
|
||||
r.Response.Request.Method, r.Response.Request.URL,
|
||||
r.Response.StatusCode, r.ErrorMessages, r.Errors)
|
||||
}
|
250
issue.go
Normal file
250
issue.go
Normal file
@ -0,0 +1,250 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
// AssigneeAutomatic represents the value of the "Assignee: Automatic" of JIRA
|
||||
AssigneeAutomatic = "-1"
|
||||
)
|
||||
|
||||
// IssueService handles Issues for the JIRA instance / API.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#d2e2279
|
||||
type IssueService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Issue represents a JIRA issue.
|
||||
type Issue struct {
|
||||
Expand string `json:"expand,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Fields *IssueFields `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// IssueFields represents single fields of a JIRA issue.
|
||||
// Every JIRA issue has several fields attached.
|
||||
type IssueFields struct {
|
||||
// TODO Missing fields
|
||||
// * "timespent": null,
|
||||
// * "fixVersions": [],
|
||||
// * "aggregatetimespent": null,
|
||||
// * "workratio": -1,
|
||||
// * "lastViewed": null,
|
||||
// * "labels": [],
|
||||
// * "timeestimate": null,
|
||||
// * "aggregatetimeoriginalestimate": null,
|
||||
// * "timeoriginalestimate": null,
|
||||
// * "timetracking": {},
|
||||
// * "attachment": [],
|
||||
// * "aggregatetimeestimate": null,
|
||||
// * "subtasks": [],
|
||||
// * "environment": null,
|
||||
// * "duedate": null,
|
||||
IssueType IssueType `json:"issuetype"`
|
||||
Project Project `json:"project"`
|
||||
Resolution *Resolution `json:"resolution,omitempty"`
|
||||
Priority *Priority `json:"priority,omitempty"`
|
||||
Resolutiondate string `json:"resolutiondate,omitempty"`
|
||||
Created string `json:"created,omitempty"`
|
||||
Watches *Watches `json:"watches,omitempty"`
|
||||
Assignee *Assignee `json:"assignee"`
|
||||
Updated string `json:"updated,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Summary string `json:"summary"`
|
||||
Creator *Assignee `json:"Creator,omitempty"`
|
||||
Reporter *Assignee `json:"reporter,omitempty"`
|
||||
Components []*Component `json:"components,omitempty"`
|
||||
Status *Status `json:"status,omitempty"`
|
||||
Progress *Progress `json:"progress,omitempty"`
|
||||
AggregateProgress *Progress `json:"aggregateprogress,omitempty"`
|
||||
Worklog *Worklog `json:"worklog,omitempty"`
|
||||
IssueLinks []*IssueLink `json:"issuelinks,omitempty"`
|
||||
Comments *CommentList `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// IssueType represents a type of a JIRA issue.
|
||||
// Typical types are "Request", "Bug", "Story", ...
|
||||
type IssueType struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
IconURL string `json:"iconUrl,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Subtask bool `json:"subtask,omitempty"`
|
||||
}
|
||||
|
||||
// Project represents a JIRA Project.
|
||||
type Project struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
AvatarURLs map[string]string `json:"avatarUrls,omitempty"`
|
||||
}
|
||||
|
||||
// Resolution represents a resolution of a JIRA issue.
|
||||
// Typical types are "Fixed", "Suspended", "Won't Fix", ...
|
||||
type Resolution struct {
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Priority represents a priority of a JIRA issue.
|
||||
// Typical types are "Normal", "Moderate", "Urgent", ...
|
||||
type Priority struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
IconURL string `json:"iconUrl,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// Watches represents a type of how many user are "observing" a JIRA issue to track the status / updates.
|
||||
type Watches struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
WatchCount int `json:"watchCount,omitempty"`
|
||||
IsWatching bool `json:"isWatching,omitempty"`
|
||||
}
|
||||
|
||||
// Assignee represents a user who is this JIRA issue assigned to.
|
||||
type Assignee struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
EmailAddress string `json:"emailAddress,omitempty"`
|
||||
AvatarURLs map[string]string `json:"avatarUrls,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Active bool `json:"active,omitempty"`
|
||||
}
|
||||
|
||||
// Component represents a "component" of a JIRA issue.
|
||||
// Components can be user defined in every JIRA instance.
|
||||
type Component struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// Status represents the current status of a JIRA issue.
|
||||
// Typical status are "Open", "In Progress", "Closed", ...
|
||||
// Status can be user defined in every JIRA instance.
|
||||
type Status struct {
|
||||
Self string `json:"self"`
|
||||
Description string `json:"description"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
StatusCategory StatusCategory `json:"statusCategory"`
|
||||
}
|
||||
|
||||
// StatusCategory represents the category a status belongs to.
|
||||
// Those categories can be user defined in every JIRA instance.
|
||||
type StatusCategory struct {
|
||||
Self string `json:"self"`
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
ColorName string `json:"colorName"`
|
||||
}
|
||||
|
||||
// Progress represents the progress of a JIRA issue.
|
||||
type Progress struct {
|
||||
Progress int `json:"progress"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// Worklog represents the work log of a JIRA issue.
|
||||
// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
|
||||
type Worklog struct {
|
||||
// Missing fields
|
||||
// * "worklogs": []
|
||||
StartAt int `json:"startAt"`
|
||||
MaxResults int `json:"maxResults"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// IssueLink represents a link between two issues in JIRA.
|
||||
type IssueLink struct {
|
||||
ID string `json:"id"`
|
||||
Self string `json:"self"`
|
||||
Type IssueLinkType `json:"type"`
|
||||
OutwardIssue Issue `json:"outwardIssue"`
|
||||
InwardIssue Issue `json:"inwardIssue"`
|
||||
}
|
||||
|
||||
// IssueLinkType represents a type of a link between to issues in JIRA.
|
||||
// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
|
||||
type IssueLinkType struct {
|
||||
ID string `json:"id"`
|
||||
Self string `json:"self"`
|
||||
Name string `json:"name"`
|
||||
Inward string `json:"inward"`
|
||||
Outward string `json:"outward"`
|
||||
}
|
||||
|
||||
// Comment represents a comment by a person to an issue in JIRA.
|
||||
type Comment struct {
|
||||
Self string `json:"self"`
|
||||
Name string `json:"name"`
|
||||
Author Assignee `json:"author"`
|
||||
Body string `json:"body"`
|
||||
UpdateAuthor Assignee `json:"updateAuthor"`
|
||||
Updated string `json:"updated"`
|
||||
Created string `json:"created"`
|
||||
}
|
||||
|
||||
// CommentList represents a list of comments by various persons of an issue in JIRA.
|
||||
type CommentList struct {
|
||||
StartAt int `json:"startAt"`
|
||||
MaxResults int `json:"maxResults"`
|
||||
Total int `json:"total"`
|
||||
Comments []Comment `json:"comments"`
|
||||
}
|
||||
|
||||
// Get returns a full representation of the issue for the given issue key.
|
||||
// JIRA will attempt to identify the issue by the issueIdOrKey path parameter.
|
||||
// This can be an issue id, or an issue key.
|
||||
// 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/#d2e2609
|
||||
func (s *IssueService) Get(issueID string) (*Issue, *http.Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
|
||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
issue := new(Issue)
|
||||
resp, err := s.client.Do(req, issue)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return issue, resp, nil
|
||||
}
|
||||
|
||||
// Create creates an issue or a sub-task from a JSON representation.
|
||||
// Creating a sub-task is similar to creating a regular issue, with two important differences:
|
||||
// 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/#d2e2280
|
||||
func (s *IssueService) Create(issue *Issue) (*Issue, *http.Response, error) {
|
||||
apiEndpoint := "rest/api/2/issue/"
|
||||
req, err := s.client.NewRequest("POST", apiEndpoint, issue)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
responseIssue := new(Issue)
|
||||
resp, err := s.client.Do(req, responseIssue)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return responseIssue, resp, nil
|
||||
}
|
131
jira.go
Normal file
131
jira.go
Normal file
@ -0,0 +1,131 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 prefered 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}
|
||||
|
||||
return c, 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 session cookie if there is one
|
||||
if c.session != nil {
|
||||
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", c.session.Session.Name, c.session.Session.Value))
|
||||
}
|
||||
|
||||
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{}) (*http.Response, error) {
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = CheckResponse(resp)
|
||||
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
|
||||
}
|
||||
|
||||
if v != nil {
|
||||
err = json.NewDecoder(resp.Body).Decode(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.
|
||||
// API error responses are expected to have either no response body, or a JSON response body that maps to ErrorResponse.
|
||||
// Any other response body will be silently ignored.
|
||||
func CheckResponse(r *http.Response) error {
|
||||
if c := r.StatusCode; 200 <= c && c <= 299 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errorResponse := &ErrorResponse{Response: r}
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
if err == nil && data != nil {
|
||||
json.Unmarshal(data, errorResponse)
|
||||
}
|
||||
return errorResponse
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user