Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add date format option with tests #188

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions date_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package notionapi
71 changes: 71 additions & 0 deletions pkg/models/date.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package models

import (
"encoding/json"
"time"
)

type Date struct {
time.Time
DateOnly bool
}

// Format returns the date in the specified format based on DateOnly flag
func (d Date) Format() string {
if d.DateOnly {
return d.Time.Format("2006-01-02")
}
return d.Time.Format(time.RFC3339)
}

// FormatForNotion returns the date in Notion's expected format
func (d Date) FormatForNotion() string {
if d.DateOnly {
return d.Time.Format("2006-01-02")
}
// Notion expects RFC3339 format without explicit timezone
return d.Time.UTC().Format("2006-01-02T15:04:05Z")
}

// MarshalJSON adds error handling and validation
func (d Date) MarshalJSON() ([]byte, error) {
if d.Time.IsZero() {
return []byte("null"), nil
}
return json.Marshal(d.FormatForNotion())
}

// UnmarshalJSON adds better error handling
func (d *Date) UnmarshalJSON(data []byte) error {
// Handle null value
if string(data) == "null" {
d.Time = time.Time{}
d.DateOnly = false
return nil
}

var rawValue string
if err := json.Unmarshal(data, &rawValue); err != nil {
return err
}

// Try both formats
formats := []string{
"2006-01-02", // Date only
"2006-01-02T15:04:05Z", // Full datetime
time.RFC3339, // Fallback
}

var lastErr error
for _, format := range formats {
if t, err := time.Parse(format, rawValue); err == nil {
d.Time = t
d.DateOnly = format == "2006-01-02"
return nil
} else {
lastErr = err
}
}

return lastErr
}
46 changes: 46 additions & 0 deletions pkg/models/date_object.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package models

import "fmt"

type DateObject struct {
Start *Date `json:"start"`
End *Date `json:"end"`
DateOnly bool `json:"date_only,omitempty"`
}

// NewDateObject creates a new DateObject with validation
func NewDateObject(start, end *Date, dateOnly bool) (*DateObject, error) {
// Validation: end date should not be before start date
if start != nil && end != nil && end.Time.Before(start.Time) {
return nil, fmt.Errorf("end date cannot be before start date")
}

if start != nil {
start.DateOnly = dateOnly
}
if end != nil {
end.DateOnly = dateOnly
}

return &DateObject{
Start: start,
End: end,
DateOnly: dateOnly,
}, nil
}

// FormatForNotion formats both dates with error handling
func (do DateObject) FormatForNotion() (map[string]interface{}, error) {
result := make(map[string]interface{})

if do.Start == nil {
return nil, fmt.Errorf("start date is required")
}

result["start"] = do.Start.FormatForNotion()
if do.End != nil {
result["end"] = do.End.FormatForNotion()
}

return result, nil
}
242 changes: 242 additions & 0 deletions pkg/models/date_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package models

import (
"encoding/json"
"testing"
"time"
)

func TestDate_MarshalJSON(t *testing.T) {
tests := []struct {
name string
date Date
want string
wantErr bool
}{
{
name: "date only format",
date: Date{
Time: time.Date(2024, 3, 14, 0, 0, 0, 0, time.UTC),
DateOnly: true,
},
want: `"2024-03-14"`,
wantErr: false,
},
{
name: "datetime format",
date: Date{
Time: time.Date(2024, 3, 14, 15, 30, 0, 0, time.UTC),
DateOnly: false,
},
want: `"2024-03-14T15:30:00Z"`,
wantErr: false,
},
{
name: "zero time",
date: Date{
Time: time.Time{},
DateOnly: false,
},
want: "null",
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := json.Marshal(tt.date)
if (err != nil) != tt.wantErr {
t.Errorf("Date.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if string(got) != tt.want {
t.Errorf("Date.MarshalJSON() = %v, want %v", string(got), tt.want)
}
})
}
}

func TestDate_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
json string
want Date
wantErr bool
}{
{
name: "date only format",
json: `"2024-03-14"`,
want: Date{
Time: time.Date(2024, 3, 14, 0, 0, 0, 0, time.UTC),
DateOnly: true,
},
wantErr: false,
},
{
name: "datetime format",
json: `"2024-03-14T15:30:00Z"`,
want: Date{
Time: time.Date(2024, 3, 14, 15, 30, 0, 0, time.UTC),
DateOnly: false,
},
wantErr: false,
},
{
name: "invalid format",
json: `"invalid-date"`,
wantErr: true,
},
{
name: "null value",
json: "null",
want: Date{
Time: time.Time{},
DateOnly: false,
},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got Date
err := json.Unmarshal([]byte(tt.json), &got)
if (err != nil) != tt.wantErr {
t.Errorf("Date.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if !got.Time.Equal(tt.want.Time) {
t.Errorf("Date.UnmarshalJSON() Time = %v, want %v", got.Time, tt.want.Time)
}
if got.DateOnly != tt.want.DateOnly {
t.Errorf("Date.UnmarshalJSON() DateOnly = %v, want %v", got.DateOnly, tt.want.DateOnly)
}
}
})
}
}

func TestDateObject_FormatForNotion(t *testing.T) {
tests := []struct {
name string
obj DateObject
want map[string]interface{}
wantErr bool
}{
{
name: "valid date range",
obj: DateObject{
Start: &Date{
Time: time.Date(2024, 3, 14, 0, 0, 0, 0, time.UTC),
DateOnly: true,
},
End: &Date{
Time: time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC),
DateOnly: true,
},
DateOnly: true,
},
want: map[string]interface{}{
"start": "2024-03-14",
"end": "2024-03-15",
},
wantErr: false,
},
{
name: "start date only",
obj: DateObject{
Start: &Date{
Time: time.Date(2024, 3, 14, 15, 30, 0, 0, time.UTC),
DateOnly: false,
},
DateOnly: false,
},
want: map[string]interface{}{
"start": "2024-03-14T15:30:00Z",
},
wantErr: false,
},
{
name: "missing start date",
obj: DateObject{
End: &Date{
Time: time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC),
DateOnly: true,
},
DateOnly: true,
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.obj.FormatForNotion()
if (err != nil) != tt.wantErr {
t.Errorf("DateObject.FormatForNotion() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
for k, v := range tt.want {
if got[k] != v {
t.Errorf("DateObject.FormatForNotion() = %v, want %v", got[k], v)
}
}
}
})
}
}

func TestNewDateObject(t *testing.T) {
tests := []struct {
name string
start *Date
end *Date
dateOnly bool
wantErr bool
}{
{
name: "valid date range",
start: &Date{
Time: time.Date(2024, 3, 14, 0, 0, 0, 0, time.UTC),
DateOnly: true,
},
end: &Date{
Time: time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC),
DateOnly: true,
},
dateOnly: true,
wantErr: false,
},
{
name: "end before start",
start: &Date{
Time: time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC),
DateOnly: true,
},
end: &Date{
Time: time.Date(2024, 3, 14, 0, 0, 0, 0, time.UTC),
DateOnly: true,
},
dateOnly: true,
wantErr: true,
},
{
name: "start date only",
start: &Date{Time: time.Now()},
end: nil,
dateOnly: false,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewDateObject(tt.start, tt.end, tt.dateOnly)
if (err != nil) != tt.wantErr {
t.Errorf("NewDateObject() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Loading