jira-cli/pkg/jira/create.go

350 lines
9.1 KiB
Go

package jira
import (
"context"
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/ankitpokhrel/jira-cli/pkg/adf"
"github.com/ankitpokhrel/jira-cli/pkg/md"
)
// CreateResponse struct holds response from POST /issue endpoint.
type CreateResponse struct {
ID string `json:"id"`
Key string `json:"key"`
}
// CreateRequest struct holds request data for create request.
type CreateRequest struct {
Project string
Name string
IssueType string
// ParentIssueKey is required when creating a sub-task for classic project.
// This can also be used to attach epic for next-gen project.
ParentIssueKey string
Summary string
Body interface{} // string in v1/v2 and adf.ADF in v3
Reporter string
Assignee string
Priority string
Labels []string
Components []string
FixVersions []string
AffectsVersions []string
OriginalEstimate string
// EpicField is the dynamic epic field name
// that changes per jira installation.
EpicField string
// SubtaskField is usually called "Sub-task" but is
// case-sensitive in Jira and can differ slightly
// in different Jira versions.
SubtaskField string
// CustomFields holds all custom fields passed
// while creating an issue.
CustomFields map[string]string
projectType string
installationType string
configuredCustomFields []IssueTypeField
}
// ForProjectType sets jira project type.
func (cr *CreateRequest) ForProjectType(pt string) {
cr.projectType = pt
}
// ForInstallationType sets jira project type.
func (cr *CreateRequest) ForInstallationType(it string) {
cr.installationType = it
}
// WithCustomFields sets valid custom fields for the issue.
func (cr *CreateRequest) WithCustomFields(cf []IssueTypeField) {
cr.configuredCustomFields = cf
}
// Create creates an issue using v3 version of the POST /issue endpoint.
func (c *Client) Create(req *CreateRequest) (*CreateResponse, error) {
return c.create(req, apiVersion3)
}
// CreateV2 creates an issue using v2 version of the POST /issue endpoint.
func (c *Client) CreateV2(req *CreateRequest) (*CreateResponse, error) {
return c.create(req, apiVersion2)
}
func (c *Client) create(req *CreateRequest, ver string) (*CreateResponse, error) {
data := c.getRequestData(req)
body, err := json.Marshal(&data)
if err != nil {
return nil, err
}
header := Header{
"Accept": "application/json",
"Content-Type": "application/json",
}
var res *http.Response
switch ver {
case apiVersion2:
res, err = c.PostV2(context.Background(), "/issue", body, header)
default:
res, err = c.Post(context.Background(), "/issue", body, header)
}
if err != nil {
return nil, err
}
if res == nil {
return nil, ErrEmptyResponse
}
defer func() { _ = res.Body.Close() }()
if res.StatusCode != http.StatusCreated {
return nil, formatUnexpectedResponse(res)
}
var out CreateResponse
err = json.NewDecoder(res.Body).Decode(&out)
return &out, err
}
func (*Client) getRequestData(req *CreateRequest) *createRequest {
if req.Labels == nil {
req.Labels = []string{}
}
cf := createFields{
Project: struct {
Key string `json:"key"`
}{Key: req.Project},
IssueType: struct {
Name string `json:"name"`
}{Name: req.IssueType},
Name: req.Name,
Summary: req.Summary,
Labels: req.Labels,
epicField: req.EpicField,
}
switch v := req.Body.(type) {
case string:
cf.Description = md.ToJiraMD(v)
case *adf.ADF:
cf.Description = v
}
data := createRequest{
Update: struct{}{},
Fields: createFieldsMarshaler{cf},
}
if req.ParentIssueKey != "" {
subtaskField := IssueTypeSubTask
if req.SubtaskField != "" {
subtaskField = req.SubtaskField
}
if req.projectType == ProjectTypeNextGen || data.Fields.M.IssueType.Name == subtaskField {
data.Fields.M.Parent = &struct {
Key string `json:"key"`
}{Key: req.ParentIssueKey}
} else {
data.Fields.M.Name = req.ParentIssueKey
}
}
if req.Reporter != "" {
if req.installationType == InstallationTypeLocal {
data.Fields.M.Reporter = &nameOrAccountID{Name: &req.Reporter}
} else {
data.Fields.M.Reporter = &nameOrAccountID{AccountID: &req.Reporter}
}
}
if req.Assignee != "" {
if req.installationType == InstallationTypeLocal {
data.Fields.M.Assignee = &nameOrAccountID{Name: &req.Assignee}
} else {
data.Fields.M.Assignee = &nameOrAccountID{AccountID: &req.Assignee}
}
}
if req.Priority != "" {
data.Fields.M.Priority = &struct {
Name string `json:"name,omitempty"`
}{Name: req.Priority}
}
if len(req.Components) > 0 {
comps := make([]struct {
Name string `json:"name,omitempty"`
}, 0, len(req.Components))
for _, c := range req.Components {
comps = append(comps, struct {
Name string `json:"name,omitempty"`
}{c})
}
data.Fields.M.Components = comps
}
if len(req.FixVersions) > 0 {
versions := make([]struct {
Name string `json:"name,omitempty"`
}, 0, len(req.FixVersions))
for _, v := range req.FixVersions {
versions = append(versions, struct {
Name string `json:"name,omitempty"`
}{v})
}
data.Fields.M.FixVersions = versions
}
if len(req.AffectsVersions) > 0 {
versions := make([]struct {
Name string `json:"name,omitempty"`
}, 0, len(req.AffectsVersions))
for _, v := range req.AffectsVersions {
versions = append(versions, struct {
Name string `json:"name,omitempty"`
}{v})
}
data.Fields.M.AffectsVersions = versions
}
if req.OriginalEstimate != "" {
data.Fields.M.TimeTracking = &struct {
OriginalEstimate string `json:"originalEstimate,omitempty"`
}{OriginalEstimate: req.OriginalEstimate}
}
constructCustomFields(req.CustomFields, req.configuredCustomFields, &data)
return &data
}
func constructCustomFields(fields map[string]string, configuredFields []IssueTypeField, data *createRequest) {
if len(fields) == 0 || len(configuredFields) == 0 {
return
}
data.Fields.M.customFields = make(customField)
for key, val := range fields {
for _, configured := range configuredFields {
identifier := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(configured.Name)), " ", "-")
if identifier != strings.ToLower(key) {
continue
}
switch configured.Schema.DataType {
case customFieldFormatOption:
data.Fields.M.customFields[configured.Key] = customFieldTypeOption{Value: val}
case customFieldFormatProject:
data.Fields.M.customFields[configured.Key] = customFieldTypeProject{Value: val}
case customFieldFormatArray:
pieces := strings.Split(strings.TrimSpace(val), ",")
if configured.Schema.Items == customFieldFormatOption {
items := make([]customFieldTypeOption, 0)
for _, p := range pieces {
items = append(items, customFieldTypeOption{Value: p})
}
data.Fields.M.customFields[configured.Key] = items
} else {
data.Fields.M.customFields[configured.Key] = pieces
}
case customFieldFormatNumber:
num, err := strconv.ParseFloat(val, 64) //nolint:gomnd
if err != nil {
// Let Jira API handle data type error for now.
data.Fields.M.customFields[configured.Key] = val
} else {
data.Fields.M.customFields[configured.Key] = customFieldTypeNumber(num)
}
default:
data.Fields.M.customFields[configured.Key] = val
}
}
}
}
type createRequest struct {
Update struct{} `json:"update"`
Fields createFieldsMarshaler `json:"fields"`
}
type nameOrAccountID struct {
Name *string `json:"name,omitempty"` // For local installation.
AccountID *string `json:"accountId,omitempty"` // For cloud installation.
}
type createFields struct {
Project struct {
Key string `json:"key"`
} `json:"project"`
IssueType struct {
Name string `json:"name"`
} `json:"issuetype"`
Parent *struct {
Key string `json:"key"`
} `json:"parent,omitempty"`
Name string `json:"name,omitempty"`
Summary string `json:"summary"`
Description interface{} `json:"description,omitempty"`
Reporter *nameOrAccountID `json:"reporter,omitempty"`
Assignee *nameOrAccountID `json:"assignee,omitempty"`
Priority *struct {
Name string `json:"name,omitempty"`
} `json:"priority,omitempty"`
Labels []string `json:"labels,omitempty"`
Components []struct {
Name string `json:"name,omitempty"`
} `json:"components,omitempty"`
FixVersions []struct {
Name string `json:"name,omitempty"`
} `json:"fixVersions,omitempty"`
AffectsVersions []struct {
Name string `json:"name,omitempty"`
} `json:"versions,omitempty"`
TimeTracking *struct {
OriginalEstimate string `json:"originalEstimate,omitempty"`
} `json:"timetracking,omitempty"`
epicField string
customFields customField
}
type createFieldsMarshaler struct {
M createFields
}
// MarshalJSON is a custom marshaler to handle dynamic field.
func (cfm *createFieldsMarshaler) MarshalJSON() ([]byte, error) {
m, err := json.Marshal(cfm.M)
if err != nil {
return m, err
}
var temp interface{}
if err := json.Unmarshal(m, &temp); err != nil {
return nil, err
}
dm := temp.(map[string]interface{})
if epic, ok := dm["name"]; ok {
if cfm.M.epicField != "" {
dm[cfm.M.epicField] = epic
}
delete(dm, "name")
}
for key, val := range cfm.M.customFields {
dm[key] = val
}
return json.Marshal(dm)
}