diff --git a/README.md b/README.md index 5c4122d..9798ed0 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,18 @@ A ICS / ICal parser and serialiser for Golang. Because the other libraries didn't quite do what I needed. Usage, parsing: -``` +```golang cal, err := ParseCalendar(strings.NewReader(input)) ``` -Creating: +Usage, parsing from a URL : +```golang + cal, err := ParseCalendar("an-ics-url") ``` + +Creating: +```golang cal := ics.NewCalendar() cal.SetMethod(ics.MethodRequest) event := cal.AddEvent(fmt.Sprintf("id@domain", p.SessionKey.IntID())) diff --git a/calendar.go b/calendar.go index 1d1fc33..7921bcc 100644 --- a/calendar.go +++ b/calendar.go @@ -3,9 +3,12 @@ package ics import ( "bufio" "bytes" + "context" "errors" "fmt" "io" + "net/http" + "reflect" "time" ) @@ -412,6 +415,108 @@ func (cal *Calendar) setProperty(property Property, value string, params ...Prop cal.CalendarProperties = append(cal.CalendarProperties, r) } +func (calendar *Calendar) AddEvent(id string) *VEvent { + e := NewEvent(id) + calendar.Components = append(calendar.Components, e) + return e +} + +func (calendar *Calendar) AddVEvent(e *VEvent) { + calendar.Components = append(calendar.Components, e) +} + +func (calendar *Calendar) Events() (r []*VEvent) { + r = []*VEvent{} + for i := range calendar.Components { + switch event := calendar.Components[i].(type) { + case *VEvent: + r = append(r, event) + } + } + return +} + +func (calendar *Calendar) RemoveEvent(id string) { + for i := range calendar.Components { + switch event := calendar.Components[i].(type) { + case *VEvent: + if event.Id() == id { + if len(calendar.Components) > i+1 { + calendar.Components = append(calendar.Components[:i], calendar.Components[i+1:]...) + } else { + calendar.Components = calendar.Components[:i] + } + return + } + } + } +} + +func WithCustomClient(client *http.Client) *http.Client { + return client +} + +func WithCustomRequest(request *http.Request) *http.Request { + return request +} + +func ParseCalendarFromUrl(url string, opts ...any) (*Calendar, error) { + var ctx context.Context + var req *http.Request + var client HttpClientLike = http.DefaultClient + for opti, opt := range opts { + switch opt := opt.(type) { + case *http.Client: + client = opt + case HttpClientLike: + client = opt + case func() *http.Client: + client = opt() + case *http.Request: + req = opt + case func() *http.Request: + req = opt() + case context.Context: + ctx = opt + case func() context.Context: + ctx = opt() + default: + return nil, fmt.Errorf("unknown optional argument %d on ParseCalendarFromUrl: %s", opti, reflect.TypeOf(opt)) + } + } + if ctx == nil { + ctx = context.Background() + } + if req == nil { + var err error + req, err = http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("creating http request: %w", err) + } + } + return parseCalendarFromHttpRequest(client, req) +} + +type HttpClientLike interface { + Do(req *http.Request) (*http.Response, error) +} + +func parseCalendarFromHttpRequest(client HttpClientLike, request *http.Request) (*Calendar, error) { + resp, err := client.Do(request) + if err != nil { + return nil, fmt.Errorf("http request: %w", err) + } + defer func(closer io.ReadCloser) { + if derr := closer.Close(); derr != nil && err == nil { + err = fmt.Errorf("http request close: %w", derr) + } + }(resp.Body) + var cal *Calendar + cal, err = ParseCalendar(resp.Body) + // This allows the defer func to change the error + return cal, err +} + func ParseCalendar(r io.Reader) (*Calendar, error) { state := "begin" c := &Calendar{} diff --git a/calendar_test.go b/calendar_test.go index 804e703..66ac80b 100644 --- a/calendar_test.go +++ b/calendar_test.go @@ -3,7 +3,10 @@ package ics import ( "errors" "github.com/stretchr/testify/assert" + "bytes" + _ "embed" "io" + "net/http" "os" "path/filepath" "regexp" @@ -411,3 +414,33 @@ func TestIssue52(t *testing.T) { t.Fatalf("cannot read test directory: %v", err) } } + +type MockHttpClient struct { + Response *http.Response + Error error +} + +func (m *MockHttpClient) Do(req *http.Request) (*http.Response, error) { + return m.Response, m.Error +} + +var ( + _ HttpClientLike = &MockHttpClient{} + //go:embed "testdata/rfc5545sec4/input1.ics" + input1TestData []byte +) + +func TestIssue77(t *testing.T) { + url := "https://proseconsult.umontpellier.fr/jsp/custom/modules/plannings/direct_cal.jsp?data=58c99062bab31d256bee14356aca3f2423c0f022cb9660eba051b2653be722c4c7f281e4e3ad06b85d3374100ac416a4dc5c094f7d1a811b903031bde802c7f50e0bd1077f9461bed8f9a32b516a3c63525f110c026ed6da86f487dd451ca812c1c60bb40b1502b6511435cf9908feb2166c54e36382c1aa3eb0ff5cb8980cdb,1" + + _, err := ParseCalendarFromUrl(url, &MockHttpClient{ + Response: &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader(input1TestData)), + }, + }) + + if err != nil { + t.Fatalf("Error reading file: %s", err) + } +} diff --git a/components.go b/components.go index 7bff898..2855569 100644 --- a/components.go +++ b/components.go @@ -475,77 +475,40 @@ func NewEvent(uniqueId string) *VEvent { return e } -func (cal *Calendar) AddEvent(id string) *VEvent { - e := NewEvent(id) - cal.Components = append(cal.Components, e) - return e +func (event *VEvent) SetEndAt(t time.Time, props ...PropertyParameter) { + event.SetProperty(ComponentPropertyDtEnd, t.UTC().Format(icalTimestampFormatUtc), props...) } -func (cal *Calendar) AddVEvent(e *VEvent) { - cal.Components = append(cal.Components, e) +func (event *VEvent) SetLastModifiedAt(t time.Time, props ...PropertyParameter) { + event.SetProperty(ComponentPropertyLastModified, t.UTC().Format(icalTimestampFormatUtc), props...) } -func (cal *Calendar) RemoveEvent(id string) { - for i := range cal.Components { - switch event := cal.Components[i].(type) { - case *VEvent: - if event.Id() == id { - if len(cal.Components) > i+1 { - cal.Components = append(cal.Components[:i], cal.Components[i+1:]...) - } else { - cal.Components = cal.Components[:i] - } - return - } - } - } +func (event *VEvent) SetGeo(lat interface{}, lng interface{}, params ...PropertyParameter) { + event.setGeo(lat, lng, params...) } -func (cal *Calendar) Events() []*VEvent { - var r []*VEvent - for i := range cal.Components { - switch event := cal.Components[i].(type) { - case *VEvent: - r = append(r, event) - } - } - return r +func (event *VEvent) SetPriority(p int, params ...PropertyParameter) { + event.setPriority(p, params...) } -func (c *VEvent) SetEndAt(t time.Time, params ...PropertyParameter) { - c.SetProperty(ComponentPropertyDtEnd, t.UTC().Format(icalTimestampFormatUtc), params...) +func (event *VEvent) SetResources(r string, params ...PropertyParameter) { + event.setResources(r, params...) } -func (c *VEvent) SetLastModifiedAt(t time.Time, params ...PropertyParameter) { - c.SetProperty(ComponentPropertyLastModified, t.UTC().Format(icalTimestampFormatUtc), params...) +func (event *VEvent) AddAlarm() *VAlarm { + return event.addAlarm() } -func (c *VEvent) SetGeo(lat interface{}, lng interface{}, params ...PropertyParameter) { - c.setGeo(lat, lng, params...) +func (event *VEvent) AddVAlarm(a *VAlarm) { + event.addVAlarm(a) } -func (c *VEvent) SetPriority(p int, params ...PropertyParameter) { - c.setPriority(p, params...) -} - -func (c *VEvent) SetResources(r string, params ...PropertyParameter) { - c.setResources(r, params...) -} - -func (c *VEvent) AddAlarm() *VAlarm { - return c.addAlarm() -} - -func (c *VEvent) AddVAlarm(a *VAlarm) { - c.addVAlarm(a) -} - -func (c *VEvent) Alarms() []*VAlarm { - return c.alarms() +func (event *VEvent) Alarms() []*VAlarm { + return event.alarms() } -func (c *VEvent) GetAllDayEndAt() (time.Time, error) { - return c.getTimeProp(ComponentPropertyDtEnd, true) +func (event *VEvent) GetAllDayEndAt() (time.Time, error) { + return event.getTimeProp(ComponentPropertyDtEnd, true) } type TimeTransparency string @@ -555,8 +518,8 @@ const ( TransparencyTransparent TimeTransparency = "TRANSPARENT" ) -func (c *VEvent) SetTimeTransparency(v TimeTransparency, params ...PropertyParameter) { - c.SetProperty(ComponentPropertyTransp, string(v), params...) +func (event *VEvent) SetTimeTransparency(v TimeTransparency, params ...PropertyParameter) { + event.SetProperty(ComponentPropertyTransp, string(v), params...) } type VTodo struct {