Repository: ctreminiom/go-atlassian
Pull request #289 (2024-07-04 06:08:53)

Added parse function for tempo account
This allowes parsing the custom field account from the jira plugin Tempo.
Which is one of the build in provided custom fields.


Patch:
diff --git a/pkg/infra/models/errors.go b/pkg/infra/models/errors.go
index 7e27230..21eb289 100644
--- a/pkg/infra/models/errors.go
+++ b/pkg/infra/models/errors.go
@@ -160,6 +160,7 @@ var (
 	ErrNoCheckBoxTypeError                 = errors.New("custom-field: no check-box type set")
 	ErrNoCascadingParentError              = errors.New("custom-field: no cascading parent value set")
 	ErrNoCascadingChildError               = errors.New("custom-field: no cascading child value set")
+	ErrNoTempoAccountTypeError             = errors.New("custom-field: no tempo account value set")
 	ErrNoAttachmentIdsError                = errors.New("sm: no attachment id's set")
 	ErrNoLabelsError                       = errors.New("sm: no label names set")
 	ErrNoComponentsError                   = errors.New("sm: no components set")
diff --git a/pkg/infra/models/jira_field.go b/pkg/infra/models/jira_field.go
index 8abbf2e..d599535 100644
--- a/pkg/infra/models/jira_field.go
+++ b/pkg/infra/models/jira_field.go
@@ -82,3 +82,8 @@ type CustomFieldRequestTypeLinkScheme struct {
 	Web      string `json:"web,omitempty"`      // The web link for the custom field request type.
 	Agent    string `json:"agent,omitempty"`    // The agent link for the custom field request type.
 }
+
+type CustomFieldTempoAccountScheme struct {
+	ID    int    `json:"id"`
+	Value string `json:"value"`
+}
diff --git a/pkg/infra/models/jira_issue_custom_fields_tempo.go b/pkg/infra/models/jira_issue_custom_fields_tempo.go
new file mode 100644
index 0000000..ebd4539
--- /dev/null
+++ b/pkg/infra/models/jira_issue_custom_fields_tempo.go
@@ -0,0 +1,134 @@
+package models
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+
+	"github.com/tidwall/gjson"
+)
+
+// ParseTempoAccountCustomField parses the Jira Tempo account type elements from the given buffer
+// data associated with the specified custom field ID and returns a struct CustomFieldTempoAccountScheme
+//
+// Parameters:
+//   - customfieldID: A string representing the unique identifier of the custom field.
+//   - buffer: A bytes.Buffer containing the serialized data to be parsed.
+//
+// Returns:
+//   - *CustomFieldTempoAccountScheme: the customfield value as CustomFieldTempoAccountScheme type
+//
+// Example usage:
+//
+//	customfieldID := "customfield_10038"
+//	buffer := bytes.NewBuffer([]byte{ /* Serialized data */ })
+//	tempoAccount, err := ParseTempoAccountCustomField(customfieldID, buffer)
+//	if err != nil {
+//	    log.Fatal(err)
+//	}
+//	fmt.Println(tempoAccount)
+//
+// Docs: https://docs.go-atlassian.io/cookbooks/extract-customfields-from-issue-s#parse-tempoaccount-customfield
+func ParseTempoAccountCustomField(buffer bytes.Buffer, customField string) (*CustomFieldTempoAccountScheme, error) {
+
+	raw := gjson.ParseBytes(buffer.Bytes())
+	path := fmt.Sprintf("fields.%v", customField)
+
+	// Check if the buffer contains the "fields" object
+	if !raw.Get("fields").Exists() {
+		return nil, ErrNoFieldInformationError
+	}
+
+	// Check if the issue iteration contains information on the customfield selected,
+	// if not, continue
+	if raw.Get(path).Type == gjson.Null {
+		return nil, ErrNoTempoAccountTypeError
+	}
+
+	var tempoAccount *CustomFieldTempoAccountScheme
+	if err := json.Unmarshal([]byte(raw.Get(path).String()), &tempoAccount); err != nil {
+		return nil, ErrNoTempoAccountTypeError
+	}
+
+	return tempoAccount, nil
+}
+
+// ParseTempoAccountCustomFields extracts and parses jira tempo account type customfield data from
+// a given bytes.Buffer from multiple issues
+//
+// This function takes the name of the custom field to parse and a bytes.Buffer containing
+// JSON data representing the custom field values associated with different issues. It returns
+// a map where the key is the issue key and the value is a slice of CustomFieldTempoAccountScheme
+// structs, representing the parsed assets associated with a Jira issues.
+//
+// The JSON data within the buffer is expected to have a specific structure where the custom field
+// values are organized by issue keys and options are represented within a context. The function
+// parses this structure to extract and organize the custom field values.
+//
+// If the custom field data cannot be parsed successfully, an error is returned.
+//
+// Example Usage:
+//
+//	customFieldName := "customfield_10038"
+//	buffer := // Populate the buffer with JSON data
+//	customFields, err := ParseTempoAccountCustomFields(customFieldName, buffer)
+//	if err != nil {
+//	    // Handle the error
+//	}
+//
+//	// Iterate through the parsed custom fields
+//	for issueKey, customFieldValues := range customFields {
+//	    fmt.Printf("Issue Key: %s\n", issueKey)
+//	    fmt.Printf("Custom Field Value: %+v\n", customFieldValues)
+//	}
+//
+// Parameters:
+//   - customField: The name of the request type custom field to parse.
+//   - buffer: A bytes.Buffer containing JSON data representing custom field values.
+//
+// Returns:
+//   - map[string]*ParseTempoAccountCustomFields: A map where the key is the issue key and the
+//     value is a ParseTempoAccountCustomFields struct representing the parsed
+//     jira tempo account type values.
+//   - error: An error if there was a problem parsing the custom field data or if the JSON data
+//     did not conform to the expected structure.
+//
+// Docs: https://docs.go-atlassian.io/cookbooks/extract-customfields-from-issue-s#parse-requesttype-customfields
+func ParseTempoAccountCustomFields(buffer bytes.Buffer, customField string) (map[string]*CustomFieldTempoAccountScheme, error) {
+
+	raw := gjson.ParseBytes(buffer.Bytes())
+
+	// Check if the buffer contains the "issues" object
+	if !raw.Get("issues").Exists() {
+		return nil, ErrNoIssuesSliceError
+	}
+
+	// Loop through each custom field, extract the information and stores the data on a map
+	customfieldsAsMap := make(map[string]*CustomFieldTempoAccountScheme)
+	raw.Get("issues").ForEach(func(key, value gjson.Result) bool {
+
+		path, issueKey := fmt.Sprintf("fields.%v", customField), value.Get("key").String()
+
+		// Check if the issue iteration contains information on the customfield selected,
+		// if not, continue
+		if value.Get(path).Type == gjson.Null {
+			return true
+		}
+
+		var customField *CustomFieldTempoAccountScheme
+		if err := json.Unmarshal([]byte(value.Get(path).String()), &customField); err != nil {
+			return true
+		}
+
+		customfieldsAsMap[issueKey] = customField
+		return true
+	})
+
+	// Check if the map processed contains elements
+	// if so, return an error interface
+	if len(customfieldsAsMap) == 0 {
+		return nil, ErrNoMapValuesError
+	}
+
+	return customfieldsAsMap, nil
+}


Test patch:
diff --git a/pkg/infra/models/jira_issue_custom_fields_tempo_test.go b/pkg/infra/models/jira_issue_custom_fields_tempo_test.go
new file mode 100644
index 0000000..90932ab
--- /dev/null
+++ b/pkg/infra/models/jira_issue_custom_fields_tempo_test.go
@@ -0,0 +1,288 @@
+package models
+
+import (
+	"bytes"
+	"reflect"
+	"testing"
+)
+
+func TestParseTempoAccountCustomField(t *testing.T) {
+
+	bufferMocked := bytes.Buffer{}
+	bufferMocked.WriteString(`
+{
+  "fields":{
+    "customfield_10036":{
+      "id":22,
+      "value":"SP Datacenter"
+    }
+  }
+}`)
+
+	bufferMockedWithNoFields := bytes.Buffer{}
+	bufferMockedWithNoFields.WriteString(`
+{
+  "no_fields":{
+    "customfield_10036":{
+      "id":22,
+      "value":"SP Datacenter"
+    }
+  }
+}`)
+
+	bufferMockedWithNoJSON := bytes.Buffer{}
+	bufferMockedWithNoJSON.WriteString(`{}{`)
+
+	bufferMockedWithNoInfo := bytes.Buffer{}
+	bufferMockedWithNoInfo.WriteString(`
+{
+  "fields":{
+    "customfield_10036":null
+  }
+}`)
+
+	bufferMockedWithInvalidType := bytes.Buffer{}
+	bufferMockedWithInvalidType.WriteString(`
+{
+  "fields":{
+    "customfield_10036":""
+  }
+}`)
+
+	type args struct {
+		buffer      bytes.Buffer
+		customField string
+	}
+
+	testCases := []struct {
+		name    string
+		args    args
+		want    *CustomFieldTempoAccountScheme
+		want1   bool
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the buffer contains information",
+			args: args{
+				buffer:      bufferMocked,
+				customField: "customfield_10036",
+			},
+			want: &CustomFieldTempoAccountScheme{
+				ID:    22,
+				Value: "SP Datacenter",
+			},
+			wantErr: false,
+		},
+
+		{
+			name: "when the buffer no contains information",
+			args: args{
+				buffer:      bufferMockedWithNoInfo,
+				customField: "customfield_10036",
+			},
+			want:    nil,
+			wantErr: true,
+			Err:     ErrNoTempoAccountTypeError,
+		},
+
+		{
+			name: "when the buffer does not contains the fields object",
+			args: args{
+				buffer:      bufferMockedWithNoFields,
+				customField: "customfield_10046",
+			},
+			want:    nil,
+			wantErr: true,
+			Err:     ErrNoFieldInformationError,
+		},
+
+		{
+			name: "when the buffer does not contains a valid field type",
+			args: args{
+				buffer:      bufferMockedWithInvalidType,
+				customField: "customfield_10036",
+			},
+			want:    nil,
+			wantErr: true,
+			Err:     ErrNoTempoAccountTypeError,
+		},
+
+		{
+			name: "when the buffer cannot be parsed",
+			args: args{
+				buffer:      bufferMockedWithNoJSON,
+				customField: "customfield_10046",
+			},
+			want:    nil,
+			wantErr: true,
+			Err:     ErrNoFieldInformationError,
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			got, err := ParseTempoAccountCustomField(testCase.args.buffer, testCase.args.customField)
+			if (err != nil) != testCase.wantErr {
+				t.Errorf("ParseTempoAccountCustomField() error = %v, wantErr %v", err, testCase.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, testCase.want) {
+				t.Errorf("ParseTempoAccountCustomField() got = %v, want %v", got, testCase.want)
+			}
+			if !reflect.DeepEqual(err, testCase.Err) {
+				t.Errorf("ParseTempoAccountCustomField() got = (%v), want (%v)", err, testCase.Err)
+			}
+		})
+	}
+}
+
+func TestParseTempoAccountCustomFields(t *testing.T) {
+
+	bufferMocked := bytes.Buffer{}
+	bufferMocked.WriteString(`
+{
+  "expand":"names,schema",
+  "startAt":0,
+  "maxResults":50,
+  "total":1,
+  "issues":[
+    {
+      "expand":"operations,versionedRepresentations,editmeta,changelog,renderedFields",
+      "id":"10035",
+      "self":"https://ctreminiom.atlassian.net/rest/api/2/issue/10035",
+      "key":"KP-22",
+      "fields":{
+        "customfield_10036":{
+          "id":22,
+          "value":"SP Datacenter"
+        }
+      }
+    }
+  ]
+}`)
+
+	bufferMockedWithNoIssues := bytes.Buffer{}
+	bufferMockedWithNoIssues.WriteString(`
+{
+  "expand":"names,schema",
+  "startAt":0,
+  "maxResults":50,
+  "total":1,
+  "no_issues":[
+    {
+      "expand":"operations,versionedRepresentations,editmeta,changelog,renderedFields",
+      "id":"10035",
+      "self":"https://ctreminiom.atlassian.net/rest/api/2/issue/10035",
+      "key":"KP-22",
+      "no_fields":{
+        "customfield_10036":{
+          "id":22,
+          "value":"SP Datacenter"
+        }
+      }
+    }
+  ]
+}`)
+
+	bufferMockedWithNoInfo := bytes.Buffer{}
+	bufferMockedWithNoInfo.WriteString(`
+{
+  "expand":"names,schema",
+  "startAt":0,
+  "maxResults":50,
+  "total":1,
+  "issues":[
+    {
+      "expand":"operations,versionedRepresentations,editmeta,changelog,renderedFields",
+      "id":"10035",
+      "self":"https://ctreminiom.atlassian.net/rest/api/2/issue/10035",
+      "key":"KP-22",
+      "fields":{
+        "customfield_10036":null
+      }
+    }
+  ]
+}`)
+
+	bufferMockedWithInvalidType := bytes.Buffer{}
+	bufferMockedWithInvalidType.WriteString(`
+{
+    "expand": "names,schema",
+    "startAt": 0,
+    "maxResults": 50,
+    "total": 1,
+    "issues": [
+        {
+            "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
+            "id": "10035",
+            "self": "https://ctreminiom.atlassian.net/rest/api/2/issue/10035",
+            "key": "KP-22",
+            "fields": {
+                "customfield_10046": "string"
+            }
+        }
+    ]
+}`)
+
+	type args struct {
+		buffer      bytes.Buffer
+		customField string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    map[string]*CustomFieldTempoAccountScheme
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the buffer contains information",
+			args: args{
+				buffer:      bufferMocked,
+				customField: "customfield_10036",
+			},
+			want: map[string]*CustomFieldTempoAccountScheme{
+				"KP-22": {
+					ID:    22,
+					Value: "SP Datacenter",
+				},
+			},
+			wantErr: false,
+		},
+
+		{
+			name: "when the buffer does not contain the issues object",
+			args: args{
+				buffer:      bufferMockedWithNoIssues,
+				customField: "customfield_10036",
+			},
+			wantErr: true,
+			Err:     ErrNoIssuesSliceError,
+		},
+
+		{
+			name: "when the buffer contains null customfields",
+			args: args{
+				buffer:      bufferMockedWithNoInfo,
+				customField: "customfield_10036",
+			},
+			wantErr: true,
+			Err:     ErrNoMapValuesError,
+		},
+	}
+	for _, testCase := range tests {
+		t.Run(testCase.name, func(t *testing.T) {
+			got, err := ParseTempoAccountCustomFields(testCase.args.buffer, testCase.args.customField)
+			if (err != nil) != testCase.wantErr {
+				t.Errorf("ParseMultiSelectCustomField() error = %v, wantErr %v", err, testCase.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, testCase.want) {
+				t.Errorf("ParseMultiSelectCustomField() got = %v, want %v", got, testCase.want)
+			}
+			if !reflect.DeepEqual(err, testCase.Err) {
+				t.Errorf("ParseMultiSelectCustomField() got = (%v), want (%v)", err, testCase.Err)
+			}
+		})
+	}
+}
diff --git a/pkg/infra/models/jira_issue_custom_fields_test.go b/pkg/infra/models/jira_issue_custom_fields_test.go
index 75d1a2c..30e4ba8 100644
--- a/pkg/infra/models/jira_issue_custom_fields_test.go
+++ b/pkg/infra/models/jira_issue_custom_fields_test.go
@@ -7,7 +7,7 @@ import (
 	"time"
 )
 
-func TestParseMultiSelectField(t *testing.T) {
+func TestParseMultiSelectCustomField(t *testing.T) {
 
 	bufferMocked := bytes.Buffer{}
 	bufferMocked.WriteString(`