-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- 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
1 parent
f2cb9bd
commit 78b0f25
Showing
4 changed files
with
360 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package notionapi |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |