Skip to content

Commit 4b57639

Browse files
authored
Merge pull request #106 from arran4/issue97
Issue 97: Support for Alternative Representations
2 parents 2467de0 + 0d0e9ff commit 4b57639

13 files changed

+661
-172
lines changed

Diff for: calendar.go

+67-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io"
1010
"net/http"
1111
"reflect"
12+
"strings"
1213
"time"
1314
)
1415

@@ -169,6 +170,10 @@ func (cp ComponentProperty) Multiple(c Component) bool {
169170
return false
170171
}
171172

173+
func ComponentPropertyExtended(s string) ComponentProperty {
174+
return ComponentProperty("X-" + strings.TrimPrefix("X-", s))
175+
}
176+
172177
type Property string
173178

174179
const (
@@ -232,6 +237,14 @@ const (
232237

233238
type Parameter string
234239

240+
func (p Parameter) IsQuoted() bool {
241+
switch p {
242+
case ParameterAltrep:
243+
return true
244+
}
245+
return false
246+
}
247+
235248
const (
236249
ParameterAltrep Parameter = "ALTREP"
237250
ParameterCn Parameter = "CN"
@@ -404,25 +417,72 @@ func NewCalendarFor(service string) *Calendar {
404417
return c
405418
}
406419

407-
func (cal *Calendar) Serialize() string {
420+
func (cal *Calendar) Serialize(ops ...any) string {
408421
b := bytes.NewBufferString("")
409422
// We are intentionally ignoring the return value. _ used to communicate this to lint.
410-
_ = cal.SerializeTo(b)
423+
_ = cal.SerializeTo(b, ops...)
411424
return b.String()
412425
}
413426

414-
func (cal *Calendar) SerializeTo(w io.Writer) error {
415-
_, _ = fmt.Fprint(w, "BEGIN:VCALENDAR", "\r\n")
427+
type WithLineLength int
428+
type WithNewLine string
429+
430+
func (cal *Calendar) SerializeTo(w io.Writer, ops ...any) error {
431+
serializeConfig, err := parseSerializeOps(ops)
432+
if err != nil {
433+
return err
434+
}
435+
_, _ = fmt.Fprint(w, "BEGIN:VCALENDAR", serializeConfig.NewLine)
416436
for _, p := range cal.CalendarProperties {
417-
p.serialize(w)
437+
err := p.serialize(w, serializeConfig)
438+
if err != nil {
439+
return err
440+
}
418441
}
419442
for _, c := range cal.Components {
420-
c.SerializeTo(w)
443+
err := c.SerializeTo(w, serializeConfig)
444+
if err != nil {
445+
return err
446+
}
421447
}
422-
_, _ = fmt.Fprint(w, "END:VCALENDAR", "\r\n")
448+
_, _ = fmt.Fprint(w, "END:VCALENDAR", serializeConfig.NewLine)
423449
return nil
424450
}
425451

452+
type SerializationConfiguration struct {
453+
MaxLength int
454+
NewLine string
455+
PropertyMaxLength int
456+
}
457+
458+
func parseSerializeOps(ops []any) (*SerializationConfiguration, error) {
459+
serializeConfig := defaultSerializationOptions()
460+
for opi, op := range ops {
461+
switch op := op.(type) {
462+
case WithLineLength:
463+
serializeConfig.MaxLength = int(op)
464+
case WithNewLine:
465+
serializeConfig.NewLine = string(op)
466+
case *SerializationConfiguration:
467+
return op, nil
468+
case error:
469+
return nil, op
470+
default:
471+
return nil, fmt.Errorf("unknown op %d of type %s", opi, reflect.TypeOf(op))
472+
}
473+
}
474+
return serializeConfig, nil
475+
}
476+
477+
func defaultSerializationOptions() *SerializationConfiguration {
478+
serializeConfig := &SerializationConfiguration{
479+
MaxLength: 75,
480+
PropertyMaxLength: 75,
481+
NewLine: string(NewLine),
482+
}
483+
return serializeConfig
484+
}
485+
426486
func (cal *Calendar) SetMethod(method Method, params ...PropertyParameter) {
427487
cal.setProperty(PropertyMethod, string(method), params...)
428488
}

Diff for: calendar_serialization_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,16 @@ func TestCalendar_ReSerialization(t *testing.T) {
3131
}
3232

3333
for _, filename := range testFileNames {
34-
t.Run(fmt.Sprintf("compare serialized -> deserialized -> serialized: %s", filename), func(t *testing.T) {
34+
fp := filepath.Join(testDir, filename)
35+
t.Run(fmt.Sprintf("compare serialized -> deserialized -> serialized: %s", fp), func(t *testing.T) {
3536
//given
36-
originalSeriailizedCal, err := os.ReadFile(filepath.Join(testDir, filename))
37+
originalSeriailizedCal, err := os.ReadFile(fp)
3738
require.NoError(t, err)
3839

3940
//when
4041
deserializedCal, err := ParseCalendar(bytes.NewReader(originalSeriailizedCal))
4142
require.NoError(t, err)
42-
serializedCal := deserializedCal.Serialize()
43+
serializedCal := deserializedCal.Serialize(WithNewLineWindows)
4344

4445
//then
4546
expectedCal, err := os.ReadFile(filepath.Join(expectedDir, filename))

Diff for: calendar_test.go

+56-6
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package ics
22

33
import (
44
"bytes"
5+
"embed"
56
_ "embed"
7+
"github.com/google/go-cmp/cmp"
68
"io"
9+
"io/fs"
710
"net/http"
8-
"os"
911
"path/filepath"
1012
"regexp"
1113
"strings"
@@ -16,8 +18,13 @@ import (
1618
"github.com/stretchr/testify/assert"
1719
)
1820

21+
var (
22+
//go:embed testdata/*
23+
TestData embed.FS
24+
)
25+
1926
func TestTimeParsing(t *testing.T) {
20-
calFile, err := os.OpenFile("./testdata/timeparsing.ics", os.O_RDONLY, 0400)
27+
calFile, err := TestData.Open("testdata/timeparsing.ics")
2128
if err != nil {
2229
t.Errorf("read file: %v", err)
2330
}
@@ -165,12 +172,15 @@ CLASS:PUBLIC
165172
func TestRfc5545Sec4Examples(t *testing.T) {
166173
rnReplace := regexp.MustCompile("\r?\n")
167174

168-
err := filepath.Walk("./testdata/rfc5545sec4/", func(path string, info os.FileInfo, _ error) error {
175+
err := fs.WalkDir(TestData, "testdata/rfc5545sec4", func(path string, info fs.DirEntry, err error) error {
176+
if err != nil {
177+
return err
178+
}
169179
if info.IsDir() {
170180
return nil
171181
}
172182

173-
inputBytes, err := os.ReadFile(path)
183+
inputBytes, err := fs.ReadFile(TestData, path)
174184
if err != nil {
175185
return err
176186
}
@@ -389,13 +399,13 @@ END:VCALENDAR
389399
}
390400

391401
func TestIssue52(t *testing.T) {
392-
err := filepath.Walk("./testdata/issue52/", func(path string, info os.FileInfo, _ error) error {
402+
err := fs.WalkDir(TestData, "testdata/issue52", func(path string, info fs.DirEntry, _ error) error {
393403
if info.IsDir() {
394404
return nil
395405
}
396406
_, fn := filepath.Split(path)
397407
t.Run(fn, func(t *testing.T) {
398-
f, err := os.Open(path)
408+
f, err := TestData.Open(path)
399409
if err != nil {
400410
t.Fatalf("Error reading file: %s", err)
401411
}
@@ -414,6 +424,46 @@ func TestIssue52(t *testing.T) {
414424
}
415425
}
416426

427+
func TestIssue97(t *testing.T) {
428+
err := fs.WalkDir(TestData, "testdata/issue97", func(path string, d fs.DirEntry, err error) error {
429+
if err != nil {
430+
return err
431+
}
432+
if d.IsDir() {
433+
return nil
434+
}
435+
if !strings.HasSuffix(d.Name(), ".ics") && !strings.HasSuffix(d.Name(), ".ics_disabled") {
436+
return nil
437+
}
438+
t.Run(path, func(t *testing.T) {
439+
if strings.HasSuffix(d.Name(), ".ics_disabled") {
440+
t.Skipf("Test disabled")
441+
}
442+
b, err := TestData.ReadFile(path)
443+
if err != nil {
444+
t.Fatalf("Error reading file: %s", err)
445+
}
446+
ics, err := ParseCalendar(bytes.NewReader(b))
447+
if err != nil {
448+
t.Fatalf("Error parsing file: %s", err)
449+
}
450+
451+
got := ics.Serialize(WithLineLength(74))
452+
if diff := cmp.Diff(string(b), got, cmp.Transformer("ToUnixText", func(a string) string {
453+
return strings.ReplaceAll(a, "\r\n", "\n")
454+
})); diff != "" {
455+
t.Errorf("ParseCalendar() mismatch (-want +got):\n%s", diff)
456+
t.Errorf("Complete got:\b%s", got)
457+
}
458+
})
459+
return nil
460+
})
461+
462+
if err != nil {
463+
t.Fatalf("cannot read test directory: %v", err)
464+
}
465+
}
466+
417467
type MockHttpClient struct {
418468
Response *http.Response
419469
Error error

Diff for: cmd/issues/test97_1/main.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
ics "github.com/arran4/golang-ical"
6+
"net/url"
7+
)
8+
9+
func main() {
10+
i := ics.NewCalendarFor("Mozilla.org/NONSGML Mozilla Calendar V1.1")
11+
tz := i.AddTimezone("Europe/Berlin")
12+
tz.AddProperty(ics.ComponentPropertyExtended("TZINFO"), "Europe/Berlin[2024a]")
13+
tzstd := tz.AddStandard()
14+
tzstd.AddProperty(ics.ComponentProperty(ics.PropertyTzoffsetto), "+010000")
15+
tzstd.AddProperty(ics.ComponentProperty(ics.PropertyTzoffsetfrom), "+005328")
16+
tzstd.AddProperty(ics.ComponentProperty(ics.PropertyTzname), "Europe/Berlin(STD)")
17+
tzstd.AddProperty(ics.ComponentProperty(ics.PropertyDtstart), "18930401T000000")
18+
tzstd.AddProperty(ics.ComponentProperty(ics.PropertyRdate), "18930401T000000")
19+
vEvent := i.AddEvent("d23cef0d-9e58-43c4-9391-5ad8483ca346")
20+
vEvent.AddProperty(ics.ComponentPropertyCreated, "20240929T120640Z")
21+
vEvent.AddProperty(ics.ComponentPropertyLastModified, "20240929T120731Z")
22+
vEvent.AddProperty(ics.ComponentPropertyDtstamp, "20240929T120731Z")
23+
vEvent.AddProperty(ics.ComponentPropertySummary, "Test Event")
24+
vEvent.AddProperty(ics.ComponentPropertyDtStart, "20240929T144500", ics.WithTZID("Europe/Berlin"))
25+
vEvent.AddProperty(ics.ComponentPropertyDtEnd, "20240929T154500", ics.WithTZID("Europe/Berlin"))
26+
vEvent.AddProperty(ics.ComponentPropertyTransp, "OPAQUE")
27+
vEvent.AddProperty(ics.ComponentPropertyLocation, "Github")
28+
uri := &url.URL{
29+
Scheme: "data",
30+
Opaque: "text/html,I%20want%20a%20custom%20linkout%20for%20Thunderbird.%3Cbr%3EThis%20is%20the%20Github%20%3Ca%20href%3D%22https%3A%2F%2Fgithub.com%2Farran4%2Fgolang-ical%2Fissues%2F97%22%3EIssue%3C%2Fa%3E.",
31+
}
32+
vEvent.AddProperty(ics.ComponentPropertyDescription, "I want a custom linkout for Thunderbird.\nThis is the Github Issue.", ics.WithAlternativeRepresentation(uri))
33+
fmt.Println(i.Serialize())
34+
}

0 commit comments

Comments
 (0)