Skip to content
Merged
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
397 changes: 194 additions & 203 deletions e2e/events_repeating_test.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/google/uuid v1.6.0
github.com/jedisct1/go-aes-siv v1.0.0
github.com/rdleal/intervalst v1.5.0
github.com/teambition/rrule-go v1.8.2
golang.org/x/crypto v0.53.0
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
Expand Down
121 changes: 69 additions & 52 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/git-calendar/core/pkg/core"
"github.com/google/uuid"
"github.com/teambition/rrule-go"
)

const (
Expand All @@ -27,6 +28,13 @@ type Api struct {
inner *core.Core
}

// eventJSON is the API representation of an event. Recurrence is transported
// as an RFC 5545 string because rrule.Set is not JSON-serializable.
type eventJSON struct {
core.Event
Repeat *string `json:"repeat"`
}

// A "constructor" for the JSON API.
func NewApi() *Api {
return &Api{
Expand Down Expand Up @@ -87,47 +95,34 @@ func (a *Api) UpdateEvent(eventJson string) (string, error) {
}

func (a *Api) UpdateRepeatingEvent(oldEventJson, newEventJson string, strategy int) (string, error) {
var oldEvent core.Event
var newEvent core.Event

if err := json.Unmarshal([]byte(oldEventJson), &oldEvent); err != nil {
fmt.Printf("CalendarCore got:\nNew: %s\nOld: %s\n", oldEventJson, newEventJson)
return emptyJson, fmt.Errorf("failed to unmarshal event data: %w", err)
}

if err := json.Unmarshal([]byte(newEventJson), &newEvent); err != nil {
fmt.Printf("CalendarCore got:\nNew: %s\nOld: %s\n", oldEventJson, newEventJson)
return emptyJson, fmt.Errorf("failed to unmarshal event data: %w", err)
oldEvent, err := unmarshalEvent(oldEventJson)
if err != nil {
return emptyJson, err
}

updatedEvent, err := a.inner.UpdateRepeatingEvent(oldEvent, newEvent, core.UpdateStrategy(strategy))
newEvent, err := unmarshalEvent(newEventJson)
if err != nil {
fmt.Printf("CalendarCore got:\nNew: %s\nOld: %s\n", oldEventJson, newEventJson)
return emptyJson, err
}

jsonBytes, err := json.Marshal(updatedEvent)
updated, err := a.inner.UpdateRepeatingEvent(oldEvent, newEvent, core.UpdateStrategy(strategy))
if err != nil {
return emptyJson, err
}

return string(jsonBytes), err
return marshalEvent(updated)
}

func (a *Api) RemoveEvent(eventJson string) error {
var event core.Event
err := json.Unmarshal([]byte(eventJson), &event)
event, err := unmarshalEvent(eventJson)
if err != nil {
return fmt.Errorf("failed to unmarshal event data: %w", err)
return err
}
return a.inner.RemoveEvent(event)
}

func (a *Api) RemoveRepeatingEvent(eventJson string, strategy int) error {
var event core.Event
err := json.Unmarshal([]byte(eventJson), &event)
event, err := unmarshalEvent(eventJson)
if err != nil {
return fmt.Errorf("failed to unmarshal event data: %w", err)
return err
}
return a.inner.RemoveRepeatingEvent(event, core.UpdateStrategy(strategy))
}
Expand All @@ -143,13 +138,7 @@ func (a *Api) GetEvent(id string) (string, error) {
return emptyJson, err
}

// marshal to json
jsonBytes, err := json.Marshal(event)
if err != nil {
return emptyJson, fmt.Errorf("failed to marshal event to json: %w", err)
}

return string(jsonBytes), nil
return marshalEvent(event)
}

func (a *Api) GetEvents(from, to string, filterJson string) (string, error) {
Expand All @@ -170,13 +159,7 @@ func (a *Api) GetEvents(from, to string, filterJson string) (string, error) {
// pass the args to inner api
events := a.inner.GetEvents(f, t, filter)

// marshal to json
jsonBytes, err := json.Marshal(events)
if err != nil {
return emptyJsonArr, fmt.Errorf("failed to marshal events to json: %w", err)
}

return string(jsonBytes), nil
return marshalEvents(events)
}

func (a *Api) CreateTag(calendar, tagJson string) (string, error) {
Expand Down Expand Up @@ -234,29 +217,63 @@ func (a *Api) RemoveTag(calendar, id string) error {

// ------------------------------------------------ Helpers -------------------------------------------------

// A helper which:
// 1. Parses and validates input event
// 2. Calls the coreFunc
// 3. Marshals event that came back to JSON
// 4. Returns json
func returnJsonEventAndError(eventJson string, coreFunc func(core.Event) (*core.Event, error)) (string, error) {
var event core.Event
err := json.Unmarshal([]byte(eventJson), &event)
event, err := unmarshalEvent(eventJson)
if err != nil {
return emptyJson, err
}
updated, err := coreFunc(event)
if err != nil {
fmt.Println("CalendarCore got: ", eventJson)
return emptyJson, fmt.Errorf("failed to unmarshal event data: %w", err)
return emptyJson, err
}
return marshalEvent(updated)
}

// unmarshalEvent decodes API JSON and rebuilds the internal recurrence set.
func unmarshalEvent(raw string) (core.Event, error) {
var data eventJSON
if err := json.Unmarshal([]byte(raw), &data); err != nil {
return core.Event{}, fmt.Errorf("failed to unmarshal event data: %w", err)
}

var repeat *rrule.Set
if data.Repeat != nil && *data.Repeat != "" {
var err error
repeat, err = rrule.StrToRRuleSet(*data.Repeat)
if err != nil {
return core.Event{}, fmt.Errorf("invalid recurrence: %w", err)
}
}
data.Event.Repeat = repeat
return data.Event, nil
}

newEvent, err := coreFunc(event)
// marshalEvent converts the recurrence set to its string form and encodes the event.
func marshalEvent(event *core.Event) (string, error) {
data, err := json.Marshal(eventToJSON(*event))
if err != nil {
fmt.Println("CalendarCore got: ", eventJson)
return emptyJson, err
return emptyJson, fmt.Errorf("failed to marshal event data: %w", err)
}
return string(data), nil
}

jsonBytes, err := json.Marshal(newEvent)
func marshalEvents(events []core.Event) (string, error) {
result := make([]eventJSON, len(events))
for i := range events {
result[i] = eventToJSON(events[i])
}
data, err := json.Marshal(result)
if err != nil {
return emptyJson, err
return emptyJsonArr, fmt.Errorf("failed to marshal event data: %w", err)
}
return string(data), nil
}

return string(jsonBytes), err
func eventToJSON(event core.Event) eventJSON {
var repeat *string
if event.Repeat != nil {
value := event.Repeat.String()
repeat = &value
}
return eventJSON{Event: event, Repeat: repeat}
}
10 changes: 1 addition & 9 deletions pkg/api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,7 @@ type Event struct {
Calendar string
TagId string
ParentId string
Repeat *Repetition
}

type Repetition struct {
Frequency int
Interval int
Until string
Count int
Exceptions []string
Repeat string // DTSTART/RRULE/EXDATE in RFC 5545 format.
}

// A DTO for Kotlin/Swift to use as the calendar structure.
Expand Down
26 changes: 3 additions & 23 deletions pkg/core/constants.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,17 @@
package core

const (
// IndexFileName string = "index.json"
// RichIndexFileName string = "index-rich.json"

EventsDirName string = "events"
TagsDirName = "tags"
TagsDirName string = "tags"

GitAuthorName string = "git-calendar"
GitRemoteName string = "origin"
GitBranchName string = "main"
)

// ------- Repeating frequency -------

// Repeating frequency.
type Freq int

const (
Invalid Freq = iota // ints default value 0 is invalid
Day // Repeat daily.
Week // Repeat weekly.
Month // Repeat monthly.
Year // Repeat yearly.
_max // boundary for validation
// IndexFileName string = "index.json"
// RichIndexFileName string = "index-rich.json"
)

func (t Freq) IsValid() bool {
return t > Invalid && t <= _max
}

// ------- Repeating update strategy -------

type UpdateStrategy int

const (
Expand Down
Loading