diff --git a/go.mod b/go.mod index b7510785..bbda54b2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ctreminiom/go-atlassian -go 1.14 +go 1.18 require ( dario.cat/mergo v1.0.0 @@ -8,3 +8,12 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.1 ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 7855961f..20ba2177 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,13 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= @@ -25,6 +18,5 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/infra/models/errors.go b/pkg/infra/models/errors.go index 7e272309..2120a90a 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") + ErrNoCustomTypeError = errors.New("custom-field: no custom type 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_issue_custom_fields.go b/pkg/infra/models/jira_issue_custom_fields.go index f895ae35..2b3699e7 100644 --- a/pkg/infra/models/jira_issue_custom_fields.go +++ b/pkg/infra/models/jira_issue_custom_fields.go @@ -4,9 +4,10 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/tidwall/gjson" "reflect" "time" + + "github.com/tidwall/gjson" ) // ParseMultiSelectCustomField parses a multi-select custom field from the given buffer data diff --git a/pkg/infra/models/jira_issue_custom_fields_go118.go b/pkg/infra/models/jira_issue_custom_fields_go118.go new file mode 100644 index 00000000..39e20402 --- /dev/null +++ b/pkg/infra/models/jira_issue_custom_fields_go118.go @@ -0,0 +1,141 @@ +//go:build go1.18 +// +build go1.18 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/tidwall/gjson" +) + +// ParseCustomField parses a generic custom field from the given buffer data associated +// with the specified custom field ID and returns a pointers to the generic T. +// +// Parameters: +// - customfieldID: A string representing the unique identifier of the custom field. +// - buffer: A bytes.Buffer containing the serialized data to be parsed. +// +// Returns: +// - *T: A pointer to CustomFieldContextOptionSchema +// structs representing the parsed generic custom field values. +// +// The ParseCustomField method is responsible for extracting and parsing the serialized +// data from the provided buffer, which is expected to be in a specific format. +// It then constructs and returns a pointer to T that represent the parsed generic +// custom field. +// +// Example usage: +// +// type MyType struct { +// FieldInsideCustomField string +// } +// customfieldID := "customfield_10001" +// buffer := bytes.NewBuffer([]byte{ /* Serialized data */ }) +// options, err := ParseCustomField[MyType](customfieldID, buffer) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Printf("Custom Field Value: %+v\n", customFieldValue.FieldInsideCustomField) +// +// Docs: https://docs.go-atlassian.io/cookbooks/extract-customfields-from-issue-s#parse-customfield +func ParseCustomField[T any](buffer bytes.Buffer, customField string) (*T, error) { + raw := gjson.ParseBytes(buffer.Bytes()) + path := fmt.Sprintf("fields.%v", customField) + + // Check if the buffer contains the "issues" 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, ErrNoCustomTypeError + } + + var value *T + if err := json.Unmarshal([]byte(raw.Get(path).String()), &value); err != nil { + return nil, ErrNoCustomTypeError + } + + return value, nil +} + +// ParseCustomFields extracts and parses generic custom field 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 pointer of T a generic type, +// representing the parsed custom field values. +// +// If the custom field data cannot be parsed successfully, an error is returned. +// +// Example Usage: +// +// type MyType struct { +// FieldInsideCustomField string +// } +// customFieldName := "customfield_10001" +// buffer := // Populate the buffer with JSON data +// customFields, err := ParseCustomFields[MyType](customFieldName, buffer) +// if err != nil { +// // Handle the error +// } +// +// // Iterate through the parsed custom fields +// for issueKey, customFieldValue := range customFields { +// fmt.Printf("Issue Key: %s\n", issueKey) +// fmt.Printf("Custom Field Value: %+v\n", customFieldValue.FieldInsideCustomField) +// } +// +// Parameters: +// - customField: The name of the generic custom field to parse. +// - buffer: A bytes.Buffer containing JSON data representing custom field values. +// +// Returns: +// - map[string]*T: A map where the key is the issue key and the +// value is a pointer of T representing the parsed generic custom field 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-customfields +func ParseCustomFields[T any](buffer bytes.Buffer, customField string) (map[string]*T, 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]*T) + 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 customFields *T + if err := json.Unmarshal([]byte(value.Get(path).String()), &customFields); err != nil { + return true + } + + customFieldsAsMap[issueKey] = customFields + 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_go118_test.go b/pkg/infra/models/jira_issue_custom_fields_go118_test.go new file mode 100644 index 00000000..260e2c06 --- /dev/null +++ b/pkg/infra/models/jira_issue_custom_fields_go118_test.go @@ -0,0 +1,259 @@ +package models + +import ( + "bytes" + "reflect" + "testing" +) + +func TestParseCustomField(t *testing.T) { + + bufferMocked := bytes.Buffer{} + bufferMocked.WriteString(` +{ + "fields": { + "customfield_10046": { + "MyFieldName": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10044" + } + } +}`) + + bufferMockedWithNoFields := bytes.Buffer{} + bufferMockedWithNoFields.WriteString(` +{ + "field_no_mapped": { + "customfield_10046": { + "MyFieldName": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10044" + } + } +}`) + + bufferMockedWithNoJSON := bytes.Buffer{} + bufferMockedWithNoJSON.WriteString(`{}{`) + + bufferMockedWithNoInfo := bytes.Buffer{} + bufferMockedWithNoInfo.WriteString(` +{ + "fields": { + "w": null + } +}`) + + bufferMockedWithInvalidType := bytes.Buffer{} + bufferMockedWithInvalidType.WriteString(` +{ + "fields": { + "customfield_10046": "Test field sample" + } +}`) + + type CustomType struct { + MyFieldName string + } + + type args struct { + buffer bytes.Buffer + customField string + } + type testCase[T any] struct { + name string + args args + want *T + wantErr bool + Err error + } + tests := []testCase[CustomType]{ + { + name: "when the buffer contains information", + args: args{ + buffer: bufferMocked, + customField: "customfield_10046", + }, + want: &CustomType{ + MyFieldName: "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10044", + }, + wantErr: false, + }, + + { + name: "when the buffer no contains information", + args: args{ + buffer: bufferMockedWithNoInfo, + customField: "customfield_10046", + }, + want: nil, + wantErr: true, + Err: ErrNoCustomTypeError, + }, + + { + 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_10046", + }, + want: nil, + wantErr: true, + Err: ErrNoCustomTypeError, + }, + + { + name: "when the buffer cannot be parsed", + args: args{ + buffer: bufferMockedWithNoJSON, + customField: "customfield_10046", + }, + want: nil, + wantErr: true, + Err: ErrNoFieldInformationError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseCustomField[CustomType](tt.args.buffer, tt.args.customField) + if (err != nil) != tt.wantErr { + t.Errorf("ParseCustomField() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseCustomField() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(err, tt.Err) { + t.Errorf("ParseCustomField() got = (%v), want (%v)", err, tt.Err) + } + }) + } +} + +func TestParseCustomFields(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_10046": { + "MyFieldName": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10044" + } + } + } + ] +}`) + + 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_10046": { + "MyFieldName": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10044" + } + } + } + ] +}`) + + bufferMockedWithNoInfo := bytes.Buffer{} + bufferMockedWithNoInfo.WriteString(` +{ + "expand": "names,schema", + "startAt": 0, + "maxResults": 50, + "total": 1, + "data": [ + { + "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", + "id": "10035", + "self": "https://ctreminiom.atlassian.net/rest/api/2/issue/10035", + "key": "KP-22", + "fields": { + "customfield_10046": null + } + } + ] +}`) + + type CustomType struct { + MyFieldName string + } + + type args struct { + buffer bytes.Buffer + customField string + } + type testCase[T any] struct { + name string + args args + want map[string]*T + wantErr bool + Err error + } + tests := []testCase[CustomType]{ + { + name: "when the buffer contains information", + args: args{ + buffer: bufferMocked, + customField: "customfield_10046", + }, + want: map[string]*CustomType{ + "KP-22": { + MyFieldName: "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10044", + }, + }, + wantErr: false, + }, + + { + name: "when the buffer does not contain the issues object", + args: args{ + buffer: bufferMockedWithNoInfo, + customField: "customfield_10046", + }, + wantErr: true, + Err: ErrNoIssuesSliceError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseCustomFields[CustomType](tt.args.buffer, tt.args.customField) + if (err != nil) != tt.wantErr { + t.Errorf("ParseCustomFields() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseCustomFields() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(err, tt.Err) { + t.Errorf("ParseCustomFields() got = (%v), want (%v)", err, tt.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..7c3d63f0 100644 --- a/pkg/infra/models/jira_issue_custom_fields_test.go +++ b/pkg/infra/models/jira_issue_custom_fields_test.go @@ -14,24 +14,24 @@ func TestParseMultiSelectField(t *testing.T) { { "fields": { "customfield_10046": [ - { - "self": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10044", - "value": "Option 1", - "id": "10044", - "optionId": "12222", - "disabled": true - }, - { - "self": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10046", - "value": "Option 3", - "id": "10046" - }, - { - "self": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10047", - "value": "Option 4", - "id": "10047" - } - ] + { + "self": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10044", + "value": "Option 1", + "id": "10044", + "optionId": "12222", + "disabled": true + }, + { + "self": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10046", + "value": "Option 3", + "id": "10046" + }, + { + "self": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10047", + "value": "Option 4", + "id": "10047" + } + ] } }`) @@ -40,17 +40,17 @@ func TestParseMultiSelectField(t *testing.T) { { "field_no_mapped": { "customfield_10046": [ - { - "self": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10044", - "value": "Option 1", - "id": "10044" - }, - { - "self": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10046", - "value": "Option 3", - "id": "10046" - } - ] + { + "self": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10044", + "value": "Option 1", + "id": "10044" + }, + { + "self": "https://ctreminiom.atlassian.net/rest/api/3/customFieldOption/10046", + "value": "Option 3", + "id": "10046" + } + ] } }`) @@ -179,17 +179,17 @@ func TestParseMultiGroupPickerField(t *testing.T) { { "fields": { "customfield_10052": [ - { - "name": "jira-administrators", - "groupId": "1da6f895-2b42-423b-8bfb-1e09ee8d7764", - "self": "https://ctreminiom.atlassian.net/rest/api/3/group?groupId=1da6f895-2b42-423b-8bfb-1e09ee8d7764" - }, - { - "name": "jira-administrators-system", - "groupId": "be9ba0ab-ecdc-445b-9ce6-b95202026c1a", - "self": "https://ctreminiom.atlassian.net/rest/api/3/group?groupId=be9ba0ab-ecdc-445b-9ce6-b95202026c1a" - } - ] + { + "name": "jira-administrators", + "groupId": "1da6f895-2b42-423b-8bfb-1e09ee8d7764", + "self": "https://ctreminiom.atlassian.net/rest/api/3/group?groupId=1da6f895-2b42-423b-8bfb-1e09ee8d7764" + }, + { + "name": "jira-administrators-system", + "groupId": "be9ba0ab-ecdc-445b-9ce6-b95202026c1a", + "self": "https://ctreminiom.atlassian.net/rest/api/3/group?groupId=be9ba0ab-ecdc-445b-9ce6-b95202026c1a" + } + ] } }`) @@ -198,17 +198,17 @@ func TestParseMultiGroupPickerField(t *testing.T) { { "field_no_mapped": { "customfield_10052": [ - { - "name": "jira-administrators", - "groupId": "1da6f895-2b42-423b-8bfb-1e09ee8d7764", - "self": "https://ctreminiom.atlassian.net/rest/api/3/group?groupId=1da6f895-2b42-423b-8bfb-1e09ee8d7764" - }, - { - "name": "jira-administrators-system", - "groupId": "be9ba0ab-ecdc-445b-9ce6-b95202026c1a", - "self": "https://ctreminiom.atlassian.net/rest/api/3/group?groupId=be9ba0ab-ecdc-445b-9ce6-b95202026c1a" - } - ] + { + "name": "jira-administrators", + "groupId": "1da6f895-2b42-423b-8bfb-1e09ee8d7764", + "self": "https://ctreminiom.atlassian.net/rest/api/3/group?groupId=1da6f895-2b42-423b-8bfb-1e09ee8d7764" + }, + { + "name": "jira-administrators-system", + "groupId": "be9ba0ab-ecdc-445b-9ce6-b95202026c1a", + "self": "https://ctreminiom.atlassian.net/rest/api/3/group?groupId=be9ba0ab-ecdc-445b-9ce6-b95202026c1a" + } + ] } }`)