Repository: ankitpokhrel/jira-cli
Pull request #38 (2020-12-23 17:46:00)

feat: Add epic field name in config during init
Related: #37



Patch:
diff --git a/internal/cmd/init/init.go b/internal/cmd/init/init.go
index cb89cba..4b7bcbb 100644
--- a/internal/cmd/init/init.go
+++ b/internal/cmd/init/init.go
@@ -32,6 +32,8 @@ func initialize(*cobra.Command, []string) {
 			fmt.Printf("\033[0;32m✓\033[0m Skipping config generation. Current config: %s\n", viper.ConfigFileUsed())
 		case jira.ErrUnexpectedStatusCode:
 			cmdutil.PrintErrF("\n\033[0;31m✗\033[0m Received unexpected status code from jira. Please try again.")
+		case jiraConfig.ErrUnexpectedResponseFormat:
+			cmdutil.PrintErrF("\n\033[0;31m✗\033[0m Got response in unexpected format when fetching metadata. Please try again.")
 		default:
 			cmdutil.PrintErrF("\n\033[0;31m✗\033[0m Unable to generate configuration: %s", err.Error())
 		}
diff --git a/internal/config/generator.go b/internal/config/generator.go
index 9fd90fe..8242e1e 100644
--- a/internal/config/generator.go
+++ b/internal/config/generator.go
@@ -5,6 +5,7 @@ import (
 	"net/url"
 	"os"
 	"regexp"
+	"strings"
 
 	"github.com/AlecAivazis/survey/v2"
 	"github.com/mitchellh/go-homedir"
@@ -20,8 +21,12 @@ const (
 	configFile = ".jira.yml"
 )
 
-// ErrSkip is returned when a user skips the config generation.
-var ErrSkip = fmt.Errorf("skipping config generation")
+var (
+	// ErrSkip is returned when a user skips the config generation.
+	ErrSkip = fmt.Errorf("skipping config generation")
+	// ErrUnexpectedResponseFormat is returned if the response data is in unexpected format.
+	ErrUnexpectedResponseFormat = fmt.Errorf("unexpected response format")
+)
 
 // JiraCLIConfig is a Jira CLI config.
 type JiraCLIConfig struct {
@@ -30,6 +35,7 @@ type JiraCLIConfig struct {
 		login   string
 		project string
 		board   *jira.Board
+		epic    *jira.Epic
 	}
 	jiraClient         *jira.Client
 	projectSuggestions []string
@@ -56,14 +62,15 @@ func (c *JiraCLIConfig) Generate() error {
 	if ce && !shallOverwrite() {
 		return ErrSkip
 	}
-
 	if err := c.configureServerAndLoginDetails(); err != nil {
 		return err
 	}
-
 	if err := c.configureProjectAndBoardDetails(); err != nil {
 		return err
 	}
+	if err := c.configureMetadata(); err != nil {
+		return err
+	}
 
 	if err := func() error {
 		s := cmdutil.Info("Creating new configuration...")
@@ -97,12 +104,10 @@ func (c *JiraCLIConfig) configureServerAndLoginDetails() error {
 				if !ok {
 					return errInvalidURL
 				}
-
 				u, err := url.Parse(str)
 				if err != nil || u.Scheme == "" || u.Host == "" {
 					return errInvalidURL
 				}
-
 				if u.Scheme != "http" && u.Scheme != "https" {
 					return errInvalidURL
 				}
@@ -128,11 +133,9 @@ func (c *JiraCLIConfig) configureServerAndLoginDetails() error {
 				if !ok {
 					return errInvalidEmail
 				}
-
 				if len(str) < 3 || len(str) > 254 {
 					return errInvalidEmail
 				}
-
 				if !emailRegex.MatchString(str) {
 					return errInvalidEmail
 				}
@@ -184,7 +187,6 @@ func (c *JiraCLIConfig) configureProjectAndBoardDetails() error {
 		Help:    "This is your project key that you want to access by default when using the cli.",
 		Options: c.projectSuggestions,
 	}
-
 	err := survey.AskOne(&projectPrompt, &project, survey.WithValidator(survey.Required))
 	if err != nil {
 		return err
@@ -211,6 +213,42 @@ func (c *JiraCLIConfig) configureProjectAndBoardDetails() error {
 	return nil
 }
 
+func (c *JiraCLIConfig) configureMetadata() error {
+	s := cmdutil.Info("Fetching metadata...")
+	defer s.Stop()
+
+	meta, err := c.jiraClient.GetCreateMeta(&jira.CreateMetaRequest{
+		Projects:       c.value.project,
+		IssueTypeNames: jira.IssueTypeEpic,
+		Expand:         "projects.issuetypes.fields",
+	})
+	if err != nil {
+		return err
+	}
+	if len(meta.Projects) == 0 || len(meta.Projects[0].IssueTypes) == 0 {
+		return ErrUnexpectedResponseFormat
+	}
+
+	fields := meta.Projects[0].IssueTypes[0].Fields
+
+	var key string
+
+	for field, value := range fields {
+		if !strings.Contains(field, "customfield") {
+			continue
+		}
+		v := value.(map[string]interface{})
+		if v["name"].(string) == "Epic Name" {
+			key = v["key"].(string)
+			break
+		}
+	}
+
+	c.value.epic = &jira.Epic{Field: key}
+
+	return nil
+}
+
 func (c *JiraCLIConfig) write() error {
 	viper.Set("server", c.value.server)
 	viper.Set("login", c.value.login)
@@ -218,6 +256,7 @@ func (c *JiraCLIConfig) write() error {
 	viper.Set("board.id", c.value.board.ID)
 	viper.Set("board.name", c.value.board.Name)
 	viper.Set("board.type", c.value.board.Type)
+	viper.Set("epic.field", c.value.epic.Field)
 
 	return viper.WriteConfig()
 }
@@ -230,7 +269,6 @@ func (c *JiraCLIConfig) getProjectSuggestions() error {
 	if err != nil {
 		return err
 	}
-
 	for _, project := range projects {
 		c.projectSuggestions = append(c.projectSuggestions, project.Key)
 	}
@@ -246,7 +284,6 @@ func (c *JiraCLIConfig) getBoardSuggestions(project string) error {
 	if err != nil {
 		return err
 	}
-
 	for _, board := range resp.Boards {
 		c.boardsMap[board.Name] = board
 		c.boardSuggestions = append(c.boardSuggestions, board.Name)
@@ -272,7 +309,6 @@ func shallOverwrite() bool {
 	prompt := &survey.Confirm{
 		Message: "Config already exist. Do you want to overwrite?",
 	}
-
 	if err := survey.AskOne(prompt, &ans); err != nil {
 		return false
 	}
diff --git a/pkg/jira/createmeta.go b/pkg/jira/createmeta.go
new file mode 100644
index 0000000..3cfbdc6
--- /dev/null
+++ b/pkg/jira/createmeta.go
@@ -0,0 +1,55 @@
+package jira
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// CreateMetaRequest struct holds request data for createmeta request.
+type CreateMetaRequest struct {
+	Projects       string
+	IssueTypeNames string
+	Expand         string
+}
+
+// CreateMetaResponse struct holds response from POST /issue/createmeta endpoint.
+type CreateMetaResponse struct {
+	Projects []struct {
+		Key        string `json:"key"`
+		Name       string `json:"name"`
+		IssueTypes []struct {
+			Name   string                 `json:"name"`
+			Fields map[string]interface{} `json:"fields"`
+		} `json:"issuetypes"`
+	} `json:"projects"`
+}
+
+// GetCreateMeta gets create metadata using GET /issue/createmeta endpoint.
+func (c *Client) GetCreateMeta(req *CreateMetaRequest) (*CreateMetaResponse, error) {
+	path := fmt.Sprintf(
+		"/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=%s",
+		req.Projects, req.IssueTypeNames, req.Expand,
+	)
+
+	res, err := c.Get(context.Background(), path, nil)
+	if err != nil {
+		return nil, err
+	}
+	if res == nil {
+		return nil, ErrEmptyResponse
+	}
+
+	defer func() { _ = res.Body.Close() }()
+
+	if res.StatusCode != http.StatusOK {
+		return nil, ErrUnexpectedStatusCode
+	}
+
+	var out CreateMetaResponse
+
+	err = json.NewDecoder(res.Body).Decode(&out)
+
+	return &out, err
+}
diff --git a/pkg/jira/types.go b/pkg/jira/types.go
index 3bb5333..919e28c 100644
--- a/pkg/jira/types.go
+++ b/pkg/jira/types.go
@@ -21,6 +21,11 @@ type Board struct {
 	Type string `json:"type"`
 }
 
+// Epic holds epic info.
+type Epic struct {
+	Field string `json:"field"`
+}
+
 // Issue holds issue info.
 type Issue struct {
 	Key    string      `json:"key"`


Test patch:
diff --git a/pkg/jira/createmeta_test.go b/pkg/jira/createmeta_test.go
new file mode 100644
index 0000000..8d83108
--- /dev/null
+++ b/pkg/jira/createmeta_test.go
@@ -0,0 +1,95 @@
+package jira
+
+import (
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetCreateMeta(t *testing.T) {
+	var unexpectedStatusCode bool
+
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "/rest/api/3/issue/createmeta", r.URL.Path)
+
+		qs := r.URL.Query()
+
+		if unexpectedStatusCode {
+			w.WriteHeader(400)
+		} else {
+			assert.Equal(t, url.Values{
+				"projectKeys":    []string{"TEST"},
+				"issuetypeNames": []string{"Epic"},
+				"expand":         []string{"projects.issuetypes.fields"},
+			}, qs)
+
+			resp, err := ioutil.ReadFile("./testdata/createmeta.json")
+			assert.NoError(t, err)
+
+			w.Header().Set("Content-Type", "application/json")
+			w.WriteHeader(200)
+			_, _ = w.Write(resp)
+		}
+	}))
+	defer server.Close()
+
+	client := NewClient(Config{Server: server.URL}, WithTimeout(3))
+
+	actual, err := client.GetCreateMeta(&CreateMetaRequest{
+		Projects:       "TEST",
+		IssueTypeNames: "Epic",
+		Expand:         "projects.issuetypes.fields",
+	})
+	assert.NoError(t, err)
+
+	expected := &CreateMetaResponse{[]struct {
+		Key        string `json:"key"`
+		Name       string `json:"name"`
+		IssueTypes []struct {
+			Name   string                 `json:"name"`
+			Fields map[string]interface{} `json:"fields"`
+		} `json:"issuetypes"`
+	}{
+		{
+			Key:  "TEST",
+			Name: "Test Project",
+			IssueTypes: []struct {
+				Name   string                 `json:"name"`
+				Fields map[string]interface{} `json:"fields"`
+			}{
+				{
+					Name: "Epic",
+					Fields: map[string]interface{}{
+						"customfield_10011": map[string]interface{}{
+							"name": "Epic Name",
+							"key":  "customfield_10011",
+						},
+						"priority": map[string]interface{}{
+							"name": "Priority",
+							"key":  "priority",
+						},
+						"customfield_10014": map[string]interface{}{
+							"name": "Epic Link",
+							"key":  "customfield_10014",
+						},
+					},
+				},
+			},
+		},
+	}}
+
+	assert.Equal(t, expected, actual)
+
+	unexpectedStatusCode = true
+
+	_, err = client.GetCreateMeta(&CreateMetaRequest{
+		Projects:       "TEST",
+		IssueTypeNames: "Epic",
+		Expand:         "projects.issuetypes.fields",
+	})
+	assert.Error(t, ErrUnexpectedStatusCode, err)
+}
diff --git a/pkg/jira/testdata/createmeta.json b/pkg/jira/testdata/createmeta.json
new file mode 100644
index 0000000..9bd27d6
--- /dev/null
+++ b/pkg/jira/testdata/createmeta.json
@@ -0,0 +1,29 @@
+{
+  "expand": "projects",
+  "projects": [
+    {
+      "expand": "issuetypes",
+      "key": "TEST",
+      "name": "Test Project",
+      "issuetypes": [
+        {
+          "name": "Epic",
+          "fields": {
+            "customfield_10011": {
+              "name": "Epic Name",
+              "key": "customfield_10011"
+            },
+            "priority": {
+              "name": "Priority",
+              "key": "priority"
+            },
+            "customfield_10014": {
+              "name": "Epic Link",
+              "key": "customfield_10014"
+            }
+          }
+        }
+      ]
+    }
+  ]
+}