Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into issue97
Browse files Browse the repository at this point in the history
* origin/master:
  Duplication issue fixed.
  Some multiple-ness.
  Test fixed.
  Resynced and moved some functions around #78
  added functionnal option pattern for url parsing
  usage example, README
  calendar parsing url support
  • Loading branch information
arran4 committed Oct 16, 2024
2 parents 5bb438c + 2467de0 commit 1015df8
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 38 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down
208 changes: 208 additions & 0 deletions calendar.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package ics
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"reflect"

Check failure on line 10 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, ubuntu-latest)

other declaration of reflect

Check failure on line 10 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (go.mod, ubuntu-latest)

other declaration of reflect

Check failure on line 10 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (go.mod, ubuntu-latest)

other declaration of reflect

Check failure on line 10 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, macos-13)

other declaration of reflect

Check failure on line 10 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, macos-13)

other declaration of reflect

Check failure on line 10 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, ubuntu-latest)

other declaration of reflect

Check failure on line 10 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (stable, ubuntu-latest)

other declaration of reflect
"strings"
"net/http"
"reflect"

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, ubuntu-latest)

reflect redeclared in this block

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, ubuntu-latest)

"reflect" imported and not used

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (go.mod, ubuntu-latest)

reflect redeclared in this block

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (go.mod, ubuntu-latest)

"reflect" imported and not used

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (go.mod, ubuntu-latest)

reflect redeclared in this block

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (go.mod, ubuntu-latest)

"reflect" imported and not used

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, macos-13)

reflect redeclared in this block

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, macos-13)

"reflect" imported and not used

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, macos-13)

reflect redeclared in this block

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, macos-13)

"reflect" imported and not used

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, ubuntu-latest)

reflect redeclared in this block

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (oldstable, ubuntu-latest)

"reflect" imported and not used

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (stable, ubuntu-latest)

reflect redeclared in this block

Check failure on line 13 in calendar.go

View workflow job for this annotation

GitHub Actions / Test (stable, ubuntu-latest)

"reflect" imported and not used
"time"
)

Expand Down Expand Up @@ -63,8 +66,111 @@ const (
ComponentPropertyTzid = ComponentProperty(PropertyTzid)
ComponentPropertyComment = ComponentProperty(PropertyComment)
ComponentPropertyRelatedTo = ComponentProperty(PropertyRelatedTo)
ComponentPropertyMethod = ComponentProperty(PropertyMethod)
ComponentPropertyRecurrenceId = ComponentProperty(PropertyRecurrenceId)
ComponentPropertyDuration = ComponentProperty(PropertyDuration)
ComponentPropertyContact = ComponentProperty(PropertyContact)
ComponentPropertyRequestStatus = ComponentProperty(PropertyRequestStatus)
ComponentPropertyRDate = ComponentProperty(PropertyRdate)
)

// Required returns the rules from the RFC as to if they are required or not for any particular component type
// If unspecified or incomplete, it returns false. -- This list is incomplete verify source. Happy to take PRs with reference
// iana-prop and x-props are not covered as it would always be true and require an exhaustive list.
func (cp ComponentProperty) Required(c Component) bool {
// https://www.rfc-editor.org/rfc/rfc5545#section-3.6.1
switch cp {
case ComponentPropertyDtstamp, ComponentPropertyUniqueId:
switch c.(type) {
case *VEvent:
return true
}
case ComponentPropertyDtStart:
switch c := c.(type) {
case *VEvent:
return !c.HasProperty(ComponentPropertyMethod)
}
}
return false
}

// Exclusive returns the ComponentProperty's using the rules from the RFC as to if one or more existing properties are prohibiting this one
// If unspecified or incomplete, it returns false. -- This list is incomplete verify source. Happy to take PRs with reference
// iana-prop and x-props are not covered as it would always be true and require an exhaustive list.
func (cp ComponentProperty) Exclusive(c Component) []ComponentProperty {
// https://www.rfc-editor.org/rfc/rfc5545#section-3.6.1
switch cp {
case ComponentPropertyDtEnd:
switch c := c.(type) {
case *VEvent:
if c.HasProperty(ComponentPropertyDuration) {
return []ComponentProperty{ComponentPropertyDuration}
}
}
case ComponentPropertyDuration:
switch c := c.(type) {
case *VEvent:
if c.HasProperty(ComponentPropertyDtEnd) {
return []ComponentProperty{ComponentPropertyDtEnd}
}
}
}
return nil
}

// Singular returns the rules from the RFC as to if the spec states that if "Must not occur more than once"
// iana-prop and x-props are not covered as it would always be true and require an exhaustive list.
func (cp ComponentProperty) Singular(c Component) bool {
// https://www.rfc-editor.org/rfc/rfc5545#section-3.6.1
switch cp {
case ComponentPropertyClass, ComponentPropertyCreated, ComponentPropertyDescription, ComponentPropertyGeo,
ComponentPropertyLastModified, ComponentPropertyLocation, ComponentPropertyOrganizer, ComponentPropertyPriority,
ComponentPropertySequence, ComponentPropertyStatus, ComponentPropertySummary, ComponentPropertyTransp,
ComponentPropertyUrl, ComponentPropertyRecurrenceId:
switch c.(type) {
case *VEvent:
return true
}
}
return false
}

// Optional returns the rules from the RFC as to if the spec states that if these are optional
// iana-prop and x-props are not covered as it would always be true and require an exhaustive list.
func (cp ComponentProperty) Optional(c Component) bool {
// https://www.rfc-editor.org/rfc/rfc5545#section-3.6.1
switch cp {
case ComponentPropertyClass, ComponentPropertyCreated, ComponentPropertyDescription, ComponentPropertyGeo,
ComponentPropertyLastModified, ComponentPropertyLocation, ComponentPropertyOrganizer, ComponentPropertyPriority,
ComponentPropertySequence, ComponentPropertyStatus, ComponentPropertySummary, ComponentPropertyTransp,
ComponentPropertyUrl, ComponentPropertyRecurrenceId, ComponentPropertyRrule, ComponentPropertyAttach,
ComponentPropertyAttendee, ComponentPropertyCategories, ComponentPropertyComment,
ComponentPropertyContact, ComponentPropertyExdate, ComponentPropertyRequestStatus, ComponentPropertyRelatedTo,
ComponentPropertyResources, ComponentPropertyRDate:
switch c.(type) {
case *VEvent:
return true
}
}
return false
}

// Multiple returns the rules from the RFC as to if the spec states explicitly if multiple are allowed
// iana-prop and x-props are not covered as it would always be true and require an exhaustive list.
func (cp ComponentProperty) Multiple(c Component) bool {
// https://www.rfc-editor.org/rfc/rfc5545#section-3.6.1
switch cp {
case ComponentPropertyAttach, ComponentPropertyAttendee, ComponentPropertyCategories, ComponentPropertyComment,
ComponentPropertyContact, ComponentPropertyExdate, ComponentPropertyRequestStatus, ComponentPropertyRelatedTo,
ComponentPropertyResources, ComponentPropertyRDate:
switch c.(type) {
case *VEvent:
return true
}
}
return false
}

func ComponentPropertyExtended(s string) ComponentProperty {
return ComponentProperty("X-" + strings.TrimPrefix("X-", s))
}
Expand Down Expand Up @@ -473,6 +579,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{}
Expand Down
34 changes: 34 additions & 0 deletions calendar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import (
"bytes"
"embed"
"github.com/google/go-cmp/cmp"
"bytes"
_ "embed"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
Expand Down Expand Up @@ -461,3 +465,33 @@ func TestIssue97(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)
}
}
Loading

0 comments on commit 1015df8

Please sign in to comment.