Repository: ctreminiom/go-atlassian
Pull request #284 (2024-07-01 05:28:56)

Added DateScheme and DateTimeScheme
The `DateTimeScheme` represents the so called `date-time` data format in Jira:
<img width="342" alt="image" src="https://github.com/ctreminiom/go-atlassian/assets/1900106/3f841b13-0259-47f0-a538-3d52fd4a1c63">
Its a little bit different to the `time.RFC3339` where the timezone hours and minutes are spereated by `:` while Jira or atlassian do not have seperators in the timezone, therefore a dedicated type with custom marshal and unmarshal functions is needed.

The `DateScheme` is for only dates like `2012-01-01` which is is used for types in `issues`.


Patch:
diff --git a/pkg/infra/models/jira_issue_v2.go b/pkg/infra/models/jira_issue_v2.go
index c985366..d953b61 100644
--- a/pkg/infra/models/jira_issue_v2.go
+++ b/pkg/infra/models/jira_issue_v2.go
@@ -2,7 +2,6 @@ package models
 
 import (
 	"encoding/json"
-	"time"
 
 	"dario.cat/mergo"
 )
@@ -112,13 +111,13 @@ type IssueFieldsSchemeV2 struct {
 	Reporter                 *UserScheme                     `json:"reporter,omitempty"`
 	Assignee                 *UserScheme                     `json:"assignee,omitempty"`
 	Resolution               *ResolutionScheme               `json:"resolution,omitempty"`
-	ResolutionDate           string                          `json:"resolutiondate,omitempty"`
+	ResolutionDate           *DateTimeScheme                 `json:"resolutiondate,omitempty"`
 	Workratio                int                             `json:"workratio,omitempty"`
-	StatusCategoryChangeDate string                          `json:"statuscategorychangedate,omitempty"`
+	StatusCategoryChangeDate *DateTimeScheme                 `json:"statuscategorychangedate,omitempty"`
 	LastViewed               string                          `json:"lastViewed,omitempty"`
 	Summary                  string                          `json:"summary,omitempty"`
-	Created                  *time.Time                      `json:"created,omitempty"`
-	Updated                  *time.Time                      `json:"updated,omitempty"`
+	Created                  *DateTimeScheme                 `json:"created,omitempty"`
+	Updated                  *DateTimeScheme                 `json:"updated,omitempty"`
 	Labels                   []string                        `json:"labels,omitempty"`
 	Status                   *StatusScheme                   `json:"status,omitempty"`
 	Description              string                          `json:"description,omitempty"`
@@ -126,7 +125,7 @@ type IssueFieldsSchemeV2 struct {
 	Subtasks                 []*IssueScheme                  `json:"subtasks,omitempty"`
 	Security                 *SecurityScheme                 `json:"security,omitempty"`
 	Worklog                  *IssueWorklogRichTextPageScheme `json:"worklog,omitempty"`
-	DueDate                  string                          `json:"duedate,omitempty"`
+	DueDate                  *DateScheme                     `json:"duedate,omitempty"`
 }
 
 // ParentScheme represents the parent of an issue in Jira.
diff --git a/pkg/infra/models/jira_issue_v3.go b/pkg/infra/models/jira_issue_v3.go
index cbeea33..36ff1f7 100644
--- a/pkg/infra/models/jira_issue_v3.go
+++ b/pkg/infra/models/jira_issue_v3.go
@@ -2,7 +2,6 @@ package models
 
 import (
 	"encoding/json"
-	"time"
 
 	"dario.cat/mergo"
 )
@@ -112,13 +111,13 @@ type IssueFieldsScheme struct {
 	Reporter                 *UserScheme                `json:"reporter,omitempty"`                 // The user who reported the issue.
 	Assignee                 *UserScheme                `json:"assignee,omitempty"`                 // The user assigned to the issue.
 	Resolution               *ResolutionScheme          `json:"resolution,omitempty"`               // The resolution of the issue.
-	ResolutionDate           string                     `json:"resolutiondate,omitempty"`           // The date the issue was resolved.
+	Resolutiondate           *DateTimeScheme            `json:"resolutiondate,omitempty"`           // The date the issue was resolved.
 	Workratio                int                        `json:"workratio,omitempty"`                // The work ratio of the issue.
-	StatusCategoryChangeDate string                     `json:"statuscategorychangedate,omitempty"` // The date the status category changed.
+	StatusCategoryChangeDate *DateTimeScheme            `json:"statuscategorychangedate,omitempty"` // The date the status category changed.
 	LastViewed               string                     `json:"lastViewed,omitempty"`               // The last time the issue was viewed.
 	Summary                  string                     `json:"summary,omitempty"`                  // The summary of the issue.
-	Created                  *time.Time                 `json:"created,omitempty"`                  // The date the issue was created.
-	Updated                  *time.Time                 `json:"updated,omitempty"`                  // The date the issue was last updated.
+	Created                  *DateTimeScheme            `json:"created,omitempty"`                  // The date the issue was created.
+	Updated                  *DateTimeScheme            `json:"updated,omitempty"`                  // The date the issue was last updated.
 	Labels                   []string                   `json:"labels,omitempty"`                   // The labels associated with the issue.
 	Status                   *StatusScheme              `json:"status,omitempty"`                   // The status of the issue.
 	Description              *CommentNodeScheme         `json:"description,omitempty"`              // The description of the issue.
@@ -127,7 +126,7 @@ type IssueFieldsScheme struct {
 	Security                 *SecurityScheme            `json:"security,omitempty"`                 // The security level of the issue.
 	Attachment               []*AttachmentScheme        `json:"attachment,omitempty"`               // The attachments of the issue.
 	Worklog                  *IssueWorklogADFPageScheme `json:"worklog,omitempty"`                  // The worklog of the issue.
-	DueDate                  string                     `json:"duedate,omitempty"`                  // The due date of the issue.
+	DueDate                  *DateScheme                `json:"duedate,omitempty"`                  // The due date of the issue.
 }
 
 // IssueTransitionScheme represents a transition of an issue in Jira.
diff --git a/pkg/infra/models/jira_project.go b/pkg/infra/models/jira_project.go
index fe394da..3a487e0 100644
--- a/pkg/infra/models/jira_project.go
+++ b/pkg/infra/models/jira_project.go
@@ -133,11 +133,11 @@ type ProjectScheme struct {
 	Insight           *ProjectInsightScheme  `json:"insight,omitempty"`           // The insight of the project.
 	Category          *ProjectCategoryScheme `json:"projectCategory,omitempty"`   // The category of the project.
 	Deleted           bool                   `json:"deleted,omitempty"`           // Indicates if the project is deleted.
-	RetentionTillDate string                 `json:"retentionTillDate,omitempty"` // The retention till date of the project.
-	DeletedDate       string                 `json:"deletedDate,omitempty"`       // The date the project was deleted.
+	RetentionTillDate *DateTimeScheme        `json:"retentionTillDate,omitempty"` // The retention till date of the project.
+	DeletedDate       *DateTimeScheme        `json:"deletedDate,omitempty"`       // The date the project was deleted.
 	DeletedBy         *UserScheme            `json:"deletedBy,omitempty"`         // The user who deleted the project.
 	Archived          bool                   `json:"archived,omitempty"`          // Indicates if the project is archived.
-	ArchivedDate      string                 `json:"archivedDate,omitempty"`      // The date the project was archived.
+	ArchivedDate      *DateTimeScheme        `json:"archivedDate,omitempty"`      // The date the project was archived.
 	ArchivedBy        *UserScheme            `json:"archivedBy,omitempty"`        // The user who archived the project.
 }
 
diff --git a/pkg/infra/models/jira_time.go b/pkg/infra/models/jira_time.go
new file mode 100644
index 0000000..b514a74
--- /dev/null
+++ b/pkg/infra/models/jira_time.go
@@ -0,0 +1,50 @@
+package models
+
+import "time"
+
+const (
+	TimeFormat = "2006-01-02T15:04:05-0700"
+	DateFormat = "2006-01-02"
+)
+
+// DateScheme is a custom time type for Jira dates.
+type DateScheme time.Time
+
+func (d *DateScheme) MarshalJSON() ([]byte, error) {
+	return []byte(time.Time(*d).Format(DateFormat)), nil
+}
+
+func (d *DateScheme) UnmarshalJSON(data []byte) error {
+	if string(data) == "null" {
+		return nil
+	}
+
+	parsed, err := time.Parse(`"`+DateFormat+`"`, string(data))
+	if err != nil {
+		return err
+	}
+
+	*d = DateScheme(parsed)
+	return nil
+}
+
+// DateTimeScheme is a custom time type for Jira times.
+type DateTimeScheme time.Time
+
+func (d *DateTimeScheme) MarshalJSON() ([]byte, error) {
+	return []byte(time.Time(*d).Format(TimeFormat)), nil
+}
+
+func (d *DateTimeScheme) UnmarshalJSON(data []byte) error {
+	if string(data) == "null" {
+		return nil
+	}
+
+	parsed, err := time.Parse(`"`+TimeFormat+`"`, string(data))
+	if err != nil {
+		return err
+	}
+
+	*d = DateTimeScheme(parsed)
+	return nil
+}
diff --git a/pkg/infra/models/sm_info.go b/pkg/infra/models/sm_info.go
index 18667bc..e38e8d2 100644
--- a/pkg/infra/models/sm_info.go
+++ b/pkg/infra/models/sm_info.go
@@ -12,10 +12,10 @@ type InfoScheme struct {
 
 // InfoBuildDataScheme represents the build date of a system.
 type InfoBuildDataScheme struct {
-	ISO8601     string `json:"iso8601,omitempty"`     // The ISO 8601 format of the build date.
-	Jira        string `json:"jira,omitempty"`        // The Jira format of the build date.
-	Friendly    string `json:"friendly,omitempty"`    // The friendly format of the build date.
-	EpochMillis int64  `json:"epochMillis,omitempty"` // The epoch milliseconds of the build date.
+	ISO8601     DateTimeScheme `json:"iso8601,omitempty"`     // The ISO 8601 format of the build date.
+	Jira        string         `json:"jira,omitempty"`        // The Jira format of the build date.
+	Friendly    string         `json:"friendly,omitempty"`    // The friendly format of the build date.
+	EpochMillis int64          `json:"epochMillis,omitempty"` // The epoch milliseconds of the build date.
 }
 
 // InfoLinkScheme represents a link related to a system.
diff --git a/pkg/infra/models/sm_request.go b/pkg/infra/models/sm_request.go
index 32adcb9..60c5de9 100644
--- a/pkg/infra/models/sm_request.go
+++ b/pkg/infra/models/sm_request.go
@@ -75,10 +75,10 @@ type CustomerRequestServiceDeskScheme struct {
 
 // CustomerRequestDateScheme represents a date for a customer request.
 type CustomerRequestDateScheme struct {
-	ISO8601     string `json:"iso8601,omitempty"`     // The ISO 8601 format of the date.
-	Jira        string `json:"jira,omitempty"`        // The Jira format of the date.
-	Friendly    string `json:"friendly,omitempty"`    // The friendly format of the date.
-	EpochMillis int    `json:"epochMillis,omitempty"` // The epoch milliseconds of the date.
+	ISO8601     DateTimeScheme `json:"iso8601,omitempty"`     // The ISO 8601 format of the date.
+	Jira        string         `json:"jira,omitempty"`        // The Jira format of the date.
+	Friendly    string         `json:"friendly,omitempty"`    // The friendly format of the date.
+	EpochMillis int            `json:"epochMillis,omitempty"` // The epoch milliseconds of the date.
 }
 
 // CustomerRequestReporterScheme represents a reporter for a customer request.
@@ -108,10 +108,10 @@ type CustomerRequestCurrentStatusScheme struct {
 
 // CustomerRequestCurrentStatusDateScheme represents a date for a customer request current status.
 type CustomerRequestCurrentStatusDateScheme struct {
-	ISO8601     string `json:"iso8601,omitempty"`     // The ISO 8601 format of the date.
-	Jira        string `json:"jira,omitempty"`        // The Jira format of the date.
-	Friendly    string `json:"friendly,omitempty"`    // The friendly format of the date.
-	EpochMillis int    `json:"epochMillis,omitempty"` // The epoch milliseconds of the date.
+	ISO8601     DateTimeScheme `json:"iso8601,omitempty"`     // The ISO 8601 format of the date.
+	Jira        string         `json:"jira,omitempty"`        // The Jira format of the date.
+	Friendly    string         `json:"friendly,omitempty"`    // The friendly format of the date.
+	EpochMillis int            `json:"epochMillis,omitempty"` // The epoch milliseconds of the date.
 }
 
 // CustomerRequestLinksScheme represents the links related to a customer request.


Test patch:
diff --git a/pkg/infra/models/jira_issue_custom_fields_test.go b/pkg/infra/models/jira_issue_custom_fields_test.go
index a555b26..55250e3 100644
--- a/pkg/infra/models/jira_issue_custom_fields_test.go
+++ b/pkg/infra/models/jira_issue_custom_fields_test.go
@@ -2098,7 +2098,6 @@ func TestParseRequestTypeCustomField(t *testing.T) {
         "status": "Check billing account",
         "statusCategory": "NEW",
         "statusDate": {
-          "iso8601": "2024-06-28T15:33:57+0900",
           "jira": "2024-06-28T08:33:57.313+0200",
           "friendly": "Friday 15:33",
           "epochMillis": 1719556437313
@@ -2205,7 +2204,6 @@ func TestParseRequestTypeCustomField(t *testing.T) {
 					Status:         "Check billing account",
 					StatusCategory: "NEW",
 					StatusDate: &CustomerRequestCurrentStatusDateScheme{
-						ISO8601:     "2024-06-28T15:33:57+0900",
 						Jira:        "2024-06-28T08:33:57.313+0200",
 						Friendly:    "Friday 15:33",
 						EpochMillis: 1719556437313,
@@ -6331,7 +6329,6 @@ func TestParseRequestTypeCustomFields(t *testing.T) {
             "status": "Check billing account",
             "statusCategory": "NEW",
             "statusDate": {
-              "iso8601": "2024-06-28T15:33:57+0900",
               "jira": "2024-06-28T08:33:57.313+0200",
               "friendly": "Friday 15:33",
               "epochMillis": 1719556437313
@@ -6384,7 +6381,6 @@ func TestParseRequestTypeCustomFields(t *testing.T) {
             "status": "Check billing account",
             "statusCategory": "NEW",
             "statusDate": {
-              "iso8601": "2024-06-28T15:33:57+0900",
               "jira": "2024-06-28T08:33:57.313+0200",
               "friendly": "Friday 15:33",
               "epochMillis": 1719556437313
@@ -6523,7 +6519,6 @@ func TestParseRequestTypeCustomFields(t *testing.T) {
 						Status:         "Check billing account",
 						StatusCategory: "NEW",
 						StatusDate: &CustomerRequestCurrentStatusDateScheme{
-							ISO8601:     "2024-06-28T15:33:57+0900",
 							Jira:        "2024-06-28T08:33:57.313+0200",
 							Friendly:    "Friday 15:33",
 							EpochMillis: 1719556437313,
@@ -6550,7 +6545,6 @@ func TestParseRequestTypeCustomFields(t *testing.T) {
 						Status:         "Check billing account",
 						StatusCategory: "NEW",
 						StatusDate: &CustomerRequestCurrentStatusDateScheme{
-							ISO8601:     "2024-06-28T15:33:57+0900",
 							Jira:        "2024-06-28T08:33:57.313+0200",
 							Friendly:    "Friday 15:33",
 							EpochMillis: 1719556437313,
diff --git a/pkg/infra/models/jira_time_test.go b/pkg/infra/models/jira_time_test.go
new file mode 100644
index 0000000..cb9be33
--- /dev/null
+++ b/pkg/infra/models/jira_time_test.go
@@ -0,0 +1,159 @@
+package models
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDateScheme_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name    string
+		t       DateScheme
+		want    []byte
+		wantErr assert.ErrorAssertionFunc
+	}{
+		{
+			name:    "random time",
+			t:       DateScheme(time.Date(2021, 6, 22, 7, 34, 41, 123, time.UTC)),
+			want:    []byte(`2021-06-22`),
+			wantErr: assert.NoError,
+		},
+		{
+			name:    "timezone",
+			t:       DateScheme(time.Date(2021, 6, 22, 7, 34, 41, 123, time.FixedZone("UTC+2", 7200))),
+			want:    []byte(`2021-06-22`),
+			wantErr: assert.NoError,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.t.MarshalJSON()
+			if !tt.wantErr(t, err, "MarshalJSON()") {
+				return
+			}
+			assert.Equalf(t, string(tt.want), string(got), "MarshalJSON()")
+		})
+	}
+}
+
+func TestDateScheme_UnmarshalJSON(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    DateScheme
+		wantErr assert.ErrorAssertionFunc
+	}{
+		{
+			name: "null",
+			args: args{
+				data: []byte(`null`),
+			},
+			want:    DateScheme{},
+			wantErr: assert.NoError,
+		},
+		{
+			name: "random date",
+			args: args{
+				data: []byte(`"2021-06-22"`),
+			},
+			want:    DateScheme(time.Date(2021, 6, 22, 0, 0, 0, 0, time.UTC)),
+			wantErr: assert.NoError,
+		},
+		{
+			name: "invalid date",
+			args: args{
+				data: []byte(`"asdf"`),
+			},
+			want:    DateScheme{},
+			wantErr: assert.Error,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := DateScheme{}
+			tt.wantErr(t, d.UnmarshalJSON(tt.args.data), fmt.Sprintf("UnmarshalJSON(%v)", tt.args.data))
+			assert.Equalf(t, tt.want, d, "MarshalJSON()")
+		})
+	}
+}
+
+func TestDateTimeScheme_MarshalJSON(t *testing.T) {
+	tests := []struct {
+		name    string
+		t       DateTimeScheme
+		want    []byte
+		wantErr assert.ErrorAssertionFunc
+	}{
+		{
+			name:    "random time",
+			t:       DateTimeScheme(time.Date(2021, 6, 22, 7, 34, 41, 123, time.FixedZone("UTC-9", -32400))),
+			want:    []byte(`2021-06-22T07:34:41-0900`),
+			wantErr: assert.NoError,
+		},
+		{
+			name:    "timezone",
+			t:       DateTimeScheme(time.Date(2021, 6, 22, 7, 34, 41, 123, time.FixedZone("UTC+2", 7200))),
+			want:    []byte(`2021-06-22T07:34:41+0200`),
+			wantErr: assert.NoError,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.t.MarshalJSON()
+			if !tt.wantErr(t, err, "MarshalJSON()") {
+				return
+			}
+			assert.Equalf(t, string(tt.want), string(got), "MarshalJSON()")
+		})
+	}
+}
+
+func TestDateTimeScheme_UnmarshalJSON(t *testing.T) {
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    DateTimeScheme
+		wantErr assert.ErrorAssertionFunc
+	}{
+		{
+			name: "null",
+			args: args{
+				data: []byte(`null`),
+			},
+			want:    DateTimeScheme{},
+			wantErr: assert.NoError,
+		},
+		{
+			name: "random date",
+			args: args{
+				data: []byte(`"2021-06-22T07:34:41+0200"`),
+			},
+			want:    DateTimeScheme(time.Date(2021, 6, 22, 7, 34, 41, 0, time.FixedZone("", 7200))),
+			wantErr: assert.NoError,
+		},
+		{
+			name: "invalid date",
+			args: args{
+				data: []byte(`"asdf"`),
+			},
+			want:    DateTimeScheme{},
+			wantErr: assert.Error,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := DateTimeScheme{}
+			tt.wantErr(t, d.UnmarshalJSON(tt.args.data), fmt.Sprintf("UnmarshalJSON(%v)", tt.args.data))
+			assert.Equalf(t, tt.want, d, "MarshalJSON()")
+		})
+	}
+}