Skip to content

Commit

Permalink
Add date format option with tests
Browse files Browse the repository at this point in the history
- Add Date struct with DateOnly flag
- Implement custom date formatting for Notion API
- Add comprehensive test coverage
- Fix timezone formatting in FormatForNotion
- Handle null values in UnmarshalJSON
- Closes #177
  • Loading branch information
aydinomer00 committed Dec 30, 2024
1 parent f2cb9bd commit 78b0f25
Show file tree
Hide file tree
Showing 4 changed files with 360 additions and 0 deletions.
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)
}
})
}
}

0 comments on commit 78b0f25

Please sign in to comment.