Skip to content

Commit 2f6e6ca

Browse files
committed
add iCalender export
1 parent 054f8a9 commit 2f6e6ca

File tree

4 files changed

+205
-1
lines changed

4 files changed

+205
-1
lines changed

cmd/create_command.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const (
2121
CVS Format = iota
2222
Table
2323
JSON
24+
ICS
2425
)
2526

2627
func RunCreateShiftPlan(writer io.Writer, arguments []string) error {
@@ -35,6 +36,9 @@ func RunCreateShiftPlan(writer io.Writer, arguments []string) error {
3536
JSON: func() apis.Exporter {
3637
return export.NewJSONExporter()
3738
},
39+
ICS: func() apis.Exporter {
40+
return export.NewICSExporter()
41+
},
3842
}
3943

4044
start := new(time.Time)
@@ -52,7 +56,7 @@ func RunCreateShiftPlan(writer io.Writer, arguments []string) error {
5256
createCommand.Func("start", "(required) start time of the schedule plan", cli.TimeValueVar(start))
5357
createCommand.Func("end", "(required) end time of the schedule plan", cli.TimeValueVar(end))
5458
createCommand.Func("team-file", "(required) path to the file that contain all on-call duties", cli.FilePathVar(teamFilePath))
55-
createCommand.Func("output", "output format. One of (cvs, table, json)", cli.EnumValueVar(enums, &outputFormat))
59+
createCommand.Func("output", "output format. One of (cvs, table, json, ics)", cli.EnumValueVar(enums, &outputFormat))
5660
createCommand.StringVar(primaryRules, "primary-rules", "vacation", "Rule to decide which employee should be on-call for the next shift")
5761
createCommand.StringVar(secondaryRules, "secondary-rules", "vacation", "Rule to decide which employee should be on-call for the next shift")
5862
createCommand.Usage = func() {

internal/export/ics.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package export
2+
3+
import (
4+
_ "embed"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"text/template"
9+
10+
"github.com/orltom/on-call-schedule/pkg/apis"
11+
)
12+
13+
//go:embed ics.tmpl
14+
var templateICSFile string
15+
16+
var _ apis.Exporter = &TableExporter{}
17+
18+
type event struct {
19+
apis.Shift
20+
UID string
21+
Category string
22+
XColor string
23+
Attendee string
24+
Summary string
25+
Description string
26+
}
27+
28+
type ICSExporter struct {
29+
template *template.Template
30+
}
31+
32+
func NewICSExporter() *ICSExporter {
33+
tmpl, _ := template.New("ical").Parse(templateICSFile)
34+
return &ICSExporter{template: tmpl}
35+
}
36+
37+
func (e *ICSExporter) Write(plan []apis.Shift, writer io.Writer) error {
38+
if plan == nil {
39+
return errors.New("shifts must not be nil")
40+
}
41+
42+
events := make([]event, 0, len(plan))
43+
for idx := range plan {
44+
s := plan[idx]
45+
events = append(events, mapPrimaryEvent(s), mapSecondaryEvent(s))
46+
}
47+
48+
if err := e.template.ExecuteTemplate(writer, "icalCalendar", events); err != nil {
49+
return fmt.Errorf("can not generate ICS: %w", err)
50+
}
51+
return nil
52+
}
53+
54+
func mapPrimaryEvent(shift apis.Shift) event {
55+
return event{
56+
Shift: shift,
57+
UID: "primary-" + shift.Start.Format("20060102"),
58+
Category: "Primary",
59+
XColor: "#FF5733",
60+
Attendee: string(shift.Primary),
61+
Summary: "Primary On-Call: " + string(shift.Primary),
62+
Description: fmt.Sprintf("Primary: %s\\nSecondary: %s", shift.Primary, shift.Secondary),
63+
}
64+
}
65+
66+
func mapSecondaryEvent(shift apis.Shift) event {
67+
return event{
68+
Shift: shift,
69+
UID: "secondary-" + shift.Start.Format("20060102"),
70+
Category: "Secondary",
71+
XColor: "#33FF57",
72+
Attendee: string(shift.Secondary),
73+
Summary: "Secondary On-Call: " + string(shift.Secondary),
74+
Description: fmt.Sprintf("Primary: %s\\nSecondary: %s", shift.Primary, shift.Secondary),
75+
}
76+
}

internal/export/ics.tmpl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{{- define "icalEvent" }}
2+
BEGIN:VEVENT
3+
UID:{{ .UID }}
4+
DTSTART:{{ .Start.Format "20060102T150405Z" }}
5+
DTEND:{{ .End.Format "20060102T150405Z" }}
6+
SUMMARY:{{ .Summary }}
7+
DESCRIPTION:Primary: {{ .Primary }}\nSecondary: {{ .Secondary }}
8+
CATEGORIES:{{ .Category }}
9+
ATTENDEE:mailto:{{ .Attendee }}
10+
X-COLOR:{{ .XColor }}
11+
BEGIN:VALARM
12+
TRIGGER:-PT8H
13+
ACTION:DISPLAY
14+
DESCRIPTION:Reminder: {{ .Summary }} starts in 8 hours.
15+
END:VALARM
16+
END:VEVENT
17+
{{ end -}}
18+
19+
{{- define "icalCalendar" -}}
20+
BEGIN:VCALENDAR
21+
VERSION:2.0
22+
PRODID:-//ocsctl//on-call-shift//EN
23+
CALSCALE:GREGORIAN
24+
{{ range . }}
25+
{{- template "icalEvent" . }}
26+
{{- end }}
27+
END:VCALENDAR
28+
29+
{{- end -}}

internal/export/ics_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package export
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/orltom/on-call-schedule/pkg/apis"
8+
)
9+
10+
func TestICSExporter_Write(t *testing.T) {
11+
type args struct {
12+
plan []apis.Shift
13+
}
14+
tests := []struct {
15+
name string
16+
args args
17+
wantWriter string
18+
wantErr bool
19+
}{
20+
{
21+
name: "When plan is nil, throw an error",
22+
args: args{
23+
plan: nil,
24+
},
25+
wantWriter: "",
26+
wantErr: true,
27+
},
28+
{
29+
name: "Print ICS result according shift plan",
30+
args: args{
31+
plan: []apis.Shift{
32+
{
33+
Start: date("2024-12-25"),
34+
End: date("2024-12-26"),
35+
Primary: "a",
36+
Secondary: "b",
37+
},
38+
},
39+
},
40+
wantWriter: `BEGIN:VCALENDAR
41+
VERSION:2.0
42+
PRODID:-//ocsctl//on-call-shift//EN
43+
CALSCALE:GREGORIAN
44+
45+
BEGIN:VEVENT
46+
UID:primary-20241225
47+
DTSTART:20241225T000000Z
48+
DTEND:20241226T000000Z
49+
SUMMARY:Primary On-Call: a
50+
DESCRIPTION:Primary: a\nSecondary: b
51+
CATEGORIES:Primary
52+
ATTENDEE:mailto:a
53+
X-COLOR:#FF5733
54+
BEGIN:VALARM
55+
TRIGGER:-PT8H
56+
ACTION:DISPLAY
57+
DESCRIPTION:Reminder: Primary On-Call: a starts in 8 hours.
58+
END:VALARM
59+
END:VEVENT
60+
61+
BEGIN:VEVENT
62+
UID:secondary-20241225
63+
DTSTART:20241225T000000Z
64+
DTEND:20241226T000000Z
65+
SUMMARY:Secondary On-Call: b
66+
DESCRIPTION:Primary: a\nSecondary: b
67+
CATEGORIES:Secondary
68+
ATTENDEE:mailto:b
69+
X-COLOR:#33FF57
70+
BEGIN:VALARM
71+
TRIGGER:-PT8H
72+
ACTION:DISPLAY
73+
DESCRIPTION:Reminder: Secondary On-Call: b starts in 8 hours.
74+
END:VALARM
75+
END:VEVENT
76+
77+
END:VCALENDAR`,
78+
wantErr: false,
79+
},
80+
}
81+
for _, tt := range tests {
82+
t.Run(tt.name, func(t *testing.T) {
83+
e := NewICSExporter()
84+
writer := &bytes.Buffer{}
85+
err := e.Write(tt.args.plan, writer)
86+
if (err != nil) != tt.wantErr {
87+
t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr)
88+
return
89+
}
90+
if gotWriter := writer.String(); gotWriter != tt.wantWriter {
91+
t.Errorf("Write() gotWriter = %v, want %v", gotWriter, tt.wantWriter)
92+
}
93+
})
94+
}
95+
}

0 commit comments

Comments
 (0)