1
0
mirror of https://github.com/dstotijn/go-notion.git synced 2025-06-17 00:07:45 +02:00

Add custom type for Notion date properties with optional time

This commit is contained in:
David Stotijn
2021-05-23 14:02:17 +02:00
parent 00a77fe3a4
commit 7e83ec7aec
4 changed files with 213 additions and 5 deletions

View File

@ -517,7 +517,7 @@ func TestQueryDatabase(t *testing.T) {
ID: "Q]uT",
Type: notion.DBPropTypeDate,
Date: &notion.Date{
Start: mustParseTime(time.RFC3339Nano, "2021-05-18T12:49:00.000-05:00"),
Start: mustParseDateTime("2021-05-18T12:49:00.000-05:00"),
},
},
"Name": notion.DatabasePageProperty{

View File

@ -1,7 +1,5 @@
package notion
import "time"
type RichText struct {
Type RichTextType `json:"type,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"`
@ -36,8 +34,8 @@ type Mention struct {
}
type Date struct {
Start time.Time `json:"start"`
End *time.Time `json:"end,omitempty"`
Start DateTime `json:"start"`
End *DateTime `json:"end,omitempty"`
}
type Text struct {

95
time.go Normal file
View File

@ -0,0 +1,95 @@
package notion
import (
"encoding/json"
"errors"
"time"
)
// Length of a date string, e.g. `2006-01-02`.
const dateLength = 10
// DateTimeFormat is used when calling time.Parse, using RFC3339 with microsecond
// precision, which is what the Notion API returns in JSON response data.
const DateTimeFormat = "2006-01-02T15:04:05.999Z07:00"
// DateTime represents a Notion date property with optional time.
type DateTime struct {
time.Time
hasTime bool
}
// ParseDateTime parses an RFC3339 formatted string with optional time.
func ParseDateTime(value string) (DateTime, error) {
if len(value) > len(DateTimeFormat) {
return DateTime{}, errors.New("invalid datetime string")
}
t, err := time.Parse(DateTimeFormat[:len(value)], value)
if err != nil {
return DateTime{}, err
}
dt := DateTime{
Time: t,
hasTime: len(value) > dateLength,
}
return dt, nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (dt *DateTime) UnmarshalJSON(b []byte) error {
if len(b) < 2 {
return errors.New("invalid datetime string")
}
parsed, err := ParseDateTime(string(b[1 : len(b)-1]))
if err != nil {
return err
}
*dt = parsed
return nil
}
// MarshalJSON implements json.Marshaler. It returns an RFC399 formatted string,
// using microsecond precision ()
func (dt DateTime) MarshalJSON() ([]byte, error) {
if dt.hasTime {
return json.Marshal(dt.Time)
}
return []byte(`"` + dt.Time.Format(DateTimeFormat[:dateLength]) + `"`), nil
}
// NewDateTime returns a new DateTime. If `haseTime` is true, time is included
// when encoding to JSON.
func NewDateTime(t time.Time, hasTime bool) DateTime {
var tt time.Time
if hasTime {
tt = t
} else {
tt = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
}
return DateTime{
Time: tt,
hasTime: hasTime,
}
}
// HasTime returns true if the datetime was parsed from a string that included time.
func (dt *DateTime) HasTime() bool {
return dt.hasTime
}
// Equal returns true if both DateTime values have equal underlying time.Time and
// hasTime fields.
func (dt DateTime) Equal(value DateTime) bool {
if !dt.Time.Equal(value.Time) {
return false
}
return dt.hasTime == value.hasTime
}

115
time_test.go Normal file
View File

@ -0,0 +1,115 @@
package notion_test
import (
"encoding/json"
"testing"
"time"
"github.com/dstotijn/go-notion"
"github.com/google/go-cmp/cmp"
)
func mustParseDateTime(value string) notion.DateTime {
dt, err := notion.ParseDateTime(value)
if err != nil {
panic(err)
}
return dt
}
func TestTimeMarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dateTime notion.DateTime
expJSON []byte
}{
{
name: "date and time",
dateTime: mustParseDateTime("2021-05-23T09:11:50.123Z"),
expJSON: []byte(`"2021-05-23T09:11:50.123Z"`),
},
{
name: "date without time",
dateTime: mustParseDateTime("2021-05-23"),
expJSON: []byte(`"2021-05-23"`),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
dtJSON, err := json.Marshal(tt.dateTime)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if diff := cmp.Diff(string(tt.expJSON), string(dtJSON)); diff != "" {
t.Fatalf("encoded JSON not equal (-exp, +got):\n%v", diff)
}
})
}
}
func TestTimeUnmarshalJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
timeString string
expDateTime notion.DateTime
expHasTime bool
expError error
}{
{
name: "date and time",
timeString: "2021-05-23T09:11:50.123+00:00",
expDateTime: notion.NewDateTime(mustParseTime(time.RFC3339Nano, "2021-05-23T09:11:50.123Z"), true),
expHasTime: true,
expError: nil,
},
{
name: "date without time",
timeString: "2021-05-23",
expDateTime: notion.NewDateTime(mustParseTime(time.RFC3339Nano, "2021-05-23T09:11:50.123Z"), false),
expHasTime: false,
expError: nil,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
type testDateTime struct {
DateTime notion.DateTime `json:"time"`
}
var dt testDateTime
err := json.Unmarshal([]byte(`{"time":"`+tt.timeString+`"}`), &dt)
if tt.expError == nil && err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tt.expError != nil && err == nil {
t.Fatalf("error not equal (expected: %v, got: nil)", tt.expError)
}
if tt.expError != nil && err != nil && tt.expError.Error() != err.Error() {
t.Fatalf("error not equal (expected: %v, got: %v)", tt.expError, err)
}
if diff := cmp.Diff(tt.expDateTime.Time, dt.DateTime.Time); diff != "" {
t.Fatalf("time not equal (-exp, +got):\n%v", diff)
}
if tt.expHasTime != dt.DateTime.HasTime() {
t.Fatalf("has time not equal (expected: %v, got: %v)", tt.expHasTime, dt.DateTime.HasTime())
}
})
}
}