Skip to content

Commit

Permalink
add iCalender export
Browse files Browse the repository at this point in the history
  • Loading branch information
orltom committed Jan 7, 2025
1 parent 054f8a9 commit ba8cda6
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 1 deletion.
6 changes: 5 additions & 1 deletion cmd/create_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
CVS Format = iota
Table
JSON
ICS
)

func RunCreateShiftPlan(writer io.Writer, arguments []string) error {
Expand All @@ -35,6 +36,9 @@ func RunCreateShiftPlan(writer io.Writer, arguments []string) error {
JSON: func() apis.Exporter {
return export.NewJSONExporter()
},
ICS: func() apis.Exporter {
return export.NewICSExporter()
},
}

start := new(time.Time)
Expand All @@ -52,7 +56,7 @@ func RunCreateShiftPlan(writer io.Writer, arguments []string) error {
createCommand.Func("start", "(required) start time of the schedule plan", cli.TimeValueVar(start))
createCommand.Func("end", "(required) end time of the schedule plan", cli.TimeValueVar(end))
createCommand.Func("team-file", "(required) path to the file that contain all on-call duties", cli.FilePathVar(teamFilePath))
createCommand.Func("output", "output format. One of (cvs, table, json)", cli.EnumValueVar(enums, &outputFormat))
createCommand.Func("output", "output format. One of (cvs, table, json, ics)", cli.EnumValueVar(enums, &outputFormat))
createCommand.StringVar(primaryRules, "primary-rules", "vacation", "Rule to decide which employee should be on-call for the next shift")
createCommand.StringVar(secondaryRules, "secondary-rules", "vacation", "Rule to decide which employee should be on-call for the next shift")
createCommand.Usage = func() {
Expand Down
76 changes: 76 additions & 0 deletions internal/export/ics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package export

import (
_ "embed"
"errors"
"fmt"
"io"
"text/template"

"github.com/orltom/on-call-schedule/pkg/apis"
)

//go:embed ics.tmpl
var templateICSFile string

var _ apis.Exporter = &TableExporter{}

type event struct {
apis.Shift
UID string
Category string
XColor string
Attendee string
Summary string
Description string
}

type ICSExporter struct {
template *template.Template
}

func NewICSExporter() *ICSExporter {
tmpl, _ := template.New("ical").Parse(templateICSFile)
return &ICSExporter{template: tmpl}
}

func (e *ICSExporter) Write(plan []apis.Shift, writer io.Writer) error {
if plan == nil {
return errors.New("shifts must not be nil")
}

events := make([]event, 0, len(plan))
for idx := range plan {
s := plan[idx]
events = append(events, mapPrimaryEvent(s), mapSecondaryEvent(s))
}

if err := e.template.ExecuteTemplate(writer, "icalCalendar", events); err != nil {
return fmt.Errorf("can not generate ICS: %w", err)
}
return nil
}

func mapPrimaryEvent(shift apis.Shift) event {
return event{
Shift: shift,
UID: "primary-" + shift.Start.Format("20060102"),
Category: "Primary",
XColor: "#FF5733",
Attendee: string(shift.Primary),
Summary: "Primary On-Call: " + string(shift.Primary),
Description: fmt.Sprintf("Primary: %s\\nSecondary: %s", shift.Primary, shift.Secondary),
}
}

func mapSecondaryEvent(shift apis.Shift) event {
return event{
Shift: shift,
UID: "secondary-" + shift.Start.Format("20060102"),
Category: "Secondary",
XColor: "#33FF57",
Attendee: string(shift.Secondary),
Summary: "Secondary On-Call: " + string(shift.Secondary),
Description: fmt.Sprintf("Primary: %s\\nSecondary: %s", shift.Primary, shift.Secondary),
}
}
29 changes: 29 additions & 0 deletions internal/export/ics.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{{- define "icalEvent" }}
BEGIN:VEVENT
UID:{{ .UID }}
DTSTART:{{ .Start.Format "20060102T150405Z" }}
DTEND:{{ .End.Format "20060102T150405Z" }}
SUMMARY:{{ .Summary }}
DESCRIPTION:Primary: {{ .Primary }}\nSecondary: {{ .Secondary }}
CATEGORIES:{{ .Category }}
ATTENDEE:mailto:{{ .Attendee }}
X-COLOR:{{ .XColor }}
BEGIN:VALARM
TRIGGER:-PT8H
ACTION:DISPLAY
DESCRIPTION:Reminder: {{ .Summary }} starts in 8 hours.
END:VALARM
END:VEVENT
{{ end -}}

{{- define "icalCalendar" -}}
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ocsctl//on-call-shift//EN
CALSCALE:GREGORIAN
{{ range . }}
{{- template "icalEvent" . }}
{{- end }}
END:VCALENDAR

{{- end -}}
95 changes: 95 additions & 0 deletions internal/export/ics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package export

import (
"bytes"
"testing"

"github.com/orltom/on-call-schedule/pkg/apis"
)

func TestICSExporter_Write(t *testing.T) {
type args struct {
plan []apis.Shift
}
tests := []struct {
name string
args args
wantWriter string
wantErr bool
}{
{
name: "When plan is nil, throw an error",
args: args{
plan: nil,
},
wantWriter: "",
wantErr: true,
},
{
name: "Print ICS result according shift plan",
args: args{
plan: []apis.Shift{
{
Start: date("2024-12-25"),
End: date("2024-12-26"),
Primary: "a",
Secondary: "b",
},
},
},
wantWriter: `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ocsctl//on-call-shift//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
UID:primary-20241225
DTSTART:20241225T000000Z
DTEND:20241226T000000Z
SUMMARY:Primary On-Call: a
DESCRIPTION:Primary: a\nSecondary: b
CATEGORIES:Primary
ATTENDEE:mailto:a
X-COLOR:#FF5733
BEGIN:VALARM
TRIGGER:-PT8H
ACTION:DISPLAY
DESCRIPTION:Reminder: Primary On-Call: a starts in 8 hours.
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:secondary-20241225
DTSTART:20241225T000000Z
DTEND:20241226T000000Z
SUMMARY:Secondary On-Call: b
DESCRIPTION:Primary: a\nSecondary: b
CATEGORIES:Secondary
ATTENDEE:mailto:b
X-COLOR:#33FF57
BEGIN:VALARM
TRIGGER:-PT8H
ACTION:DISPLAY
DESCRIPTION:Reminder: Secondary On-Call: b starts in 8 hours.
END:VALARM
END:VEVENT
END:VCALENDAR`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := NewICSExporter()
writer := &bytes.Buffer{}
err := e.Write(tt.args.plan, writer)
if (err != nil) != tt.wantErr {
t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotWriter := writer.String(); gotWriter != tt.wantWriter {
t.Errorf("Write() gotWriter = %v, want %v", gotWriter, tt.wantWriter)
}
})
}
}

0 comments on commit ba8cda6

Please sign in to comment.