diff --git a/pkg/infra/models/errors.go b/pkg/infra/models/errors.go index 7e272309..21eb2896 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 8abbf2e9..d5995358 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 00000000..ebd45399 --- /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 +} 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 00000000..90932abd --- /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 75d1a2c5..30e4ba8d 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(`