mirror of
https://github.com/interviewstreet/go-jira.git
synced 2025-02-09 13:36:58 +02:00
Adds unknown map for arbitrary fields in IssueFields. Adds Custom Marshall,Unmarshall. Adds structs tag where necessary
This commit is contained in:
parent
fe6b129172
commit
5ce765977f
38
board.go
38
board.go
@ -14,20 +14,20 @@ type BoardService struct {
|
||||
|
||||
// BoardsList reflects a list of agile boards
|
||||
type BoardsList struct {
|
||||
MaxResults int `json:"maxResults"`
|
||||
StartAt int `json:"startAt"`
|
||||
Total int `json:"total"`
|
||||
IsLast bool `json:"isLast"`
|
||||
Values []Board `json:"values"`
|
||||
MaxResults int `json:"maxResults" structs:"maxResults"`
|
||||
StartAt int `json:"startAt" structs:"startAt"`
|
||||
Total int `json:"total" structs:"total"`
|
||||
IsLast bool `json:"isLast" structs:"isLast"`
|
||||
Values []Board `json:"values" structs:"values"`
|
||||
}
|
||||
|
||||
// Board represents a JIRA agile board
|
||||
type Board struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
FilterID int `json:"filterId,omitempty"`
|
||||
ID int `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitemtpy"`
|
||||
Type string `json:"type,omitempty" structs:"type,omitempty"`
|
||||
FilterID int `json:"filterId,omitempty" structs:"filterId,omitempty"`
|
||||
}
|
||||
|
||||
// BoardListOptions specifies the optional parameters to the BoardService.GetList
|
||||
@ -46,19 +46,19 @@ type BoardListOptions struct {
|
||||
|
||||
// Wrapper struct for search result
|
||||
type sprintsResult struct {
|
||||
Sprints []Sprint `json:"values"`
|
||||
Sprints []Sprint `json:"values" structs:"values"`
|
||||
}
|
||||
|
||||
// Sprint represents a sprint on JIRA agile board
|
||||
type Sprint struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CompleteDate *time.Time `json:"completeDate"`
|
||||
EndDate *time.Time `json:"endDate"`
|
||||
StartDate *time.Time `json:"startDate"`
|
||||
OriginBoardID int `json:"originBoardId"`
|
||||
Self string `json:"self"`
|
||||
State string `json:"state"`
|
||||
ID int `json:"id" structs:"id"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
CompleteDate *time.Time `json:"completeDate" structs:"completeDate"`
|
||||
EndDate *time.Time `json:"endDate" structs:"endDate"`
|
||||
StartDate *time.Time `json:"startDate" structs:"startDate"`
|
||||
OriginBoardID int `json:"originBoardId" structs:"originBoardId"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
State string `json:"state" structs:"state"`
|
||||
}
|
||||
|
||||
// GetAllBoards will returns all boards. This only includes boards that the user has permission to view.
|
||||
|
369
issue.go
369
issue.go
@ -2,10 +2,14 @@ package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fatih/structs"
|
||||
"github.com/trivago/tgo/tcontainer"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -24,35 +28,35 @@ type IssueService struct {
|
||||
|
||||
// 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"`
|
||||
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Key string `json:"key,omitempty" structs:"key,omitempty"`
|
||||
Fields *IssueFields `json:"fields,omitempty" structs:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// Attachment represents a JIRA attachment
|
||||
type Attachment struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Author *User `json:"author,omitempty"`
|
||||
Created string `json:"created,omitempty"`
|
||||
Size int `json:"size,omitempty"`
|
||||
MimeType string `json:"mimeType,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Thumbnail string `json:"thumbnail,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Filename string `json:"filename,omitempty" structs:"filename,omitempty"`
|
||||
Author *User `json:"author,omitempty" structs:"author,omitempty"`
|
||||
Created string `json:"created,omitempty" structs:"created,omitempty"`
|
||||
Size int `json:"size,omitempty" structs:"size,omitempty"`
|
||||
MimeType string `json:"mimeType,omitempty" structs:"mimeType,omitempty"`
|
||||
Content string `json:"content,omitempty" structs:"content,omitempty"`
|
||||
Thumbnail string `json:"thumbnail,omitempty" structs:"thumbnail,omitempty"`
|
||||
}
|
||||
|
||||
// Epic represents the epic to which an issue is associated
|
||||
// Not that this struct does not process the returned "color" value
|
||||
type Epic struct {
|
||||
ID int `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Self string `json:"self"`
|
||||
Name string `json:"name"`
|
||||
Summary string `json:"summary"`
|
||||
Done bool `json:"done"`
|
||||
ID int `json:"id" structs:"id"`
|
||||
Key string `json:"key" structs:"key"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Summary string `json:"summary" structs:"summary"`
|
||||
Done bool `json:"done" structs:"done"`
|
||||
}
|
||||
|
||||
// IssueFields represents single fields of a JIRA issue.
|
||||
@ -70,124 +74,185 @@ type IssueFields struct {
|
||||
// * "aggregatetimeestimate": null,
|
||||
// * "environment": null,
|
||||
// * "duedate": null,
|
||||
Type IssueType `json:"issuetype"`
|
||||
Project Project `json:"project,omitempty"`
|
||||
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 *User `json:"assignee,omitempty"`
|
||||
Updated string `json:"updated,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Summary string `json:"summary"`
|
||||
Creator *User `json:"Creator,omitempty"`
|
||||
Reporter *User `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 *Comments `json:"comment,omitempty"`
|
||||
FixVersions []*FixVersion `json:"fixVersions,omitempty"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
Subtasks []*Subtasks `json:"subtasks,omitempty"`
|
||||
Attachments []*Attachment `json:"attachment,omitempty"`
|
||||
Epic *Epic `json:"epic,omitempty"`
|
||||
Type IssueType `json:"issuetype" structs:"issuetype"`
|
||||
Project Project `json:"project,omitempty" structs:"project,omitempty"`
|
||||
Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"`
|
||||
Priority *Priority `json:"priority,omitempty" structs:"priority,omitempty"`
|
||||
Resolutiondate string `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"`
|
||||
Created string `json:"created,omitempty" structs:"created,omitempty"`
|
||||
Watches *Watches `json:"watches,omitempty" structs:"watches,omitempty"`
|
||||
Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"`
|
||||
Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
|
||||
Description string `json:"description,omitempty" structs:"description,omitempty"`
|
||||
Summary string `json:"summary" structs:"summary"`
|
||||
Creator *User `json:"Creator,omitempty" structs:"Creator,omitempty"`
|
||||
Reporter *User `json:"reporter,omitempty" structs:"reporter,omitempty"`
|
||||
Components []*Component `json:"components,omitempty" structs:"components,omitempty"`
|
||||
Status *Status `json:"status,omitempty" structs:"status,omitempty"`
|
||||
Progress *Progress `json:"progress,omitempty" structs:"progress,omitempty"`
|
||||
AggregateProgress *Progress `json:"aggregateprogress,omitempty" structs:"aggregateprogress,omitempty"`
|
||||
Worklog *Worklog `json:"worklog,omitempty" structs:"worklog,omitempty"`
|
||||
IssueLinks []*IssueLink `json:"issuelinks,omitempty" structs:"issuelinks,omitempty"`
|
||||
Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"`
|
||||
FixVersions []*FixVersion `json:"fixVersions,omitempty" structs:"fixVersions,omitempty"`
|
||||
Labels []string `json:"labels,omitempty" structs:"labels,omitempty"`
|
||||
Subtasks []*Subtasks `json:"subtasks,omitempty" structs:"subtasks,omitempty"`
|
||||
Attachments []*Attachment `json:"attachment,omitempty" structs:"attachment,omitempty"`
|
||||
Epic *Epic `json:"epic,omitempty" structs:"epic,omitempty"`
|
||||
Unknowns tcontainer.MarshalMap
|
||||
}
|
||||
|
||||
func (i *IssueFields) MarshalJSON() ([]byte, error) {
|
||||
m := structs.Map(i)
|
||||
unknowns, okay := m["Unknowns"]
|
||||
if okay {
|
||||
// if unknowns present, shift all key value from unkown to a level up
|
||||
for key, value := range unknowns.(tcontainer.MarshalMap) {
|
||||
m[key] = value
|
||||
}
|
||||
delete(m, "Unknowns")
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (i *IssueFields) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// Do the normal unmarshalling first
|
||||
// Details for this way: http://choly.ca/post/go-json-marshalling/
|
||||
type Alias IssueFields
|
||||
aux := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(i),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalMap := tcontainer.NewMarshalMap()
|
||||
err := json.Unmarshal(data, &totalMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(*i)
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
tagDetail := field.Tag.Get("json")
|
||||
if tagDetail == "" {
|
||||
// ignore if there are no tags
|
||||
continue
|
||||
}
|
||||
options := strings.Split(tagDetail, ",")
|
||||
|
||||
if len(options) == 0 {
|
||||
return fmt.Errorf("No tags options found for %s", field.Name)
|
||||
}
|
||||
// the first one is the json tag
|
||||
key := options[0]
|
||||
if _, okay := totalMap.Value(key); okay {
|
||||
delete(totalMap, key)
|
||||
}
|
||||
|
||||
}
|
||||
i = (*IssueFields)(aux.Alias)
|
||||
// all the tags found in the struct were removed. Whatever is left are unknowns to struct
|
||||
i.Unknowns = totalMap
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// 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"`
|
||||
AvatarID int `json:"avatarId,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
ID string `json:"id,omitempty" struct:"id,omitempty"`
|
||||
Description string `json:"description,omitempty" struct:"description,omitempty"`
|
||||
IconURL string `json:"iconUrl,omitempty" struct:"iconUrl,omitempty"`
|
||||
Name string `json:"name,omitempty" struct:"name,omitempty"`
|
||||
Subtask bool `json:"subtask,omitempty" struct:"subtask,omitempty"`
|
||||
AvatarID int `json:"avatarId,omitempty" struct:"avatarId,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"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Description string `json:"description" structs:"description"`
|
||||
Name string `json:"name" structs:"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"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"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"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
WatchCount int `json:"watchCount,omitempty" structs:"watchCount,omitempty"`
|
||||
IsWatching bool `json:"isWatching,omitempty" structs:"isWatching,omitempty"`
|
||||
}
|
||||
|
||||
// User represents a user who is this JIRA issue assigned to.
|
||||
type User struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
EmailAddress string `json:"emailAddress,omitempty"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Active bool `json:"active,omitempty"`
|
||||
TimeZone string `json:"timeZone,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||
Key string `json:"key,omitempty" structs:"key,omitempty"`
|
||||
EmailAddress string `json:"emailAddress,omitempty" structs:"emailAddress,omitempty"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"`
|
||||
Active bool `json:"active,omitempty" structs:"active,omitempty"`
|
||||
TimeZone string `json:"timeZone,omitempty" structs:"timeZone,omitempty"`
|
||||
}
|
||||
|
||||
// AvatarUrls represents different dimensions of avatars / images
|
||||
type AvatarUrls struct {
|
||||
Four8X48 string `json:"48x48,omitempty"`
|
||||
Two4X24 string `json:"24x24,omitempty"`
|
||||
One6X16 string `json:"16x16,omitempty"`
|
||||
Three2X32 string `json:"32x32,omitempty"`
|
||||
Four8X48 string `json:"48x48,omitempty" structs:"48x48,omitempty"`
|
||||
Two4X24 string `json:"24x24,omitempty" structs:"24x24,omitempty"`
|
||||
One6X16 string `json:"16x16,omitempty" structs:"16x16,omitempty"`
|
||||
Three2X32 string `json:"32x32,omitempty" structs:"32x32,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"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"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"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
Description string `json:"description" structs:"description"`
|
||||
IconURL string `json:"iconUrl" structs:"iconUrl"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
StatusCategory StatusCategory `json:"statusCategory" structs:"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"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
ID int `json:"id" structs:"id"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Key string `json:"key" structs:"key"`
|
||||
ColorName string `json:"colorName" structs:"colorName"`
|
||||
}
|
||||
|
||||
// Progress represents the progress of a JIRA issue.
|
||||
type Progress struct {
|
||||
Progress int `json:"progress"`
|
||||
Total int `json:"total"`
|
||||
Progress int `json:"progress" structs:"progress"`
|
||||
Total int `json:"total" structs:"total"`
|
||||
}
|
||||
|
||||
// Time represents the Time definition of JIRA as a time.Time of go
|
||||
@ -195,29 +260,29 @@ type Time time.Time
|
||||
|
||||
// Wrapper struct for search result
|
||||
type transitionResult struct {
|
||||
Transitions []Transition `json:"transitions"`
|
||||
Transitions []Transition `json:"transitions" structs:"transitions"`
|
||||
}
|
||||
|
||||
// Transition represents an issue transition in JIRA
|
||||
type Transition struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Fields map[string]TransitionField `json:"fields"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Fields map[string]TransitionField `json:"fields" structs:"fields"`
|
||||
}
|
||||
|
||||
// TransitionField represents the value of one Transistion
|
||||
type TransitionField struct {
|
||||
Required bool `json:"required"`
|
||||
Required bool `json:"required" structs:"required"`
|
||||
}
|
||||
|
||||
// CreateTransitionPayload is used for creating new issue transitions
|
||||
type CreateTransitionPayload struct {
|
||||
Transition TransitionPayload `json:"transition"`
|
||||
Transition TransitionPayload `json:"transition" structs:"transition"`
|
||||
}
|
||||
|
||||
// TransitionPayload represents the request payload of Transistion calls like DoTransition
|
||||
type TransitionPayload struct {
|
||||
ID string `json:"id"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON will transform the JIRA time into a time.Time
|
||||
@ -235,90 +300,90 @@ func (t *Time) UnmarshalJSON(b []byte) error {
|
||||
// One Worklog contains zero or n WorklogRecords
|
||||
// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
|
||||
type Worklog struct {
|
||||
StartAt int `json:"startAt"`
|
||||
MaxResults int `json:"maxResults"`
|
||||
Total int `json:"total"`
|
||||
Worklogs []WorklogRecord `json:"worklogs"`
|
||||
StartAt int `json:"startAt" structs:"startAt"`
|
||||
MaxResults int `json:"maxResults" structs:"maxResults"`
|
||||
Total int `json:"total" structs:"total"`
|
||||
Worklogs []WorklogRecord `json:"worklogs" structs:"worklogs"`
|
||||
}
|
||||
|
||||
// WorklogRecord represents one entry of a Worklog
|
||||
type WorklogRecord struct {
|
||||
Self string `json:"self"`
|
||||
Author User `json:"author"`
|
||||
UpdateAuthor User `json:"updateAuthor"`
|
||||
Comment string `json:"comment"`
|
||||
Created Time `json:"created"`
|
||||
Updated Time `json:"updated"`
|
||||
Started Time `json:"started"`
|
||||
TimeSpent string `json:"timeSpent"`
|
||||
TimeSpentSeconds int `json:"timeSpentSeconds"`
|
||||
ID string `json:"id"`
|
||||
IssueID string `json:"issueId"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
Author User `json:"author" structs:"author"`
|
||||
UpdateAuthor User `json:"updateAuthor" structs:"updateAuthor"`
|
||||
Comment string `json:"comment" structs:"comment"`
|
||||
Created Time `json:"created" structs:"created"`
|
||||
Updated Time `json:"updated" structs:"updated"`
|
||||
Started Time `json:"started" structs:"started"`
|
||||
TimeSpent string `json:"timeSpent" structs:"timeSpent"`
|
||||
TimeSpentSeconds int `json:"timeSpentSeconds" structs:"timeSpentSeconds"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
IssueID string `json:"issueId" structs:"issueId"`
|
||||
}
|
||||
|
||||
// Subtasks represents all issues of a parent issue.
|
||||
type Subtasks struct {
|
||||
ID string `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Self string `json:"self"`
|
||||
Fields IssueFields `json:"fields"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Key string `json:"key" structs:"key"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
Fields IssueFields `json:"fields" structs:"fields"`
|
||||
}
|
||||
|
||||
// IssueLink represents a link between two issues in JIRA.
|
||||
type IssueLink struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
Type IssueLinkType `json:"type"`
|
||||
OutwardIssue *Issue `json:"outwardIssue"`
|
||||
InwardIssue *Issue `json:"inwardIssue"`
|
||||
Comment *Comment `json:"comment,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Type IssueLinkType `json:"type" structs:"type"`
|
||||
OutwardIssue *Issue `json:"outwardIssue" structs:"outwardIssue"`
|
||||
InwardIssue *Issue `json:"inwardIssue" structs:"inwardIssue"`
|
||||
Comment *Comment `json:"comment,omitempty" structs:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// 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,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Inward string `json:"inward"`
|
||||
Outward string `json:"outward"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Inward string `json:"inward" structs:"inward"`
|
||||
Outward string `json:"outward" structs:"outward"`
|
||||
}
|
||||
|
||||
// Comments represents a list of Comment.
|
||||
type Comments struct {
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
Comments []*Comment `json:"comments,omitempty" structs:"comments,omitempty"`
|
||||
}
|
||||
|
||||
// Comment represents a comment by a person to an issue in JIRA.
|
||||
type Comment struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Author User `json:"author,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
UpdateAuthor User `json:"updateAuthor,omitempty"`
|
||||
Updated string `json:"updated,omitempty"`
|
||||
Created string `json:"created,omitempty"`
|
||||
Visibility CommentVisibility `json:"visibility,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:name,omitempty"`
|
||||
Author User `json:"author,omitempty" structs:"author,omitempty"`
|
||||
Body string `json:"body,omitempty" structs:"body,omitempty"`
|
||||
UpdateAuthor User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"`
|
||||
Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
|
||||
Created string `json:"created,omitempty" structs:"created,omitempty"`
|
||||
Visibility CommentVisibility `json:"visibility,omitempty" structs:"visibility,omitempty"`
|
||||
}
|
||||
|
||||
// FixVersion represents a software release in which an issue is fixed.
|
||||
type FixVersion struct {
|
||||
Archived *bool `json:"archived,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ProjectID int `json:"projectId,omitempty"`
|
||||
ReleaseDate string `json:"releaseDate,omitempty"`
|
||||
Released *bool `json:"released,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
UserReleaseDate string `json:"userReleaseDate,omitempty"`
|
||||
Archived *bool `json:"archived,omitempty" structs:"archived,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||
ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"`
|
||||
ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"`
|
||||
Released *bool `json:"released,omitempty" structs:"released,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"`
|
||||
}
|
||||
|
||||
// CommentVisibility represents he visibility of a comment.
|
||||
// E.g. Type could be "role" and Value "Administrators"
|
||||
type CommentVisibility struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Type string `json:"type,omitempty" structs:"type,omitempty"`
|
||||
Value string `json:"value,omitempty" structs:"value,omitempty"`
|
||||
}
|
||||
|
||||
// SearchOptions specifies the optional parameters to various List methods that
|
||||
@ -337,10 +402,10 @@ type SearchOptions struct {
|
||||
// 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"`
|
||||
StartAt int `json:"startAt"`
|
||||
MaxResults int `json:"maxResults"`
|
||||
Total int `json:"total"`
|
||||
Issues []Issue `json:"issues" structs:"issues"`
|
||||
StartAt int `json:"startAt" structs:"startAt"`
|
||||
MaxResults int `json:"maxResults" structs:"maxResults"`
|
||||
Total int `json:"total" structs:"total"`
|
||||
}
|
||||
|
||||
// CustomFields represents custom fields of JIRA
|
||||
|
127
issue_test.go
127
issue_test.go
@ -8,6 +8,8 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/trivago/tgo/tcontainer"
|
||||
)
|
||||
|
||||
func TestIssueService_Get_Success(t *testing.T) {
|
||||
@ -465,3 +467,128 @@ func TestIssueService_DoTransition(t *testing.T) {
|
||||
t.Errorf("Got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueFields_TestMarshalJSON_PopulateUnknownsSuccess(t *testing.T) {
|
||||
data := `{
|
||||
"customfield_123":"test",
|
||||
"description":"example bug report",
|
||||
"project":{
|
||||
"self":"http://www.example.com/jira/rest/api/2/project/EX",
|
||||
"id":"10000",
|
||||
"key":"EX",
|
||||
"name":"Example",
|
||||
"avatarUrls":{
|
||||
"48x48":"http://www.example.com/jira/secure/projectavatar?size=large&pid=10000",
|
||||
"24x24":"http://www.example.com/jira/secure/projectavatar?size=small&pid=10000",
|
||||
"16x16":"http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000",
|
||||
"32x32":"http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000"
|
||||
},
|
||||
"projectCategory":{
|
||||
"self":"http://www.example.com/jira/rest/api/2/projectCategory/10000",
|
||||
"id":"10000",
|
||||
"name":"FIRST",
|
||||
"description":"First Project Category"
|
||||
}
|
||||
},
|
||||
"issuelinks":[
|
||||
{
|
||||
"id":"10001",
|
||||
"type":{
|
||||
"id":"10000",
|
||||
"name":"Dependent",
|
||||
"inward":"depends on",
|
||||
"outward":"is depended by"
|
||||
},
|
||||
"outwardIssue":{
|
||||
"id":"10004L",
|
||||
"key":"PRJ-2",
|
||||
"self":"http://www.example.com/jira/rest/api/2/issue/PRJ-2",
|
||||
"fields":{
|
||||
"status":{
|
||||
"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png",
|
||||
"name":"Open"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id":"10002",
|
||||
"type":{
|
||||
"id":"10000",
|
||||
"name":"Dependent",
|
||||
"inward":"depends on",
|
||||
"outward":"is depended by"
|
||||
},
|
||||
"inwardIssue":{
|
||||
"id":"10004",
|
||||
"key":"PRJ-3",
|
||||
"self":"http://www.example.com/jira/rest/api/2/issue/PRJ-3",
|
||||
"fields":{
|
||||
"status":{
|
||||
"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png",
|
||||
"name":"Open"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}`
|
||||
|
||||
i := new(IssueFields)
|
||||
err := json.Unmarshal([]byte(data), i)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, recieved %s", err)
|
||||
}
|
||||
|
||||
if len(i.Unknowns) != 1 {
|
||||
t.Errorf("Expected 1 unknown field to be present, recieved %d", len(i.Unknowns))
|
||||
}
|
||||
if i.Description != "example bug report" {
|
||||
t.Errorf("Expected description to be \"%s\", recieved \"%s\"", "example bug report", i.Description)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIssueFields_MarshalJSON_Success(t *testing.T) {
|
||||
/*
|
||||
{
|
||||
"customfield_123":"test",
|
||||
"description":"example bug report",
|
||||
"project":{
|
||||
"self":"http://www.example.com/jira/rest/api/2/project/EX",
|
||||
"id":"10000",
|
||||
"key":"EX"
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
i := &IssueFields{
|
||||
Description: "example bug report",
|
||||
Unknowns: tcontainer.MarshalMap{
|
||||
"customfield_123": "test",
|
||||
},
|
||||
Project: Project{
|
||||
Self: "http://www.example.com/jira/rest/api/2/project/EX",
|
||||
ID: "10000",
|
||||
Key: "EX",
|
||||
},
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil err, recieved %s", err)
|
||||
}
|
||||
|
||||
recieved := new(IssueFields)
|
||||
// the order of json might be different. so unmarshal it again and comapre objects
|
||||
err = json.Unmarshal(bytes, recieved)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil err, recieved %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(i, recieved) {
|
||||
t.Errorf("Recieved object different from expected")
|
||||
}
|
||||
|
||||
}
|
||||
|
50
metaissue.go
50
metaissue.go
@ -2,6 +2,7 @@ package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/trivago/tgo/tcontainer"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -29,14 +30,14 @@ type MetaProject struct {
|
||||
// expect these for a general way. This will be returning a map.
|
||||
// Further processing must be done depending on what is required.
|
||||
type MetaIssueTypes struct {
|
||||
Self string `json:"expand,omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
IconUrl string `json:"iconurl,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Subtasks bool `json:"subtask,omitempty"`
|
||||
Expand string `json:"expand,omitempty"`
|
||||
Fields map[string]interface{} `json:"fields,omitempty"`
|
||||
Self string `json:"expand,omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
IconUrl string `json:"iconurl,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Subtasks bool `json:"subtask,omitempty"`
|
||||
Expand string `json:"expand,omitempty"`
|
||||
Fields tcontainer.MarshalMap `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// GetCreateMeta makes the api call to get the meta information required to create a ticket
|
||||
@ -98,24 +99,35 @@ func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueTypes {
|
||||
// }
|
||||
// the returned map would have "Epic Link" as the key and "customfield_10806" as value.
|
||||
// This choice has been made so that the it is easier to generate the create api request later.
|
||||
func (t *MetaIssueTypes) GetMandatoryFields() map[string]string {
|
||||
func (t *MetaIssueTypes) GetMandatoryFields() (map[string]string, error) {
|
||||
ret := make(map[string]string)
|
||||
for key, obj := range t.Fields {
|
||||
details := obj.(map[string]interface{})
|
||||
if details["required"] == true {
|
||||
ret[details["name"].(string)] = key
|
||||
for key, _ := range t.Fields {
|
||||
required, err := t.Fields.Bool(key + "/required")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if required {
|
||||
name, err := t.Fields.String(key + "/name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[name] = key
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// GetAllFields returns a map of all the fields for an IssueType. This includes all required and not required.
|
||||
// The key of the returned map is what you see in the form and the value is how it is representated in the jira schema.
|
||||
func (t *MetaIssueTypes) GetAllFields() map[string]string {
|
||||
func (t *MetaIssueTypes) GetAllFields() (map[string]string, error) {
|
||||
ret := make(map[string]string)
|
||||
for key, obj := range t.Fields {
|
||||
details := obj.(map[string]interface{})
|
||||
ret[details["name"].(string)] = key
|
||||
for key, _ := range t.Fields {
|
||||
|
||||
name, err := t.Fields.String(key + "/name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[name] = key
|
||||
}
|
||||
return ret
|
||||
return ret, nil
|
||||
}
|
||||
|
@ -377,3 +377,68 @@ func TestIssueService_GetCreateMeta_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMetaIssueTypes_GetMandatoryFields(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["summary"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Summary",
|
||||
}
|
||||
|
||||
data["components"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Components",
|
||||
}
|
||||
|
||||
data["epicLink"] = map[string]interface{}{
|
||||
"required": false,
|
||||
"name": "Epic Link",
|
||||
}
|
||||
|
||||
m := new(MetaIssueTypes)
|
||||
m.Fields = data
|
||||
|
||||
mandatory, err := m.GetMandatoryFields()
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, recieved %s", err)
|
||||
}
|
||||
|
||||
if len(mandatory) != 2 {
|
||||
t.Errorf("Expected 2 recieved %d", mandatory)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMetaIssueTypes_GetAllFields(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["summary"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Summary",
|
||||
}
|
||||
|
||||
data["components"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Components",
|
||||
}
|
||||
|
||||
data["epicLink"] = map[string]interface{}{
|
||||
"required": false,
|
||||
"name": "Epic Link",
|
||||
}
|
||||
|
||||
m := new(MetaIssueTypes)
|
||||
m.Fields = data
|
||||
|
||||
mandatory, err := m.GetAllFields()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil err, recieved %s", err)
|
||||
}
|
||||
|
||||
if len(mandatory) != 3 {
|
||||
t.Errorf("Expected 3 recieved %d", mandatory)
|
||||
}
|
||||
|
||||
}
|
||||
|
98
project.go
98
project.go
@ -13,72 +13,72 @@ type ProjectService struct {
|
||||
|
||||
// ProjectList represent a list of Projects
|
||||
type ProjectList []struct {
|
||||
Expand string `json:"expand"`
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls"`
|
||||
ProjectTypeKey string `json:"projectTypeKey"`
|
||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty"`
|
||||
Expand string `json:"expand" structs:"expand"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Key string `json:"key" structs:"key"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls" structs:"avatarUrls"`
|
||||
ProjectTypeKey string `json:"projectTypeKey" structs:"projectTypeKey"`
|
||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectsCategory,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectCategory represents a single project category
|
||||
type ProjectCategory struct {
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Description string `json:"description" structs:"description"`
|
||||
}
|
||||
|
||||
// Project represents a JIRA Project.
|
||||
type Project struct {
|
||||
Expand string `json:"expand,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Lead User `json:"lead,omitempty"`
|
||||
Components []ProjectComponent `json:"components,omitempty"`
|
||||
IssueTypes []IssueType `json:"issueTypes,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
AssigneeType string `json:"assigneeType,omitempty"`
|
||||
Versions []Version `json:"versions,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Key string `json:"key,omitempty" structs:"key,omitempty"`
|
||||
Description string `json:"description,omitempty" structs:"description,omitempty"`
|
||||
Lead User `json:"lead,omitempty" structs:"lead,omitempty"`
|
||||
Components []ProjectComponent `json:"components,omitempty" structs:"components,omitempty"`
|
||||
IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"`
|
||||
URL string `json:"url,omitempty" structs:"url,omitempty"`
|
||||
Email string `json:"email,omitempty" structs:"email,omitempty"`
|
||||
AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"`
|
||||
Versions []Version `json:"versions,omitempty" structs:"versions,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||
Roles struct {
|
||||
Developers string `json:"Developers,omitempty"`
|
||||
} `json:"roles,omitempty"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"`
|
||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty"`
|
||||
Developers string `json:"Developers,omitempty" structs:"Developers,omitempty"`
|
||||
} `json:"roles,omitempty" structs:"roles,omitempty"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"`
|
||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectCategory,omitempty"`
|
||||
}
|
||||
|
||||
// Version represents a single release version of a project
|
||||
type Version struct {
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Archived bool `json:"archived"`
|
||||
Released bool `json:"released"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
UserReleaseDate string `json:"userReleaseDate"`
|
||||
ProjectID int `json:"projectId"` // Unlike other IDs, this is returned as a number
|
||||
Self string `json:"self" structs:"self"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Archived bool `json:"archived" structs:"archived"`
|
||||
Released bool `json:"released" structs:"released"`
|
||||
ReleaseDate string `json:"releaseDate" structs:"releaseDate"`
|
||||
UserReleaseDate string `json:"userReleaseDate" structs:"userReleaseDate"`
|
||||
ProjectID int `json:"projectId" structs:"projectId"` // Unlike other IDs, this is returned as a number
|
||||
}
|
||||
|
||||
// ProjectComponent represents a single component of a project
|
||||
type ProjectComponent struct {
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Lead User `json:"lead"`
|
||||
AssigneeType string `json:"assigneeType"`
|
||||
Assignee User `json:"assignee"`
|
||||
RealAssigneeType string `json:"realAssigneeType"`
|
||||
RealAssignee User `json:"realAssignee"`
|
||||
IsAssigneeTypeValid bool `json:"isAssigneeTypeValid"`
|
||||
Project string `json:"project"`
|
||||
ProjectID int `json:"projectId"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Description string `json:"description" structs:"description"`
|
||||
Lead User `json:"lead" structs:"lead"`
|
||||
AssigneeType string `json:"assigneeType" structs:"assigneeType"`
|
||||
Assignee User `json:"assignee" structs:"assignee"`
|
||||
RealAssigneeType string `json:"realAssigneeType" structs:"realAssigneeType"`
|
||||
RealAssignee User `json:"realAssignee" structs:"realAssignee"`
|
||||
IsAssigneeTypeValid bool `json:"isAssigneeTypeValid" structs:"isAssigneeTypeValid"`
|
||||
Project string `json:"project" structs:"project"`
|
||||
ProjectID int `json:"projectId" structs:"projectId"`
|
||||
}
|
||||
|
||||
// GetList gets all projects form JIRA
|
||||
|
Loading…
x
Reference in New Issue
Block a user