diff --git a/date_test.go b/date_test.go new file mode 100644 index 0000000..61628c2 --- /dev/null +++ b/date_test.go @@ -0,0 +1 @@ +package notionapi diff --git a/pkg/models/date.go b/pkg/models/date.go new file mode 100644 index 0000000..1c2bd44 --- /dev/null +++ b/pkg/models/date.go @@ -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 +} diff --git a/pkg/models/date_object.go b/pkg/models/date_object.go new file mode 100644 index 0000000..3ba4902 --- /dev/null +++ b/pkg/models/date_object.go @@ -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 +} diff --git a/pkg/models/date_test.go b/pkg/models/date_test.go new file mode 100644 index 0000000..dca4e8c --- /dev/null +++ b/pkg/models/date_test.go @@ -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) + } + }) + } +}