feat: implement caldav search

This commit is contained in:
Cyrille Nofficial 2022-04-18 12:47:47 +02:00
parent f9e8f4b9c1
commit d7191461eb
57 changed files with 3893 additions and 20 deletions

View File

@ -1,12 +1,73 @@
package calendar
import (
"fmt"
"github.com/dolanor/caldav-go/caldav"
"github.com/dolanor/caldav-go/caldav/entities"
"github.com/dolanor/caldav-go/icalendar/components"
"log"
"math"
"net/http"
"strings"
"time"
)
type Caldav interface {
QueryEvents(path string, query *entities.CalendarQuery) (events []*components.Event, oerr error)
}
type Calendar struct {
Location *time.Location
Location *time.Location
cdav Caldav
caldavPath string
caldavSummaryPattern string
}
func NewCaldav(caldavUrl, caldavPath string) (Caldav, error) {
// create a reference to your CalDAV-compliant server
server, _ := caldav.NewServer(caldavUrl)
// create a CalDAV client to speak to the server
var client = caldav.NewClient(server, http.DefaultClient)
// start executing requests!
err := client.ValidateServer(caldavPath)
if err != nil {
return nil, fmt.Errorf("bad caldav configuration, unable to validate connexion: %w", err)
}
return client, nil
}
type Option func(calendar *Calendar)
func WithCaldav(cdav Caldav) Option {
return func(calendar *Calendar) {
calendar.cdav = cdav
}
}
func WithCaldavSummaryPattern(caldavSummaryPattern string) Option {
return func(calendar *Calendar) {
calendar.caldavSummaryPattern = caldavSummaryPattern
}
}
func WithCaldavPath(caldavPath string) Option {
return func(calendar *Calendar) {
calendar.caldavPath = caldavPath
}
}
func New(location *time.Location, opts ...Option) *Calendar {
c := &Calendar{
location,
nil,
"",
"",
}
for _, opt := range opts {
opt(c)
}
return c
}
func (cal *Calendar) GetEasterDay(year int) time.Time {
@ -72,20 +133,24 @@ func (cal *Calendar) GetHolidays(year int) *[]time.Time {
return &joursFeries
}
func (cal *Calendar) GetHolidaysSet(year int) *map[time.Time]bool {
func (cal *Calendar) GetHolidaysSet(year int) map[time.Time]bool {
holidays := cal.GetHolidays(year)
result := make(map[time.Time]bool, len(*holidays))
for _, h := range *holidays {
result[h] = true
}
return &result
return result
}
func(cal *Calendar) IsHoliday(date time.Time) bool{
func (cal *Calendar) IsHoliday(date time.Time) bool {
h := cal.GetHolidaysSet(date.Year())
d := date.In(cal.Location)
day := time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, cal.Location)
return (*h)[day]
caldavHolidays, err := cal.IsHolidaysFromCaldav(day)
if err != nil {
log.Printf("unable to check holidays from caldav: %v", err)
}
return h[day] || caldavHolidays
}
func (cal *Calendar) IsWorkingDay(date time.Time) bool {
@ -96,6 +161,27 @@ func (cal *Calendar) IsWorkingDayToday() bool {
return cal.IsWorkingDay(time.Now())
}
func (cal *Calendar) IsWeekDay(day time.Time) bool{
func (cal *Calendar) IsWeekDay(day time.Time) bool {
return day.Weekday() >= time.Monday && day.Weekday() <= time.Friday
}
func (cal *Calendar) IsHolidaysFromCaldav(day time.Time) (bool, error) {
if cal.cdav == nil {
return false, nil
}
query, err := entities.NewEventRangeQuery(day.UTC(), day.UTC().Add(23*time.Hour+59*time.Minute))
if err != nil {
return false, fmt.Errorf("unable to build events range query: %v", err)
}
events, err := cal.cdav.QueryEvents(cal.caldavPath, query)
if err != nil {
return false, fmt.Errorf("unable list events from caldav: %v", err)
}
for _, evt := range events {
if strings.Contains(evt.Summary, cal.caldavSummaryPattern) {
return true, nil
}
}
return false, nil
}

View File

@ -1,6 +1,9 @@
package calendar
import (
"github.com/dolanor/caldav-go/caldav/entities"
"github.com/dolanor/caldav-go/icalendar/components"
"github.com/dolanor/caldav-go/icalendar/values"
"testing"
"time"
)
@ -48,7 +51,7 @@ func TestCalendar_GetHolidays(t *testing.T) {
time.Date(2020, time.December, 25, 0, 0, 0, 0, loc): true,
}
c := Calendar{loc}
c := New(loc)
holidays := c.GetHolidays(2020)
if len(*holidays) != len(expectedHolidays) {
t.Errorf("bad number of holidays, %d but %d are expected", len(*holidays), len(expectedHolidays))
@ -80,13 +83,13 @@ func TestCalendar_GetHolidaysSet(t *testing.T) {
time.Date(2020, time.December, 25, 0, 0, 0, 0, loc),
}
c := Calendar{loc}
c := New(loc)
holidays := c.GetHolidaysSet(2020)
if len(*holidays) != len(expectedHolidays) {
t.Errorf("bad number of holidays, %d but %d are expected", len(*holidays), len(expectedHolidays))
if len(holidays) != len(expectedHolidays) {
t.Errorf("bad number of holidays, %d but %d are expected", len(holidays), len(expectedHolidays))
}
for _, h := range expectedHolidays {
if !(*holidays)[h] {
if !(holidays)[h] {
t.Errorf("%v is not a holiday", h)
}
}
@ -112,10 +115,10 @@ func TestCalendar_IsHolidays(t *testing.T) {
time.Date(2020, time.December, 25, 0, 0, 0, 0, loc),
}
c := Calendar{loc}
c := New(loc)
holidays := c.GetHolidaysSet(2020)
if len(*holidays) != len(expectedHolidays) {
t.Errorf("bad number of holidays, %d but %d are expected", len(*holidays), len(expectedHolidays))
if len(holidays) != len(expectedHolidays) {
t.Errorf("bad number of holidays, %d but %d are expected", len(holidays), len(expectedHolidays))
}
for _, h := range expectedHolidays {
if !c.IsHoliday(h) {
@ -133,7 +136,7 @@ func TestCalendar_IsWorkingDay(t *testing.T) {
t.Errorf("unable to load time location: %v", err)
t.Fail()
}
c := Calendar{loc}
c := New(loc)
if c.IsWorkingDay(time.Date(2019, time.January, 01, 0, 0, 0, 0, loc)) {
t.Error("1st january is not a working day")
@ -164,3 +167,114 @@ func TestCalendar_IsWorkingDay(t *testing.T) {
t.Error("Sunday should not be a working day")
}
}
type MockCaldav struct {
events []*components.Event
}
func (m *MockCaldav) QueryEvents(_ string, _ *entities.CalendarQuery) ([]*components.Event, error) {
return m.events, nil
}
func TestCalendar_IsHolidaysFromCaldav(t *testing.T) {
loc, err := time.LoadLocation("Europe/Paris")
if err != nil {
t.Errorf("unable to load time location: %v", err)
t.Fail()
}
type fields struct {
Location *time.Location
cdav *MockCaldav
caldavPath string
caldavSummaryPattern string
}
type args struct {
day time.Time
}
tests := []struct {
name string
fields fields
args args
want bool
wantErr bool
}{
{
name: "Holidays in events",
fields: fields{
Location: loc,
cdav: &MockCaldav{
events: []*components.Event{
{
UID: "1",
DateStart: values.NewDateTime(time.Date(2022, time.April, 16, 0, 0, 0, 0, loc)),
DateEnd: values.NewDateTime(time.Date(2022, time.April, 17, 0, 0, 0, 0, loc)),
Summary: "Holidays",
},
},
},
caldavPath: "my_calendar/",
caldavSummaryPattern: "Holidays",
},
args: args{
day: time.Date(2022, time.April, 16, 0, 0, 0, 0, loc),
},
want: true,
wantErr: false,
},
{
name: "Not Holidays in events",
fields: fields{
Location: loc,
cdav: &MockCaldav{
events: []*components.Event{
{
UID: "1",
DateStart: values.NewDateTime(time.Date(2022, time.April, 16, 0, 0, 0, 0, loc)),
DateEnd: values.NewDateTime(time.Date(2022, time.April, 17, 0, 0, 0, 0, loc)),
Summary: "Another event",
},
},
},
caldavPath: "my_calendar/",
caldavSummaryPattern: "Holidays",
},
args: args{
day: time.Date(2022, time.April, 16, 0, 0, 0, 0, loc),
},
want: false,
wantErr: false,
},
{
name: "No events",
fields: fields{
Location: loc,
cdav: &MockCaldav{},
caldavPath: "my_calendar/",
caldavSummaryPattern: "Holidays",
},
args: args{
day: time.Date(2022, time.April, 15, 0, 0, 0, 0, loc),
},
want: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cal := New(
loc,
WithCaldav(tt.fields.cdav),
WithCaldavPath(tt.fields.caldavPath),
WithCaldavSummaryPattern(tt.fields.caldavSummaryPattern),
)
got, err := cal.IsHolidaysFromCaldav(tt.args.day)
if (err != nil) != tt.wantErr {
t.Errorf("IsHolidaysFromCaldav() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("IsHolidaysFromCaldav() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -6,6 +6,9 @@ import (
"encoding/json"
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/hellofresh/health-go/v4"
"github.com/prometheus/client_golang/prometheus"
@ -17,7 +20,8 @@ import (
)
var (
cal calendar.Calendar
cal *calendar.Calendar
location *time.Location
calCounter *prometheus.CounterVec
calSummary *prometheus.SummaryVec
calHistogram *prometheus.HistogramVec
@ -28,7 +32,7 @@ func init() {
if err != nil {
log.Fatalf("unable to load time location: %v", err)
}
cal = calendar.Calendar{Location: loc}
location = loc
calCounter = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "domogeek",
@ -93,11 +97,25 @@ func (c *CalendarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func main() {
var port int
var host string
var caldavUrl, caldavPath, caldavSummaryPattern string
flag.StringVar(&host, "host", "", "host to listen, default all addresses")
flag.IntVar(&port, "port", 8080, "port to listen")
flag.StringVar(&caldavUrl, "caldav-url", "", "caldav url to use to read holidays events")
flag.StringVar(&caldavPath, "caldav-path", "", "caldav path to use to read holidays events")
flag.StringVar(&caldavSummaryPattern, "caldav-summary-pattern", "Holidays", "Summary pattern that matches holidays event")
flag.Parse()
cdav, err := calendar.NewCaldav(caldavUrl, caldavPath)
if err != nil {
log.Fatalf("unable to init caldav instance")
}
cal = calendar.New(location,
calendar.WithCaldav(cdav),
calendar.WithCaldavPath(caldavPath),
calendar.WithCaldavSummaryPattern(caldavSummaryPattern),
)
addr := fmt.Sprintf("%s:%d", host, port)
log.Printf("start server on %s", addr)
@ -117,9 +135,25 @@ func main() {
Check: func(ctx context.Context) error {
return nil
},
},
))
}),
health.WithChecks(health.Config{
Name: "caldav",
Timeout: 5 * time.Second,
SkipOnErr: false,
Check: func(ctx context.Context) error {
_, err := cal.IsHolidaysFromCaldav(time.Now())
return err
},
}),
)
http.Handle("/status", healthz.Handler())
log.Fatal(http.ListenAndServe(addr, nil))
signChan := make(chan os.Signal, 1)
go func() {
log.Fatal(http.ListenAndServe(addr, nil))
}()
signal.Notify(signChan, syscall.SIGTERM)
<-signChan
log.Printf("exit on sigterm")
}

1
go.mod
View File

@ -3,6 +3,7 @@ module domogeek
go 1.18
require (
github.com/dolanor/caldav-go v0.2.1
github.com/hellofresh/health-go/v4 v4.5.0
github.com/prometheus/client_golang v1.12.1
)

5
go.sum
View File

@ -62,6 +62,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dolanor/caldav-go v0.2.1 h1:wbARF+WKIryMOApYdX6CnFKY25Kz3ChPerfFc/R3Txk=
github.com/dolanor/caldav-go v0.2.1/go.mod h1:0A9uEq2TN7U1eNh11hHAZ0FA7jyuFSh5hXeaoFPC0fc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -226,9 +228,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -645,6 +649,7 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=

22
vendor/github.com/dolanor/caldav-go/LICENSE generated vendored Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

191
vendor/github.com/dolanor/caldav-go/caldav/client.go generated vendored Normal file
View File

@ -0,0 +1,191 @@
package caldav
import (
"fmt"
"log"
"net/http"
"strings"
cent "github.com/dolanor/caldav-go/caldav/entities"
"github.com/dolanor/caldav-go/icalendar/components"
"github.com/dolanor/caldav-go/utils"
"github.com/dolanor/caldav-go/webdav"
"github.com/dolanor/caldav-go/webdav/entities"
)
var _ = log.Print
// a client for making WebDAV requests
type Client webdav.Client
// downcasts the client to the WebDAV interface
func (c *Client) WebDAV() *webdav.Client {
return (*webdav.Client)(c)
}
// returns the embedded CalDAV server reference
func (c *Client) Server() *Server {
return (*Server)(c.WebDAV().Server())
}
// fetches a list of CalDAV features supported by the server
// returns an error if the server does not support DAV
func (c *Client) Features(path string) ([]string, error) {
var cfeatures []string
if features, err := c.WebDAV().Features(path); err != nil {
return cfeatures, utils.NewError(c.Features, "unable to detect features", c, err)
} else {
for _, feature := range features {
if strings.HasPrefix(feature, "calendar-") {
cfeatures = append(cfeatures, feature)
}
}
return cfeatures, nil
}
}
// fetches a list of CalDAV features and checks if a certain one is supported by the server
// returns an error if the server does not support DAV
func (c *Client) SupportsFeature(name string, path string) (bool, error) {
if features, err := c.Features(path); err != nil {
return false, utils.NewError(c.SupportsFeature, "feature detection failed", c, err)
} else {
var test = fmt.Sprintf("calendar-%s", name)
for _, feature := range features {
if feature == test {
return true, nil
}
}
return false, nil
}
}
// fetches a list of CalDAV features and checks if a certain one is supported by the server
// returns an error if the server does not support DAV
func (c *Client) ValidateServer(path string) error {
if found, err := c.SupportsFeature("access", path); err != nil {
return utils.NewError(c.SupportsFeature, "feature detection failed", c, err)
} else if !found {
return utils.NewError(c.SupportsFeature, "calendar access feature missing", c, nil)
} else {
return nil
}
}
// creates a new calendar collection on a given path
func (c *Client) MakeCalendar(path string) error {
if req, err := c.Server().NewRequest("MKCALENDAR", path); err != nil {
return utils.NewError(c.MakeCalendar, "unable to create request", c, err)
} else if resp, err := c.Do(req); err != nil {
return utils.NewError(c.MakeCalendar, "unable to execute request", c, err)
} else if resp.StatusCode != http.StatusCreated {
err := new(entities.Error)
resp.Decode(err)
msg := fmt.Sprintf("unexpected server response %s", resp.Status)
return utils.NewError(c.MakeCalendar, msg, c, err)
} else {
return nil
}
}
// creates or updates one or more events on the remote CalDAV server
func (c *Client) PutEvents(path string, events ...*components.Event) error {
if len(events) <= 0 {
return utils.NewError(c.PutEvents, "no calendar events provided", c, nil)
} else if cal := components.NewCalendar(events...); events[0] == nil {
return utils.NewError(c.PutEvents, "icalendar event must not be nil", c, nil)
} else if err := c.PutCalendars(path, cal); err != nil {
return utils.NewError(c.PutEvents, "unable to put calendar", c, err)
}
return nil
}
// creates or updates one or more calendars on the remote CalDAV server
func (c *Client) PutCalendars(path string, calendars ...*components.Calendar) error {
if req, err := c.Server().NewRequest("PUT", path, calendars); err != nil {
return utils.NewError(c.PutCalendars, "unable to encode request", c, err)
} else if resp, err := c.Do(req); err != nil {
return utils.NewError(c.PutCalendars, "unable to execute request", c, err)
} else if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
err := new(entities.Error)
resp.WebDAV().Decode(err)
msg := fmt.Sprintf("unexpected server response %s", resp.Status)
return utils.NewError(c.PutCalendars, msg, c, err)
}
return nil
}
// attempts to fetch an event on the remote CalDAV server
func (c *Client) GetEvents(path string) ([]*components.Event, error) {
cal := new(components.Calendar)
if req, err := c.Server().NewRequest("GET", path); err != nil {
return nil, utils.NewError(c.GetEvents, "unable to create request", c, err)
} else if resp, err := c.Do(req); err != nil {
return nil, utils.NewError(c.GetEvents, "unable to execute request", c, err)
} else if resp.StatusCode != http.StatusOK {
err := new(entities.Error)
resp.WebDAV().Decode(err)
msg := fmt.Sprintf("unexpected server response %s", resp.Status)
return nil, utils.NewError(c.GetEvents, msg, c, err)
} else if err := resp.Decode(cal); err != nil {
return nil, utils.NewError(c.GetEvents, "unable to decode response", c, err)
} else {
return cal.Events, nil
}
}
// attempts to fetch an event on the remote CalDAV server
func (c *Client) QueryEvents(path string, query *cent.CalendarQuery) (events []*components.Event, oerr error) {
ms := new(cent.Multistatus)
if req, err := c.Server().WebDAV().NewRequest("REPORT", path, query); err != nil {
oerr = utils.NewError(c.QueryEvents, "unable to create request", c, err)
} else if req.Http().Native().Header.Set("Depth", string(webdav.Depth1)); false {
} else if resp, err := c.WebDAV().Do(req); err != nil {
oerr = utils.NewError(c.QueryEvents, "unable to execute request", c, err)
} else if resp.StatusCode == http.StatusNotFound {
return // no events if not found
} else if resp.StatusCode != webdav.StatusMulti {
err := new(entities.Error)
msg := fmt.Sprintf("unexpected server response %s", resp.Status)
resp.Decode(err)
oerr = utils.NewError(c.QueryEvents, msg, c, err)
} else if err := resp.Decode(ms); err != nil {
msg := "unable to decode response"
oerr = utils.NewError(c.QueryEvents, msg, c, err)
} else {
for i, r := range ms.Responses {
for j, p := range r.PropStats {
if p.Prop == nil || p.Prop.CalendarData == nil {
continue
} else if cal, err := p.Prop.CalendarData.CalendarComponent(); err != nil {
msg := fmt.Sprintf("unable to decode property %d of response %d", j, i)
oerr = utils.NewError(c.QueryEvents, msg, c, err)
return
} else {
events = append(events, cal.Events...)
}
}
}
}
return
}
// executes a CalDAV request
func (c *Client) Do(req *Request) (*Response, error) {
if resp, err := c.WebDAV().Do((*webdav.Request)(req)); err != nil {
return nil, utils.NewError(c.Do, "unable to execute CalDAV request", c, err)
} else {
return NewResponse(resp), nil
}
}
// creates a new client for communicating with an WebDAV server
func NewClient(server *Server, native *http.Client) *Client {
return (*Client)(webdav.NewClient((*webdav.Server)(server), native))
}
// creates a new client for communicating with a WebDAV server
// uses the default HTTP client from net/http
func NewDefaultClient(server *Server) *Client {
return NewClient(server, http.DefaultClient)
}

View File

@ -0,0 +1,51 @@
package entities
import (
"encoding/xml"
"github.com/dolanor/caldav-go/caldav/values"
"github.com/dolanor/caldav-go/icalendar"
"github.com/dolanor/caldav-go/icalendar/components"
"github.com/dolanor/caldav-go/utils"
"strings"
)
// a CalDAV calendar data object
type CalendarData struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-data"`
Component *Component `xml:",omitempty"`
RecurrenceSetLimit *RecurrenceSetLimit `xml:",omitempty"`
ExpandRecurrenceSet *ExpandRecurrenceSet `xml:",omitempty"`
Content string `xml:",chardata"`
}
func (c *CalendarData) CalendarComponent() (*components.Calendar, error) {
cal := new(components.Calendar)
if content := strings.TrimSpace(c.Content); content == "" {
return nil, utils.NewError(c.CalendarComponent, "no calendar data to decode", c, nil)
} else if err := icalendar.Unmarshal(content, cal); err != nil {
return nil, utils.NewError(c.CalendarComponent, "decoding calendar data failed", c, err)
} else {
return cal, nil
}
}
// an iCalendar specifier for returned calendar data
type Component struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav comp"`
Properties []*PropertyName `xml:",omitempty"`
Components []*Component `xml:",omitempty"`
}
// used to restrict recurring event data to a particular time range
type RecurrenceSetLimit struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav limit-recurrence-set"`
StartTime *values.DateTime `xml:"start,attr"`
EndTime *values.DateTime `xml:"end,attr"`
}
// used to expand recurring events into individual calendar event data
type ExpandRecurrenceSet struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav expand"`
StartTime *values.DateTime `xml:"start,attr"`
EndTime *values.DateTime `xml:"end,attr"`
}

View File

@ -0,0 +1,59 @@
package entities
import (
"encoding/xml"
"github.com/dolanor/caldav-go/caldav/values"
"github.com/dolanor/caldav-go/utils"
"github.com/dolanor/caldav-go/webdav/entities"
"time"
)
// a CalDAV calendar query object
type CalendarQuery struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar-query"`
Prop *Prop `xml:",omitempty"`
AllProp *entities.AllProp `xml:",omitempty"`
Filter *Filter `xml:",omitempty"`
}
// creates a new CalDAV query for iCalendar events from a particular time range
func NewEventRangeQuery(start, end time.Time) (*CalendarQuery, error) {
var err error
var dtstart, dtend *values.DateTime
if dtstart, err = values.NewDateTime("start", start); err != nil {
return nil, utils.NewError(NewEventRangeQuery, "unable to encode start time", start, err)
} else if dtend, err = values.NewDateTime("end", end); err != nil {
return nil, utils.NewError(NewEventRangeQuery, "unable to encode end time", end, err)
}
// construct the query object
query := new(CalendarQuery)
// request all calendar data
query.Prop = new(Prop)
query.Prop.CalendarData = new(CalendarData)
// expand recurring events
query.Prop.CalendarData.ExpandRecurrenceSet = new(ExpandRecurrenceSet)
query.Prop.CalendarData.ExpandRecurrenceSet.StartTime = dtstart
query.Prop.CalendarData.ExpandRecurrenceSet.EndTime = dtend
// filter down calendar data to only iCalendar data
query.Filter = new(Filter)
query.Filter.ComponentFilter = new(ComponentFilter)
query.Filter.ComponentFilter.Name = values.CalendarComponentName
// filter down iCalendar data to only events
query.Filter.ComponentFilter.ComponentFilter = new(ComponentFilter)
query.Filter.ComponentFilter.ComponentFilter.Name = values.EventComponentName
// filter down the events to only those that fall within the time range
query.Filter.ComponentFilter.ComponentFilter.TimeRange = new(TimeRange)
query.Filter.ComponentFilter.ComponentFilter.TimeRange.StartTime = dtstart
query.Filter.ComponentFilter.ComponentFilter.TimeRange.EndTime = dtend
// return the event query
return query, nil
}

View File

@ -0,0 +1,62 @@
package entities
import (
"encoding/xml"
"github.com/dolanor/caldav-go/caldav/values"
"github.com/dolanor/caldav-go/icalendar/properties"
)
// a CalDAV query filter entity
type Filter struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav filter"`
ComponentFilter *ComponentFilter `xml:",omitempty"`
}
// used to filter down calendar components, such as VCALENDAR > VEVENT
type ComponentFilter struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav comp-filter"`
Name values.ComponentName `xml:"name,attr"`
ComponentFilter *ComponentFilter `xml:",omitempty"`
TimeRange *TimeRange `xml:",omitempty"`
PropertyFilter *PropertyFilter `xml:",omitempty"`
ParameterFilter *ParameterFilter `xml:",omitempty"`
}
// used to restrict component filters to a particular time range
type TimeRange struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav time-range"`
StartTime *values.DateTime `xml:"start,attr"`
EndTime *values.DateTime `xml:"end,attr"`
}
// used to restrict component filters to a property value
type PropertyFilter struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav prop-filter"`
Name properties.PropertyName `xml:"name,attr"`
TextMatch *TextMatch `xml:",omitempty"`
ParameterFilter *ParameterFilter `xml:",omitempty"`
}
// used to restrict component filters to a parameter value
type ParameterFilter struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav param-filter"`
Name properties.ParameterName `xml:"name,attr"`
TextMatch *TextMatch `xml:",omitempty"`
}
// used to match properties by text value
type TextMatch struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav text-match"`
Collation values.TextCollation `xml:"collation,attr,omitempty"`
NegateCondition values.HumanBoolean `xml:"attr,negate-condition,omitempty"`
Content string `xml:",innerxml"`
}
// creates a new CalDAV property value matcher
func NewPropertyMatcher(name properties.PropertyName, content string) *PropertyFilter {
pf := new(PropertyFilter)
pf.Name = name
pf.TextMatch = new(TextMatch)
pf.TextMatch.Content = content
return pf
}

View File

@ -0,0 +1,23 @@
package entities
import "encoding/xml"
// metadata about a property
type PropStat struct {
XMLName xml.Name `xml:"propstat"`
Status string `xml:"status"`
Prop *Prop `xml:",omitempty"`
}
// a multistatus response entity
type Response struct {
XMLName xml.Name `xml:"response"`
Href string `xml:"href"`
PropStats []*PropStat `xml:"propstat,omitempty"`
}
// a request to find properties on an an entity or collection
type Multistatus struct {
XMLName xml.Name `xml:"DAV: multistatus"`
Responses []*Response `xml:"response,omitempty"`
}

View File

@ -0,0 +1,23 @@
package entities
import (
"encoding/xml"
"github.com/dolanor/caldav-go/webdav/entities"
)
// a CalDAV Property resource
type Prop struct {
XMLName xml.Name `xml:"DAV: prop"`
GetContentType string `xml:"getcontenttype,omitempty"`
DisplayName string `xml:"displayname,omitempty"`
CalendarData *CalendarData `xml:",omitempty"`
ResourceType *entities.ResourceType `xml:",omitempty"`
CTag string `xml:"http://calendarserver.org/ns/ getctag,omitempty"`
ETag string `xml:"http://calendarserver.org/ns/ getetag,omitempty"`
}
// used to restrict properties returned in calendar data
type PropertyName struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav prop"`
Name string `xml:"name,attr"`
}

56
vendor/github.com/dolanor/caldav-go/caldav/request.go generated vendored Normal file
View File

@ -0,0 +1,56 @@
package caldav
import (
"bytes"
"github.com/dolanor/caldav-go/http"
"github.com/dolanor/caldav-go/icalendar"
"github.com/dolanor/caldav-go/utils"
"github.com/dolanor/caldav-go/webdav"
"io"
"io/ioutil"
"log"
"strings"
)
var _ = log.Print
// an CalDAV request object
type Request webdav.Request
// downcasts the request to the WebDAV interface
func (r *Request) WebDAV() *webdav.Request {
return (*webdav.Request)(r)
}
// creates a new CalDAV request object
func NewRequest(method string, urlstr string, icaldata ...interface{}) (*Request, error) {
if buffer, err := icalToReadCloser(icaldata...); err != nil {
return nil, utils.NewError(NewRequest, "unable to encode icalendar data", icaldata, err)
} else if r, err := http.NewRequest(method, urlstr, buffer); err != nil {
return nil, utils.NewError(NewRequest, "unable to create request", urlstr, err)
} else {
if buffer != nil {
// set the content type to XML if we have a body
r.Native().Header.Set("Content-Type", "text/calendar; charset=UTF-8")
}
return (*Request)(r), nil
}
}
func icalToReadCloser(icaldata ...interface{}) (io.ReadCloser, error) {
var buffer []string
for _, icaldatum := range icaldata {
if encoded, err := icalendar.Marshal(icaldatum); err != nil {
return nil, utils.NewError(icalToReadCloser, "unable to encode as icalendar data", icaldatum, err)
} else {
// log.Printf("OUT: %+v", encoded)
buffer = append(buffer, encoded)
}
}
if len(buffer) > 0 {
var encoded = strings.Join(buffer, "\n")
return ioutil.NopCloser(bytes.NewBuffer([]byte(encoded))), nil
} else {
return nil, nil
}
}

40
vendor/github.com/dolanor/caldav-go/caldav/response.go generated vendored Normal file
View File

@ -0,0 +1,40 @@
package caldav
import (
"github.com/dolanor/caldav-go/icalendar"
"github.com/dolanor/caldav-go/utils"
"github.com/dolanor/caldav-go/webdav"
"io/ioutil"
"log"
)
var _ = log.Print
// a WebDAV response object
type Response webdav.Response
// downcasts the response to the WebDAV interface
func (r *Response) WebDAV() *webdav.Response {
return (*webdav.Response)(r)
}
// decodes a CalDAV iCalendar response into the provided interface
func (r *Response) Decode(into interface{}) error {
if body := r.Body; body == nil {
return nil
} else if encoded, err := ioutil.ReadAll(body); err != nil {
return utils.NewError(r.Decode, "unable to read response body", r, err)
} else {
// log.Printf("IN: %+v", string(encoded))
if err := icalendar.Unmarshal(string(encoded), into); err != nil {
return utils.NewError(r.Decode, "unable to decode response body", r, err)
} else {
return nil
}
}
}
// creates a new WebDAV response object
func NewResponse(response *webdav.Response) *Response {
return (*Response)(response)
}

30
vendor/github.com/dolanor/caldav-go/caldav/server.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
package caldav
import (
"github.com/dolanor/caldav-go/utils"
"github.com/dolanor/caldav-go/webdav"
)
// a server that accepts CalDAV requests
type Server webdav.Server
// NewServer creates a reference to a CalDAV server.
// host is the url to access the server, stopping at the port:
// https://user:password@host:port/
func NewServer(host string) (*Server, error) {
if s, err := webdav.NewServer(host); err != nil {
return nil, utils.NewError(NewServer, "unable to create WebDAV server", host, err)
} else {
return (*Server)(s), nil
}
}
// downcasts the server to the WebDAV interface
func (s *Server) WebDAV() *webdav.Server {
return (*webdav.Server)(s)
}
// creates a new CalDAV request object
func (s *Server) NewRequest(method string, path string, icaldata ...interface{}) (*Request, error) {
return NewRequest(method, s.WebDAV().Http().AbsUrlStr(path), icaldata...)
}

View File

@ -0,0 +1,8 @@
package values
type ComponentName string
const (
CalendarComponentName ComponentName = "VCALENDAR"
EventComponentName = "VEVENT"
)

View File

@ -0,0 +1,31 @@
package values
import (
"encoding/xml"
"errors"
"github.com/dolanor/caldav-go/icalendar/values"
"time"
)
// a representation of a date and time for iCalendar
type DateTime struct {
name string
t time.Time
}
// creates a new caldav datetime representation, must be in UTC
func NewDateTime(name string, t time.Time) (*DateTime, error) {
if t.Location() != time.UTC {
return nil, errors.New("CalDAV datetime must be in UTC")
} else {
return &DateTime{name: name, t: t.Truncate(time.Second)}, nil
}
}
// encodes the datetime value for the iCalendar specification
func (d *DateTime) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
layout := values.UTCDateTimeFormatString
value := d.t.Format(layout)
attr := xml.Attr{Name: name, Value: value}
return attr, nil
}

View File

@ -0,0 +1,8 @@
package values
type HumanBoolean string
const (
YesHumanBoolean HumanBoolean = "yes"
NoHumanBoolean = "no"
)

View File

@ -0,0 +1,8 @@
package values
type TextCollation string
const (
OctetTextCollation TextCollation = "i;octet"
ASCIICaseMapCollation = "i;ascii-casemap"
)

53
vendor/github.com/dolanor/caldav-go/http/client.go generated vendored Normal file
View File

@ -0,0 +1,53 @@
package http
import (
"github.com/dolanor/caldav-go/utils"
"net/http"
)
// a client for making HTTP requests
type Client struct {
native *http.Client
server *Server
requestHeaders map[string]string
}
func (c *Client) SetHeader(key string, value string) {
if c.requestHeaders == nil {
c.requestHeaders = map[string]string{}
}
c.requestHeaders[key] = value
}
// downcasts to the native HTTP interface
func (c *Client) Native() *http.Client {
return c.native
}
// returns the embedded HTTP server reference
func (c *Client) Server() *Server {
return c.server
}
// executes an HTTP request
func (c *Client) Do(req *Request) (*Response, error) {
for key, value := range c.requestHeaders {
req.Header.Add(key, value)
}
if resp, err := c.Native().Do((*http.Request)(req)); err != nil {
return nil, utils.NewError(c.Do, "unable to execute HTTP request", c, err)
} else {
return NewResponse(resp), nil
}
}
// creates a new client for communicating with an HTTP server
func NewClient(server *Server, native *http.Client) *Client {
return &Client{server: server, native: native}
}
// creates a new client for communicating with a server
// uses the default HTTP client from net/http
func NewDefaultClient(server *Server) *Client {
return NewClient(server, http.DefaultClient)
}

39
vendor/github.com/dolanor/caldav-go/http/request.go generated vendored Normal file
View File

@ -0,0 +1,39 @@
package http
import (
"github.com/dolanor/caldav-go/utils"
"io"
"net/http"
)
// an HTTP request object
type Request http.Request
// downcasts the request to the native HTTP interface
func (r *Request) Native() *http.Request {
return (*http.Request)(r)
}
// creates a new HTTP request object
func NewRequest(method string, urlstr string, body ...io.ReadCloser) (*Request, error) {
var err error
var r = new(http.Request)
if len(body) > 0 && body[0] != nil {
r, err = http.NewRequest(method, urlstr, body[0])
} else {
r, err = http.NewRequest(method, urlstr, nil)
}
if err != nil {
return nil, utils.NewError(NewRequest, "unable to create request", urlstr, err)
} else if auth := r.URL.User; auth != nil {
pass, _ := auth.Password()
r.SetBasicAuth(auth.Username(), pass)
r.URL.User = nil
}
return (*Request)(r), nil
}

18
vendor/github.com/dolanor/caldav-go/http/response.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
package http
import (
"net/http"
)
// an HTTP response object
type Response http.Response
// downcasts the response to the native HTTP interface
func (r *Response) Native() *http.Response {
return (*http.Response)(r)
}
// creates a new HTTP response object
func NewResponse(response *http.Response) *Response {
return (*Response)(response)
}

48
vendor/github.com/dolanor/caldav-go/http/server.go generated vendored Normal file
View File

@ -0,0 +1,48 @@
package http
import (
"github.com/dolanor/caldav-go/utils"
"io"
"log"
"net/url"
spath "path"
"strings"
)
var _ = log.Print
// a server that accepts HTTP requests
type Server struct {
baseUrl *url.URL
}
// creates a reference to an http server
func NewServer(baseUrlStr string) (*Server, error) {
var err error
var s = new(Server)
if s.baseUrl, err = url.Parse(baseUrlStr); err != nil {
return nil, utils.NewError(NewServer, "unable to parse server base url", baseUrlStr, err)
} else {
return s, nil
}
}
// converts a path name to an absolute URL
func (s *Server) UserInfo() *url.Userinfo {
return s.baseUrl.User
}
// converts a path name to an absolute URL
func (s *Server) AbsUrlStr(path string) string {
uri := *s.baseUrl
uri.Path = spath.Join(uri.Path, path)
if strings.HasSuffix(path, "/") {
uri.Path = uri.Path + "/"
}
return uri.String()
}
// creates a new HTTP request object
func (s *Server) NewRequest(method string, path string, body ...io.ReadCloser) (*Request, error) {
return NewRequest(method, s.AbsUrlStr(path), body...)
}

View File

@ -0,0 +1,87 @@
package components
import (
"fmt"
"github.com/dolanor/caldav-go/icalendar/values"
"github.com/dolanor/caldav-go/utils"
"time"
)
type Calendar struct {
// specifies the identifier corresponding to the highest version number or the minimum and maximum
// range of the iCalendar specification that is required in order to interpret the iCalendar object.
Version string `ical:",2.0"`
// specifies the identifier for the product that created the iCalendar object
ProductId string `ical:"prodid,-//dolanor/caldav-go//NONSGML v1.0.0//EN"`
// specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
TimeZoneId string `ical:"tzid,omitempty"`
// defines the iCalendar object method associated with the calendar object.
Method values.Method `ical:",omitempty"`
// defines the calendar scale used for the calendar information specified in the iCalendar object.
CalScale values.CalScale `ical:",omitempty"`
// defines the different timezones used by the various components nested within
TimeZones []*TimeZone `ical:",omitempty"`
// unique events to be stored together in the icalendar file
Events []*Event `ical:",omitempty"`
}
func (c *Calendar) UseTimeZone(location *time.Location) *TimeZone {
tz := NewDynamicTimeZone(location)
c.TimeZones = append(c.TimeZones, tz)
c.TimeZoneId = tz.Id
return tz
}
func (c *Calendar) UsingTimeZone() bool {
return len(c.TimeZoneId) > 0
}
func (c *Calendar) UsingGlobalTimeZone() bool {
return c.UsingTimeZone() && c.TimeZoneId[0] == '/'
}
func (c *Calendar) ValidateICalValue() error {
for i, e := range c.Events {
if e == nil {
continue // skip nil events
}
if err := e.ValidateICalValue(); err != nil {
msg := fmt.Sprintf("event %d failed validation", i)
return utils.NewError(c.ValidateICalValue, msg, c, err)
}
if e.DateStart == nil && c.Method == "" {
msg := fmt.Sprintf("no value for method and no start date defined on event %d", i)
return utils.NewError(c.ValidateICalValue, msg, c, nil)
}
}
if c.UsingTimeZone() && !c.UsingGlobalTimeZone() {
for i, t := range c.TimeZones {
if t == nil || t.Id != c.TimeZoneId {
msg := fmt.Sprintf("timezone ID does not match timezone %d", i)
return utils.NewError(c.ValidateICalValue, msg, c, nil)
}
}
}
return nil
}
func NewCalendar(events ...*Event) *Calendar {
cal := new(Calendar)
cal.Events = events
return cal
}

View File

@ -0,0 +1,172 @@
package components
import (
"github.com/dolanor/caldav-go/icalendar/values"
"github.com/dolanor/caldav-go/utils"
"time"
)
type Event struct {
// defines the persistent, globally unique identifier for the calendar component.
UID string `ical:",required"`
// indicates the date/time that the instance of the iCalendar object was created.
DateStamp *values.DateTime `ical:"dtstamp,required"`
// specifies when the calendar component begins.
DateStart *values.DateTime `ical:"dtstart,required"`
// specifies the date and time that a calendar component ends.
DateEnd *values.DateTime `ical:"dtend,omitempty"`
// specifies a positive duration of time.
Duration *values.Duration `ical:",omitempty"`
// defines the access classification for a calendar component.
AccessClassification values.EventAccessClassification `ical:"class,omitempty"`
// specifies the date and time that the calendar information was created by the calendar user agent in the
// calendar store.
// Note: This is analogous to the creation date and time for a file in the file system.
Created *values.DateTime `ical:",omitempty"`
// provides a more complete description of the calendar component, than that provided by the Summary property.
Description string `ical:",omitempty"`
// specifies information related to the global position for the activity specified by a calendar component.
Geo *values.Geo `ical:",omitempty"`
// specifies the date and time that the information associated with the calendar component was last revised in the
// calendar store.
// Note: This is analogous to the modification date and time for a file in the file system.
LastModified *values.DateTime `ical:"last-modified,omitempty"`
// defines the intended venue for the activity defined by a calendar component.
Location *values.Location `ical:",omitempty"`
// defines the organizer for a calendar component.
Organizer *values.OrganizerContact `ical:",omitempty"`
// defines the relative priority for a calendar component.
Priority int `ical:",omitempty"`
// defines the revision sequence number of the calendar component within a sequence of revisions.
Sequence int `ical:",omitempty"`
// efines the overall status or confirmation for the calendar component.
Status values.EventStatus `ical:",omitempty"`
// defines a short summary or subject for the calendar component.
Summary string `ical:",omitempty"`
// defines whether an event is transparent or not to busy time searches.
values.TimeTransparency `ical:"transp,omitempty"`
// defines a Uniform Resource Locator (URL) associated with the iCalendar object.
Url *values.Url `ical:",omitempty"`
// used in conjunction with the "UID" and "SEQUENCE" property to identify a specific instance of a recurring
// event calendar component. The property value is the effective value of the DateStart property of the
// recurrence instance.
RecurrenceId *values.DateTime `ical:"recurrence_id,omitempty"`
// defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
RecurrenceRules []*values.RecurrenceRule `ical:",omitempty"`
// property provides the capability to associate a document object with a calendar component.
Attachment *values.Url `ical:"attach,omitempty"`
// defines an "Attendee" within a calendar component.
Attendees []*values.AttendeeContact `ical:",omitempty"`
// defines the categories for a calendar component.
Categories *values.CSV `ical:",omitempty"`
// specifies non-processing information intended to provide a comment to the calendar user.
Comments []values.Comment `ical:",omitempty"`
// used to represent contact information or alternately a reference to contact information associated with the calendar component.
ContactInfo *values.CSV `ical:"contact,omitempty"`
// defines the list of date/time exceptions for a recurring calendar component.
*values.ExceptionDateTimes `ical:",omitempty"`
// defines the list of date/times for a recurrence set.
*values.RecurrenceDateTimes `ical:",omitempty"`
// used to represent a relationship or reference between one calendar component and another.
RelatedTo *values.Url `ical:"related-to,omitempty"`
// defines the equipment or resources anticipated for an activity specified by a calendar entity.
Resources *values.CSV `ical:",omitempty"`
}
// validates the event internals
func (e *Event) ValidateICalValue() error {
if e.UID == "" {
return utils.NewError(e.ValidateICalValue, "the UID value must be set", e, nil)
}
if e.DateStart == nil {
return utils.NewError(e.ValidateICalValue, "event start date must be set", e, nil)
}
if e.DateEnd == nil && e.Duration == nil {
return utils.NewError(e.ValidateICalValue, "event end date or duration must be set", e, nil)
}
if e.DateEnd != nil && e.Duration != nil {
return utils.NewError(e.ValidateICalValue, "event end date and duration are mutually exclusive fields", e, nil)
}
return nil
}
// adds one or more recurrence rule to the event
func (e *Event) AddRecurrenceRules(r ...*values.RecurrenceRule) {
e.RecurrenceRules = append(e.RecurrenceRules, r...)
}
// adds one or more recurrence rule exception to the event
func (e *Event) AddRecurrenceExceptions(d ...*values.DateTime) {
if e.ExceptionDateTimes == nil {
e.ExceptionDateTimes = new(values.ExceptionDateTimes)
}
*e.ExceptionDateTimes = append(*e.ExceptionDateTimes, d...)
}
// checks to see if the event is a recurrence
func (e *Event) IsRecurrence() bool {
return e.RecurrenceId != nil
}
// checks to see if the event is a recurrence override
func (e *Event) IsOverride() bool {
return e.IsRecurrence() && !e.RecurrenceId.Equals(e.DateStart)
}
// creates a new iCalendar event with no end time
func NewEvent(uid string, start time.Time) *Event {
e := new(Event)
e.UID = uid
e.DateStamp = values.NewDateTime(time.Now().UTC())
e.DateStart = values.NewDateTime(start)
return e
}
// creates a new iCalendar event that lasts a certain duration
func NewEventWithDuration(uid string, start time.Time, duration time.Duration) *Event {
e := NewEvent(uid, start)
e.Duration = values.NewDuration(duration)
return e
}
// creates a new iCalendar event that has an explicit start and end time
func NewEventWithEnd(uid string, start time.Time, end time.Time) *Event {
e := NewEvent(uid, start)
e.DateEnd = values.NewDateTime(end)
return e
}

View File

@ -0,0 +1,40 @@
package components
import (
"fmt"
"github.com/dolanor/caldav-go/icalendar/values"
"net/url"
"time"
)
type TimeZone struct {
// defines the persistent, globally unique identifier for the calendar component.
Id string `ical:"tzid,required"`
// the location name, as defined by the standards body
ExtLocationName string `ical:"x-lic-location,omitempty"`
// defines a Uniform Resource Locator (URL) associated with the iCalendar object.
Url *values.Url `ical:"tzurl,omitempty"`
// specifies the date and time that the information associated with the calendar component was last revised in the
// calendar store.
// Note: This is analogous to the modification date and time for a file in the file system.
LastModified *values.DateTime `ical:"last-modified,omitempty"`
// TODO need to figure out how to handle standard and daylight savings time
}
func NewDynamicTimeZone(location *time.Location) *TimeZone {
t := new(TimeZone)
t.Id = location.String()
t.ExtLocationName = location.String()
t.Url = values.NewUrl(url.URL{
Scheme: "http",
Host: "tzurl.org",
Path: fmt.Sprintf("/zoneinfo/%s", t.Id),
})
return t
}

View File

@ -0,0 +1,196 @@
package icalendar
import (
"fmt"
"github.com/dolanor/caldav-go/icalendar/properties"
"github.com/dolanor/caldav-go/utils"
"log"
"reflect"
"strings"
)
const (
Newline = "\r\n"
)
var _ = log.Print
type encoder func(reflect.Value) (string, error)
func tagAndJoinValue(v reflect.Value, in []string) (string, error) {
if tag, err := extractTagFromValue(v); err != nil {
return "", utils.NewError(tagAndJoinValue, "unable to extract tag from value", v, err)
} else {
var out []string
out = append(out, properties.MarshalProperty(properties.NewProperty("begin", tag)))
out = append(out, in...)
out = append(out, properties.MarshalProperty(properties.NewProperty("end", tag)))
return strings.Join(out, Newline), nil
}
}
func marshalCollection(v reflect.Value) (string, error) {
var out []string
for i, n := 0, v.Len(); i < n; i++ {
vi := v.Index(i).Interface()
if encoded, err := Marshal(vi); err != nil {
msg := fmt.Sprintf("unable to encode interface at index %d", i)
return "", utils.NewError(marshalCollection, msg, vi, err)
} else if encoded != "" {
out = append(out, encoded)
}
}
return strings.Join(out, Newline), nil
}
func marshalStruct(v reflect.Value) (string, error) {
var out []string
// iterate over all fields
vtype := v.Type()
n := vtype.NumField()
for i := 0; i < n; i++ {
// keep a reference to the field value and definition
fv := v.Field(i)
fs := vtype.Field(i)
// use the field definition to extract out property defaults
p := properties.PropertyFromStructField(fs)
if p == nil {
continue // skip explicitly ignored fields and private members
}
fi := fv.Interface()
// some fields are not properties, but actually nested objects.
// detect those early using the property and object encoder...
if _, ok := fi.(properties.CanEncodeValue); !ok && !isInvalidOrEmptyValue(fv) {
if encoded, err := encode(fv, objectEncoder); err != nil {
msg := fmt.Sprintf("unable to encode field %s", fs.Name)
return "", utils.NewError(marshalStruct, msg, v.Interface(), err)
} else if encoded != "" {
// encoding worked! no need to process as a property
out = append(out, encoded)
continue
}
}
// now check to see if the field value overrides the defaults...
if !isInvalidOrEmptyValue(fv) {
// first, check the field value interface for overrides...
if overrides, err := properties.PropertyFromInterface(fi); err != nil {
msg := fmt.Sprintf("field %s failed validation", fs.Name)
return "", utils.NewError(marshalStruct, msg, v.Interface(), err)
} else if p.Merge(overrides); p.Value == "" {
// then, if we couldn't find an override from the interface,
// try the simple string encoder...
if p.Value, err = stringEncoder(fv); err != nil {
msg := fmt.Sprintf("unable to encode field %s", fs.Name)
return "", utils.NewError(marshalStruct, msg, v.Interface(), err)
}
}
}
// make sure we have a value by this point
if !p.HasNameAndValue() {
if p.OmitEmpty {
continue
} else if p.DefaultValue != "" {
p.Value = p.DefaultValue
} else if p.Required {
msg := fmt.Sprintf("missing value for required field %s", fs.Name)
return "", utils.NewError(Marshal, msg, v.Interface(), nil)
}
}
// encode in the property
out = append(out, properties.MarshalProperty(p))
}
// wrap the fields in the enclosing struct tags
return tagAndJoinValue(v, out)
}
func objectEncoder(v reflect.Value) (string, error) {
// decompose the value into its interface parts
v = dereferencePointerValue(v)
// encode the value based off of its type
switch v.Kind() {
case reflect.Slice:
fallthrough
case reflect.Array:
return marshalCollection(v)
case reflect.Struct:
return marshalStruct(v)
}
return "", nil
}
func stringEncoder(v reflect.Value) (string, error) {
return fmt.Sprintf("%v", v.Interface()), nil
}
func propertyEncoder(v reflect.Value) (string, error) {
vi := v.Interface()
if p, err := properties.PropertyFromInterface(vi); err != nil {
// return early if interface fails its own validation
return "", err
} else if p.HasNameAndValue() {
// if an interface encodes its own name and value, it's a property
return properties.MarshalProperty(p), nil
}
return "", nil
}
func encode(v reflect.Value, encoders ...encoder) (string, error) {
for _, encode := range encoders {
if encoded, err := encode(v); err != nil {
return "", err
} else if encoded != "" {
return encoded, nil
}
}
return "", nil
}
// converts an iCalendar component into its string representation
func Marshal(target interface{}) (string, error) {
// don't do anything with invalid interfaces
v := reflect.ValueOf(target)
if isInvalidOrEmptyValue(v) {
return "", utils.NewError(Marshal, "unable to marshal empty or invalid values", target, nil)
}
if encoded, err := encode(v, propertyEncoder, objectEncoder, stringEncoder); err != nil {
return "", err
} else if encoded == "" {
return "", utils.NewError(Marshal, "unable to encode interface, all methods exhausted", v.Interface(), nil)
} else {
return encoded, nil
}
}

View File

@ -0,0 +1,29 @@
package properties
type CanValidateValue interface {
ValidateICalValue() error
}
type CanDecodeValue interface {
DecodeICalValue(string) error
}
type CanDecodeParams interface {
DecodeICalParams(Params) error
}
type CanEncodeTag interface {
EncodeICalTag() (string, error)
}
type CanEncodeValue interface {
EncodeICalValue() (string, error)
}
type CanEncodeName interface {
EncodeICalName() (PropertyName, error)
}
type CanEncodeParams interface {
EncodeICalParams() (Params, error)
}

View File

@ -0,0 +1,31 @@
package properties
import "strings"
type PropertyName string
const (
UIDPropertyName PropertyName = "UID"
CommentPropertyName = "COMMENT"
OrganizerPropertyName = "ORGANIZER"
AttendeePropertyName = "ATTENDEE"
ExceptionDateTimesPropertyName = "EXDATE"
RecurrenceDateTimesPropertyName = "RDATE"
RecurrenceRulePropertyName = "RRULE"
LocationPropertyName = "LOCATION"
)
type ParameterName string
const (
CanonicalNameParameterName ParameterName = "CN"
TimeZoneIdPropertyName = "TZID"
ValuePropertyName = "VALUE"
AlternateRepresentationName = "ALTREP"
)
type Params map[ParameterName]string
func (p PropertyName) Equals(test string) bool {
return strings.EqualFold(string(p), test)
}

View File

@ -0,0 +1,177 @@
package properties
import (
"fmt"
"github.com/dolanor/caldav-go/utils"
"log"
"reflect"
"strings"
)
var _ = log.Print
var propNameSanitizer = strings.NewReplacer(
"_", "-",
":", "\\:",
)
var propValueSanitizer = strings.NewReplacer(
"\"", "'",
"\\", "\\\\",
"\n", "\\n",
)
var propNameDesanitizer = strings.NewReplacer(
"-", "_",
"\\:", ":",
)
var propValueDesanitizer = strings.NewReplacer(
"'", "\"",
"\\\\", "\\",
"\\n", "\n",
)
type Property struct {
Name PropertyName
Value, DefaultValue string
Params Params
OmitEmpty, Required bool
}
func (p *Property) HasNameAndValue() bool {
return p.Name != "" && p.Value != ""
}
func (p *Property) Merge(override *Property) {
if override.Name != "" {
p.Name = override.Name
}
if override.Value != "" {
p.Value = override.Value
}
if override.Params != nil {
p.Params = override.Params
}
}
func PropertyFromStructField(fs reflect.StructField) (p *Property) {
ftag := fs.Tag.Get("ical")
if fs.PkgPath != "" || ftag == "-" {
return
}
p = new(Property)
// parse the field tag
if ftag != "" {
tags := strings.Split(ftag, ",")
p.Name = PropertyName(tags[0])
if len(tags) > 1 {
if tags[1] == "omitempty" {
p.OmitEmpty = true
} else if tags[1] == "required" {
p.Required = true
} else {
p.DefaultValue = tags[1]
}
}
}
// make sure we have a name
if p.Name == "" {
p.Name = PropertyName(fs.Name)
}
p.Name = PropertyName(strings.ToUpper(string(p.Name)))
return
}
func MarshalProperty(p *Property) string {
name := strings.ToUpper(propNameSanitizer.Replace(string(p.Name)))
value := propValueSanitizer.Replace(p.Value)
keys := []string{name}
for name, value := range p.Params {
name = ParameterName(strings.ToUpper(propNameSanitizer.Replace(string(name))))
value = propValueSanitizer.Replace(value)
if strings.ContainsAny(value, " :") {
keys = append(keys, fmt.Sprintf("%s=\"%s\"", name, value))
} else {
keys = append(keys, fmt.Sprintf("%s=%s", name, value))
}
}
name = strings.Join(keys, ";")
return fmt.Sprintf("%s:%s", name, value)
}
func PropertyFromInterface(target interface{}) (p *Property, err error) {
var ierr error
if va, ok := target.(CanValidateValue); ok {
if ierr = va.ValidateICalValue(); ierr != nil {
err = utils.NewError(PropertyFromInterface, "interface failed validation", target, ierr)
return
}
}
p = new(Property)
if enc, ok := target.(CanEncodeName); ok {
if p.Name, ierr = enc.EncodeICalName(); ierr != nil {
err = utils.NewError(PropertyFromInterface, "interface failed name encoding", target, ierr)
return
}
}
if enc, ok := target.(CanEncodeParams); ok {
if p.Params, ierr = enc.EncodeICalParams(); ierr != nil {
err = utils.NewError(PropertyFromInterface, "interface failed params encoding", target, ierr)
return
}
}
if enc, ok := target.(CanEncodeValue); ok {
if p.Value, ierr = enc.EncodeICalValue(); ierr != nil {
err = utils.NewError(PropertyFromInterface, "interface failed value encoding", target, ierr)
return
}
}
return
}
func UnmarshalProperty(line string) *Property {
nvp := strings.SplitN(line, ":", 2)
prop := new(Property)
if len(nvp) > 1 {
prop.Value = strings.TrimSpace(nvp[1])
}
npp := strings.Split(nvp[0], ";")
if len(npp) > 1 {
prop.Params = make(map[ParameterName]string, 0)
for i := 1; i < len(npp); i++ {
var key, value string
kvp := strings.Split(npp[i], "=")
key = strings.TrimSpace(kvp[0])
key = propNameDesanitizer.Replace(key)
if len(kvp) > 1 {
value = strings.TrimSpace(kvp[1])
value = propValueDesanitizer.Replace(value)
value = strings.Trim(value, "\"")
}
prop.Params[ParameterName(key)] = value
}
}
prop.Name = PropertyName(strings.TrimSpace(npp[0]))
prop.Name = PropertyName(propNameDesanitizer.Replace(string(prop.Name)))
prop.Value = propValueDesanitizer.Replace(prop.Value)
return prop
}
func NewProperty(name, value string) *Property {
return &Property{Name: PropertyName(name), Value: value}
}

View File

@ -0,0 +1,78 @@
package icalendar
import (
"fmt"
"github.com/dolanor/caldav-go/icalendar/properties"
"github.com/dolanor/caldav-go/utils"
"log"
"reflect"
"strings"
)
var _ = log.Print
func isInvalidOrEmptyValue(v reflect.Value) bool {
if !v.IsValid() {
return true
}
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}
func newValue(in reflect.Value) (out reflect.Value, isArrayElement bool) {
typ := in.Type()
kind := typ.Kind()
for {
if kind == reflect.Array || kind == reflect.Slice {
isArrayElement = true
} else if kind != reflect.Ptr {
break
}
typ = typ.Elem()
kind = typ.Kind()
}
out = reflect.New(typ)
return
}
func dereferencePointerValue(v reflect.Value) reflect.Value {
for (v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr) && v.Elem().IsValid() {
v = v.Elem()
}
return v
}
func extractTagFromValue(v reflect.Value) (string, error) {
vdref := dereferencePointerValue(v)
vtemp, _ := newValue(vdref)
if encoder, ok := vtemp.Interface().(properties.CanEncodeTag); ok {
if tag, err := encoder.EncodeICalTag(); err != nil {
return "", utils.NewError(extractTagFromValue, "unable to extract tag from interface", v.Interface(), err)
} else {
return strings.ToUpper(tag), nil
}
} else {
typ := vtemp.Elem().Type()
return strings.ToUpper(fmt.Sprintf("v%s", typ.Name())), nil
}
}

View File

@ -0,0 +1,365 @@
package icalendar
import (
"fmt"
"github.com/dolanor/caldav-go/icalendar/properties"
"github.com/dolanor/caldav-go/utils"
"log"
"reflect"
"regexp"
"strconv"
"strings"
)
var _ = log.Print
var splitter = regexp.MustCompile("\r?\n")
type token struct {
name string
components map[string][]*token
properties map[properties.PropertyName][]*properties.Property
}
func tokenize(encoded string) (*token, error) {
if encoded = strings.TrimSpace(encoded); encoded == "" {
return nil, utils.NewError(tokenize, "no content to tokenize", encoded, nil)
}
return tokenizeSlice(splitter.Split(encoded, -1))
}
func tokenizeSlice(slice []string, name ...string) (*token, error) {
tok := new(token)
size := len(slice)
if len(name) > 0 {
tok.name = name[0]
} else if size <= 0 {
return nil, utils.NewError(tokenizeSlice, "token has no content", slice, nil)
}
tok.properties = make(map[properties.PropertyName][]*properties.Property, 0)
tok.components = make(map[string][]*token, 0)
for i := 0; i < size; i++ {
// Handle iCalendar's space-indented line break format
// See: https://www.ietf.org/rfc/rfc2445.txt section 4.1
// "a long line can be split between any two characters by inserting a CRLF immediately followed by a single
// linear white space character"
line := slice[i]
for ; i < size-1 && strings.HasPrefix(slice[i+1], " "); i++ {
next := slice[i+1]
line += next[1:len(next)]
}
prop := properties.UnmarshalProperty(line)
if prop.Name.Equals("begin") {
for j := i; j < size; j++ {
end := strings.Replace(line, "BEGIN", "END", 1)
if slice[j] == end {
if component, err := tokenizeSlice(slice[i+1:j], prop.Value); err != nil {
msg := fmt.Sprintf("unable to tokenize %s component", prop.Value)
return nil, utils.NewError(tokenizeSlice, msg, slice, err)
} else {
existing, _ := tok.components[prop.Value]
tok.components[prop.Value] = append(existing, component)
i = j
break
}
}
}
} else if existing, ok := tok.properties[prop.Name]; ok {
tok.properties[prop.Name] = []*properties.Property{prop}
} else {
tok.properties[prop.Name] = append(existing, prop)
}
}
return tok, nil
}
func hydrateInterface(v reflect.Value, prop *properties.Property) (bool, error) {
// unable to decode into empty values
if isInvalidOrEmptyValue(v) {
return false, nil
}
var i = v.Interface()
var hasValue = false
// decode a value if possible
if decoder, ok := i.(properties.CanDecodeValue); ok {
if err := decoder.DecodeICalValue(prop.Value); err != nil {
return false, utils.NewError(hydrateInterface, "error decoding property value", v, err)
} else {
hasValue = true
}
}
// decode any params, if supported
if len(prop.Params) > 0 {
if decoder, ok := i.(properties.CanDecodeParams); ok {
if err := decoder.DecodeICalParams(prop.Params); err != nil {
return false, utils.NewError(hydrateInterface, "error decoding property parameters", v, err)
}
}
}
// finish with any validation
if validator, ok := i.(properties.CanValidateValue); ok {
if err := validator.ValidateICalValue(); err != nil {
return false, utils.NewError(hydrateInterface, "error validating property value", v, err)
}
}
return hasValue, nil
}
func hydrateLiteral(v reflect.Value, prop *properties.Property) (reflect.Value, error) {
literal := dereferencePointerValue(v)
switch literal.Kind() {
case reflect.Bool:
if i, err := strconv.ParseBool(prop.Value); err != nil {
return literal, utils.NewError(hydrateLiteral, "unable to decode bool "+prop.Value, literal.Interface(), err)
} else {
literal.SetBool(i)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if i, err := strconv.ParseInt(prop.Value, 10, 64); err != nil {
return literal, utils.NewError(hydrateLiteral, "unable to decode int "+prop.Value, literal.Interface(), err)
} else {
literal.SetInt(i)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
if i, err := strconv.ParseUint(prop.Value, 10, 64); err != nil {
return literal, utils.NewError(hydrateLiteral, "unable to decode uint "+prop.Value, literal.Interface(), err)
} else {
literal.SetUint(i)
}
case reflect.Float32, reflect.Float64:
if i, err := strconv.ParseFloat(prop.Value, 64); err != nil {
return literal, utils.NewError(hydrateLiteral, "unable to decode float "+prop.Value, literal.Interface(), err)
} else {
literal.SetFloat(i)
}
case reflect.String:
literal.SetString(prop.Value)
default:
return literal, utils.NewError(hydrateLiteral, "unable to decode value as literal "+prop.Value, literal.Interface(), nil)
}
return literal, nil
}
func hydrateProperty(v reflect.Value, prop *properties.Property) error {
// check to see if the interface handles it's own hydration
if handled, err := hydrateInterface(v, prop); err != nil {
return utils.NewError(hydrateProperty, "unable to hydrate interface", v, err)
} else if handled {
return nil // exit early if handled by the interface
}
// if we got here, we need to create a new instance to
// set into the property.
var vnew, varr = newValue(v)
var vlit bool
// check to see if the new value handles it's own hydration
if handled, err := hydrateInterface(vnew, prop); err != nil {
return utils.NewError(hydrateProperty, "unable to hydrate new interface value", vnew, err)
} else if vlit = !handled; vlit {
// if not, treat it as a literal
if vnewlit, err := hydrateLiteral(vnew, prop); err != nil {
return utils.NewError(hydrateProperty, "unable to hydrate new literal value", vnew, err)
} else if _, err := hydrateInterface(vnewlit, prop); err != nil {
return utils.NewError(hydrateProperty, "unable to hydrate new literal interface value", vnewlit, err)
}
}
// now we can set the value
vnewval := dereferencePointerValue(vnew)
voldval := dereferencePointerValue(v)
// make sure we can set the new value into the provided pointer
if varr {
// for arrays, append the new value into the array structure
if !voldval.CanSet() {
return utils.NewError(hydrateProperty, "unable to set array value", v, nil)
} else {
voldval.Set(reflect.Append(voldval, vnewval))
}
} else if vlit {
// for literals, set the dereferenced value
if !voldval.CanSet() {
return utils.NewError(hydrateProperty, "unable to set literal value", v, nil)
} else {
voldval.Set(vnewval)
}
} else if !v.CanSet() {
return utils.NewError(hydrateProperty, "unable to set pointer value", v, nil)
} else {
// everything else should be a pointer, set it directly
v.Set(vnew)
}
return nil
}
func hydrateNestedComponent(v reflect.Value, component *token) error {
// create a new object to hold the property value
var vnew, varr = newValue(v)
if err := hydrateComponent(vnew, component); err != nil {
return utils.NewError(hydrateNestedComponent, "unable to decode component", component, err)
}
if varr {
// for arrays, append the new value into the array structure
voldval := dereferencePointerValue(v)
if !voldval.CanSet() {
return utils.NewError(hydrateNestedComponent, "unable to set array value", v, nil)
} else {
voldval.Set(reflect.Append(voldval, vnew))
}
} else if !v.CanSet() {
return utils.NewError(hydrateNestedComponent, "unable to set pointer value", v, nil)
} else {
// everything else should be a pointer, set it directly
v.Set(vnew)
}
return nil
}
func hydrateProperties(v reflect.Value, component *token) error {
vdref := dereferencePointerValue(v)
vtype := vdref.Type()
vkind := vdref.Kind()
if vkind != reflect.Struct {
return utils.NewError(hydrateProperties, "unable to hydrate properties of non-struct", v, nil)
}
n := vtype.NumField()
for i := 0; i < n; i++ {
prop := properties.PropertyFromStructField(vtype.Field(i))
if prop == nil {
continue // skip if field is ignored
}
vfield := vdref.Field(i)
// first try to hydrate property values
if properties, ok := component.properties[prop.Name]; ok {
for _, prop := range properties {
if err := hydrateProperty(vfield, prop); err != nil {
msg := fmt.Sprintf("unable to hydrate property %s", prop.Name)
return utils.NewError(hydrateProperties, msg, v, err)
}
}
}
// then try to hydrate components
vtemp, _ := newValue(vfield)
if tag, err := extractTagFromValue(vtemp); err != nil {
msg := fmt.Sprintf("unable to extract tag from property %s", prop.Name)
return utils.NewError(hydrateProperties, msg, v, err)
} else if components, ok := component.components[tag]; ok {
for _, comp := range components {
if err := hydrateNestedComponent(vfield, comp); err != nil {
msg := fmt.Sprintf("unable to hydrate component %s", prop.Name)
return utils.NewError(hydrateProperties, msg, v, err)
}
}
}
}
return nil
}
func hydrateComponent(v reflect.Value, component *token) error {
if tag, err := extractTagFromValue(v); err != nil {
return utils.NewError(hydrateComponent, "error extracting tag from value", component, err)
} else if tag != component.name {
msg := fmt.Sprintf("expected %s and found %s", tag, component.name)
return utils.NewError(hydrateComponent, msg, component, nil)
} else if err := hydrateProperties(v, component); err != nil {
return utils.NewError(hydrateComponent, "unable to hydrate properties", component, err)
}
return nil
}
func hydrateComponents(v reflect.Value, components []*token) error {
vdref := dereferencePointerValue(v)
for i, component := range components {
velem := reflect.New(vdref.Type().Elem())
if err := hydrateComponent(velem, component); err != nil {
msg := fmt.Sprintf("unable to hydrate component %d", i)
return utils.NewError(hydrateComponent, msg, component, err)
} else {
v.Set(reflect.Append(vdref, velem))
}
}
return nil
}
func hydrateValue(v reflect.Value, component *token) error {
if !v.IsValid() || v.Kind() != reflect.Ptr {
return utils.NewError(hydrateValue, "unmarshal target must be a valid pointer", v, nil)
}
// handle any encodable properties
if encoder, isprop := v.Interface().(properties.CanEncodeName); isprop {
if name, err := encoder.EncodeICalName(); err != nil {
return utils.NewError(hydrateValue, "unable to lookup property name", v, err)
} else if properties, found := component.properties[name]; !found || len(properties) == 0 {
return utils.NewError(hydrateValue, "no matching propery values found for "+string(name), v, nil)
} else if len(properties) > 1 {
return utils.NewError(hydrateValue, "more than one property value matches single property interface", v, nil)
} else {
return hydrateProperty(v, properties[0])
}
}
// handle components
vkind := dereferencePointerValue(v).Kind()
if tag, err := extractTagFromValue(v); err != nil {
return utils.NewError(hydrateValue, "unable to extract component tag", v, err)
} else if components, found := component.components[tag]; !found || len(components) == 0 {
msg := fmt.Sprintf("unable to find matching component for %s", tag)
return utils.NewError(hydrateValue, msg, v, nil)
} else if vkind == reflect.Array || vkind == reflect.Slice {
return hydrateComponents(v, components)
} else if len(components) > 1 {
return utils.NewError(hydrateValue, "non-array interface provided but more than one component found!", v, nil)
} else {
return hydrateComponent(v, components[0])
}
}
// decodes encoded icalendar data into a native interface
func Unmarshal(encoded string, into interface{}) error {
if component, err := tokenize(encoded); err != nil {
return utils.NewError(Unmarshal, "unable to tokenize encoded data", encoded, err)
} else {
return hydrateValue(reflect.ValueOf(into), component)
}
}

View File

@ -0,0 +1,7 @@
package values
type CalScale string
const (
GregorianCalScale CalScale = "GREGORIAN"
)

View File

@ -0,0 +1,33 @@
package values
import (
"github.com/dolanor/caldav-go/icalendar/properties"
)
// specifies non-processing information intended to provide a comment to the calendar user.
type Comment string
// encodes the comment value for the iCalendar specification
func (c Comment) EncodeICalValue() (string, error) {
return string(c), nil
}
// decodes the comment value from the iCalendar specification
func (c Comment) DecodeICalValue(value string) error {
c = Comment(value)
return nil
}
// encodes the comment value for the iCalendar specification
func (c Comment) EncodeICalName() (properties.PropertyName, error) {
return properties.CommentPropertyName, nil
}
// creates a list of comments from strings
func NewComments(comments ...string) []Comment {
var _comments []Comment
for _, comment := range comments {
_comments = append(_comments, Comment(comment))
}
return _comments
}

View File

@ -0,0 +1,139 @@
package values
import (
"fmt"
"github.com/dolanor/caldav-go/icalendar/properties"
"github.com/dolanor/caldav-go/utils"
"log"
"net/mail"
"strings"
)
var _ = log.Print
// Specifies the organizer of a group scheduled calendar entity. The property is specified within the "VFREEBUSY"
// calendar component to specify the calendar user requesting the free or busy time. When publishing a "VFREEBUSY"
// calendar component, the property is used to specify the calendar that the published busy time came from.
//
// The property has the property parameters CN, for specifying the common or display name associated with the
// "Organizer", DIR, for specifying a pointer to the directory information associated with the "Organizer",
// SENT-BY, for specifying another calendar user that is acting on behalf of the "Organizer". The non-standard
// parameters may also be specified on this property. If the LANGUAGE property parameter is specified, the identified
// language applies to the CN parameter value.
type Contact struct {
Entry mail.Address
}
type AttendeeContact Contact
type OrganizerContact Contact
// creates a new icalendar attendee representation
func NewAttendeeContact(name, email string) *AttendeeContact {
return &AttendeeContact{Entry: mail.Address{Name: name, Address: email}}
}
// creates a new icalendar organizer representation
func NewOrganizerContact(name, email string) *OrganizerContact {
return &OrganizerContact{Entry: mail.Address{Name: name, Address: email}}
}
// validates the contact value for the iCalendar specification
func (c *Contact) ValidateICalValue() error {
email := c.Entry.String()
if _, err := mail.ParseAddress(email); err != nil {
msg := fmt.Sprintf("unable to validate address %s", email)
return utils.NewError(c.ValidateICalValue, msg, c, err)
} else {
return nil
}
}
// encodes the contact value for the iCalendar specification
func (c *Contact) EncodeICalValue() (string, error) {
return fmt.Sprintf("MAILTO:%s", c.Entry.Address), nil
}
// encodes the contact params for the iCalendar specification
func (c *Contact) EncodeICalParams() (params properties.Params, err error) {
if c.Entry.Name != "" {
params = properties.Params{properties.CanonicalNameParameterName: c.Entry.Name}
}
return
}
// decodes the contact value from the iCalendar specification
func (c *Contact) DecodeICalValue(value string) error {
parts := strings.SplitN(value, ":", 2)
if len(parts) > 1 {
c.Entry.Address = parts[1]
}
return nil
}
// decodes the contact params from the iCalendar specification
func (c *Contact) DecodeICalParams(params properties.Params) error {
if name, found := params[properties.CanonicalNameParameterName]; found {
c.Entry.Name = name
}
return nil
}
// validates the contact value for the iCalendar specification
func (c *OrganizerContact) ValidateICalValue() error {
return (*Contact)(c).ValidateICalValue()
}
// encodes the contact value for the iCalendar specification
func (c *OrganizerContact) EncodeICalValue() (string, error) {
return (*Contact)(c).EncodeICalValue()
}
// encodes the contact params for the iCalendar specification
func (c *OrganizerContact) EncodeICalParams() (params properties.Params, err error) {
return (*Contact)(c).EncodeICalParams()
}
// decodes the contact value from the iCalendar specification
func (c *OrganizerContact) DecodeICalValue(value string) error {
return (*Contact)(c).DecodeICalValue(value)
}
// decodes the contact params from the iCalendar specification
func (c *OrganizerContact) DecodeICalParams(params properties.Params) error {
return (*Contact)(c).DecodeICalParams(params)
}
// encodes the contact property name for the iCalendar specification
func (o *OrganizerContact) EncodeICalName() (properties.PropertyName, error) {
return properties.OrganizerPropertyName, nil
}
// validates the contact value for the iCalendar specification
func (c *AttendeeContact) ValidateICalValue() error {
return (*Contact)(c).ValidateICalValue()
}
// encodes the contact value for the iCalendar specification
func (c *AttendeeContact) EncodeICalValue() (string, error) {
return (*Contact)(c).EncodeICalValue()
}
// encodes the contact params for the iCalendar specification
func (c *AttendeeContact) EncodeICalParams() (params properties.Params, err error) {
return (*Contact)(c).EncodeICalParams()
}
// decodes the contact value from the iCalendar specification
func (c *AttendeeContact) DecodeICalValue(value string) error {
return (*Contact)(c).DecodeICalValue(value)
}
// decodes the contact params from the iCalendar specification
func (c *AttendeeContact) DecodeICalParams(params properties.Params) error {
return (*Contact)(c).DecodeICalParams(params)
}
// encodes the contact property name for the iCalendar specification
func (o *AttendeeContact) EncodeICalName() (properties.PropertyName, error) {
return properties.AttendeePropertyName, nil
}

View File

@ -0,0 +1,24 @@
package values
import (
"log"
"strings"
)
var _ = log.Print
type CSV []string
func (csv *CSV) EncodeICalValue() (string, error) {
return strings.Join(*csv, ","), nil
}
func (csv *CSV) DecodeICalValue(value string) error {
value = strings.TrimSpace(value)
*csv = CSV(strings.Split(value, ","))
return nil
}
func NewCSV(items ...string) *CSV {
return (*CSV)(&items)
}

View File

@ -0,0 +1,265 @@
package values
import (
"fmt"
"github.com/dolanor/caldav-go/icalendar/properties"
"github.com/dolanor/caldav-go/utils"
"log"
"strings"
"time"
)
var _ = log.Print
const DateFormatString = "20060102"
const DateTimeFormatString = "20060102T150405"
const UTCDateTimeFormatString = "20060102T150405Z"
// a representation of a date and time for iCalendar
type DateTime struct {
t time.Time
}
type DateTimes []*DateTime
// The exception dates, if specified, are used in computing the recurrence set. The recurrence set is the complete set
// of recurrence instances for a calendar component. The recurrence set is generated by considering the initial
// "DTSTART" property along with the "RRULE", "RDATE", "EXDATE" and "EXRULE" properties contained within the iCalendar
// object. The "DTSTART" property defines the first instance in the recurrence set. Multiple instances of the "RRULE"
// and "EXRULE" properties can also be specified to define more sophisticated recurrence sets. The final recurrence set
// is generated by gathering all of the start date-times generated by any of the specified "RRULE" and "RDATE"
// properties, and then excluding any start date and times which fall within the union of start date and times
// generated by any specified "EXRULE" and "EXDATE" properties. This implies that start date and times within exclusion
// related properties (i.e., "EXDATE" and "EXRULE") take precedence over those specified by inclusion properties
// (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only
// one recurrence is considered. Duplicate instances are ignored.
//
// The "EXDATE" property can be used to exclude the value specified in "DTSTART". However, in such cases the original
// "DTSTART" date MUST still be maintained by the calendaring and scheduling system because the original "DTSTART"
// value has inherent usage dependencies by other properties such as the "RECURRENCE-ID".
type ExceptionDateTimes DateTimes
// The recurrence dates, if specified, are used in computing the recurrence set. The recurrence set is the complete set
// of recurrence instances for a calendar component. The recurrence set is generated by considering the initial
// "DTSTART" property along with the "RRULE", "RDATE", "EXDATE" and "EXRULE" properties contained within the iCalendar
// object. The "DTSTART" property defines the first instance in the recurrence set. Multiple instances of the "RRULE"
// and "EXRULE" properties can also be specified to define more sophisticated recurrence sets. The final recurrence set
// is generated by gathering all of the start date-times generated by any of the specified "RRULE" and "RDATE"
// properties, and then excluding any start date and times which fall within the union of start date and times
// generated by any specified "EXRULE" and "EXDATE" properties. This implies that start date and times within exclusion
// related properties (i.e., "EXDATE" and "EXRULE") take precedence over those specified by inclusion properties
// (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only
// one recurrence is considered. Duplicate instances are ignored.
type RecurrenceDateTimes DateTimes
// creates a new icalendar datetime representation
func NewDateTime(t time.Time) *DateTime {
return &DateTime{t: t.Truncate(time.Second)}
}
// creates a new icalendar datetime array representation
func NewDateTimes(dates ...*DateTime) DateTimes {
return DateTimes(dates)
}
// creates a new icalendar datetime array representation
func NewExceptionDateTimes(dates ...*DateTime) *ExceptionDateTimes {
datetimes := NewDateTimes(dates...)
return (*ExceptionDateTimes)(&datetimes)
}
// creates a new icalendar datetime array representation
func NewRecurrenceDateTimes(dates ...*DateTime) *RecurrenceDateTimes {
datetimes := NewDateTimes(dates...)
return (*RecurrenceDateTimes)(&datetimes)
}
// checks to see if two datetimes are equal
func (d *DateTime) Equals(test *DateTime) bool {
return d.t.Equal(test.t)
}
// returns the native time for the datetime object
func (d *DateTime) NativeTime() time.Time {
return d.t
}
// encodes the datetime value for the iCalendar specification
func (d *DateTime) EncodeICalValue() (string, error) {
val := d.t.Format(DateTimeFormatString)
loc := d.t.Location()
if loc == time.UTC {
val = fmt.Sprintf("%sZ", val)
}
return val, nil
}
// decodes the datetime value from the iCalendar specification
func (d *DateTime) DecodeICalValue(value string) error {
layout := DateTimeFormatString
if strings.HasSuffix(value, "Z") {
layout = UTCDateTimeFormatString
} else if len(value) == 8 {
layout = DateFormatString
}
var err error
d.t, err = time.ParseInLocation(layout, value, time.UTC)
if err != nil {
return utils.NewError(d.DecodeICalValue, "unable to parse datetime value", d, err)
} else {
return nil
}
}
// encodes the datetime params for the iCalendar specification
func (d *DateTime) EncodeICalParams() (params properties.Params, err error) {
loc := d.t.Location()
if loc != time.UTC {
params = properties.Params{properties.TimeZoneIdPropertyName: loc.String()}
}
return
}
// decodes the datetime params from the iCalendar specification
func (d *DateTime) DecodeICalParams(params properties.Params) error {
layout := DateTimeFormatString
value := d.t.Format(layout)
if name, found := params[properties.TimeZoneIdPropertyName]; !found {
return nil
} else if loc, err := time.LoadLocation(name); err != nil {
return utils.NewError(d.DecodeICalValue, "unable to parse timezone", d, err)
} else if t, err := time.ParseInLocation(layout, value, loc); err != nil {
return utils.NewError(d.DecodeICalValue, "unable to parse datetime value", d, err)
} else {
d.t = t
return nil
}
}
// validates the datetime value against the iCalendar specification
func (d *DateTime) ValidateICalValue() error {
loc := d.t.Location()
if loc == time.Local {
msg := "DateTime location may not Local, please use UTC or explicit Location"
return utils.NewError(d.ValidateICalValue, msg, d, nil)
}
if loc.String() == "" {
msg := "DateTime location must have a valid name"
return utils.NewError(d.ValidateICalValue, msg, d, nil)
}
return nil
}
// encodes the datetime value for the iCalendar specification
func (d *DateTime) String() string {
if s, err := d.EncodeICalValue(); err != nil {
panic(err)
} else {
return s
}
}
// encodes a list of datetime values for the iCalendar specification
func (ds *DateTimes) EncodeICalValue() (string, error) {
var csv CSV
for i, d := range *ds {
if s, err := d.EncodeICalValue(); err != nil {
msg := fmt.Sprintf("unable to encode datetime at index %d", i)
return "", utils.NewError(ds.EncodeICalValue, msg, ds, err)
} else {
csv = append(csv, s)
}
}
return csv.EncodeICalValue()
}
// encodes a list of datetime params for the iCalendar specification
func (ds *DateTimes) EncodeICalParams() (params properties.Params, err error) {
if len(*ds) > 0 {
params, err = (*ds)[0].EncodeICalParams()
}
return
}
// decodes a list of datetime params from the iCalendar specification
func (ds *DateTimes) DecodeICalParams(params properties.Params) error {
for i, d := range *ds {
if err := d.DecodeICalParams(params); err != nil {
msg := fmt.Sprintf("unable to decode datetime params for index %d", i)
return utils.NewError(ds.DecodeICalValue, msg, ds, err)
}
}
return nil
}
// encodes a list of datetime values for the iCalendar specification
func (ds *DateTimes) DecodeICalValue(value string) error {
csv := new(CSV)
if err := csv.DecodeICalValue(value); err != nil {
return utils.NewError(ds.DecodeICalValue, "unable to decode datetime list as CSV", ds, err)
}
for i, value := range *csv {
d := new(DateTime)
if err := d.DecodeICalValue(value); err != nil {
msg := fmt.Sprintf("unable to decode datetime at index %d", i)
return utils.NewError(ds.DecodeICalValue, msg, ds, err)
} else {
*ds = append(*ds, d)
}
}
return nil
}
// encodes exception date times property name for icalendar
func (e *ExceptionDateTimes) EncodeICalName() (properties.PropertyName, error) {
return properties.ExceptionDateTimesPropertyName, nil
}
// encodes recurrence date times property name for icalendar
func (r *RecurrenceDateTimes) EncodeICalName() (properties.PropertyName, error) {
return properties.RecurrenceDateTimesPropertyName, nil
}
// encodes exception date times property value for icalendar
func (e *ExceptionDateTimes) EncodeICalValue() (string, error) {
return (*DateTimes)(e).EncodeICalValue()
}
// encodes recurrence date times property value for icalendar
func (r *RecurrenceDateTimes) EncodeICalValue() (string, error) {
return (*DateTimes)(r).EncodeICalValue()
}
// decodes exception date times property value for icalendar
func (e *ExceptionDateTimes) DecodeICalValue(value string) error {
return (*DateTimes)(e).DecodeICalValue(value)
}
// decodes recurrence date times property value for icalendar
func (r *RecurrenceDateTimes) DecodeICalValue(value string) error {
return (*DateTimes)(r).DecodeICalValue(value)
}
// encodes exception date times property params for icalendar
func (e *ExceptionDateTimes) EncodeICalParams() (params properties.Params, err error) {
return (*DateTimes)(e).EncodeICalParams()
}
// encodes recurrence date times property params for icalendar
func (r *RecurrenceDateTimes) EncodeICalParams() (params properties.Params, err error) {
return (*DateTimes)(r).EncodeICalParams()
}
// encodes exception date times property params for icalendar
func (e *ExceptionDateTimes) DecodeICalParams(params properties.Params) error {
return (*DateTimes)(e).DecodeICalParams(params)
}
// encodes recurrence date times property params for icalendar
func (r *RecurrenceDateTimes) DecodeICalParams(params properties.Params) error {
return (*DateTimes)(r).DecodeICalParams(params)
}

View File

@ -0,0 +1,132 @@
package values
import (
"fmt"
"github.com/dolanor/caldav-go/utils"
"log"
"math"
"regexp"
"strconv"
"strings"
"time"
)
var _ = log.Print
// a representation of duration for iCalendar
type Duration struct {
d time.Duration
}
// breaks apart the duration into its component time parts
func (d *Duration) Decompose() (weeks, days, hours, minutes, seconds int64) {
// chip away at this
rem := time.Duration(math.Abs(float64(d.d)))
div := time.Hour * 24 * 7
weeks = int64(rem / div)
rem = rem % div
div = div / 7
days = int64(rem / div)
rem = rem % div
div = div / 24
hours = int64(rem / div)
rem = rem % div
div = div / 60
minutes = int64(rem / div)
rem = rem % div
div = div / 60
seconds = int64(rem / div)
return
}
// returns the native golang duration
func (d *Duration) NativeDuration() time.Duration {
return d.d
}
// returns true if the duration is negative
func (d *Duration) IsPast() bool {
return d.d < 0
}
// encodes the duration of time into iCalendar format
func (d *Duration) EncodeICalValue() (string, error) {
var parts []string
weeks, days, hours, minutes, seconds := d.Decompose()
if d.IsPast() {
parts = append(parts, "-")
}
parts = append(parts, "P")
if weeks > 0 {
parts = append(parts, fmt.Sprintf("%dW", weeks))
}
if days > 0 {
parts = append(parts, fmt.Sprintf("%dD", days))
}
if hours > 0 || minutes > 0 || seconds > 0 {
parts = append(parts, "T")
if hours > 0 {
parts = append(parts, fmt.Sprintf("%dH", hours))
}
if minutes > 0 {
parts = append(parts, fmt.Sprintf("%dM", minutes))
}
if seconds > 0 {
parts = append(parts, fmt.Sprintf("%dS", seconds))
}
}
return strings.Join(parts, ""), nil
}
var durationRegEx = regexp.MustCompile("(\\d+)(\\w)")
// decodes the duration of time from iCalendar format
func (d *Duration) DecodeICalValue(value string) error {
var seconds int64
var isPast = strings.HasPrefix(value, "-P")
var matches = durationRegEx.FindAllStringSubmatch(value, -1)
for _, match := range matches {
var multiplier int64
ivalue, err := strconv.ParseInt(match[1], 10, 64)
if err != nil {
return utils.NewError(d.DecodeICalValue, "unable to decode duration value "+match[1], d, nil)
}
switch match[2] {
case "S":
multiplier = 1
case "M":
multiplier = 60
case "H":
multiplier = 60 * 60
case "D":
multiplier = 60 * 60 * 24
case "W":
multiplier = 60 * 60 * 24 * 7
default:
return utils.NewError(d.DecodeICalValue, "unable to decode duration segment "+match[2], d, nil)
}
seconds = seconds + multiplier*ivalue
}
d.d = time.Duration(seconds) * time.Second
if isPast {
d.d = -d.d
}
return nil
}
func (d *Duration) String() string {
if s, err := d.EncodeICalValue(); err != nil {
panic(err)
} else {
return s
}
}
// creates a new iCalendar duration representation
func NewDuration(d time.Duration) *Duration {
return &Duration{d: d}
}

View File

@ -0,0 +1,17 @@
package values
// An access classification is only one component of the general security system within a calendar application.
// It provides a method of capturing the scope of the access the calendar owner intends for information within an
// individual calendar entry. The access classification of an individual iCalendar component is useful when measured
// along with the other security components of a calendar system (e.g., calendar user authentication, authorization,
// access rights, access role, etc.). Hence, the semantics of the individual access classifications cannot be completely
// defined by this memo alone. Additionally, due to the "blind" nature of most exchange processes using this memo, these
// access classifications cannot serve as an enforcement statement for a system receiving an iCalendar object. Rather,
// they provide a method for capturing the intention of the calendar owner for the access to the calendar component.
type EventAccessClassification string
const (
PublicEventAccessClassification EventAccessClassification = "PUBLIC"
PrivateEventAccessClassification = "PRIVATE"
ConfidentialEventAccessClassification = "CONFIDENTIAL"
)

View File

@ -0,0 +1,13 @@
package values
// In a group scheduled calendar component, the property is used by the "Organizer" to provide a confirmation of the
// event to the "Attendees".
// For example in an Event calendar component, the "Organizer" can indicate that a meeting is tentative, confirmed or
// cancelled.
type EventStatus string
const (
TentativeEventStatus EventStatus = "TENTATIVE" // Indicates event is tentative.
ConfirmedEventStatus = "CONFIRMED" // Indicates event is definite.
CancelledEventStatus = "CANCELLED" // Indicates event is cancelled.
)

View File

@ -0,0 +1,69 @@
package values
import (
"fmt"
"github.com/dolanor/caldav-go/utils"
"log"
"strconv"
"strings"
)
var _ = log.Print
// a representation of a geographical point for iCalendar
type Geo struct {
coords []float64
}
// creates a new icalendar geo representation
func NewGeo(lat, lng float64) *Geo {
return &Geo{coords: []float64{lat, lng}}
}
// returns the latitude encoded into the geo point
func (g *Geo) Lat() float64 {
return g.coords[0]
}
// returns the longitude encoded into the geo point
func (g *Geo) Lng() float64 {
return g.coords[1]
}
// validates the geo value against the iCalendar specification
func (g *Geo) ValidateICalValue() error {
if len(g.coords) != 2 {
return utils.NewError(g.ValidateICalValue, "geo value must have length of 2", g, nil)
}
if g.Lat() < -90 || g.Lat() > 90 {
return utils.NewError(g.ValidateICalValue, "geo latitude must be between -90 and 90 degrees", g, nil)
}
if g.Lng() < -180 || g.Lng() > 180 {
return utils.NewError(g.ValidateICalValue, "geo longitude must be between -180 and 180 degrees", g, nil)
}
return nil
}
// encodes the geo value for the iCalendar specification
func (g *Geo) EncodeICalValue() (string, error) {
return fmt.Sprintf("%f %f", g.Lat(), g.Lng()), nil
}
// decodes the geo value from the iCalendar specification
func (g *Geo) DecodeICalValue(value string) error {
if latlng := strings.Split(value, " "); len(latlng) < 2 {
return utils.NewError(g.DecodeICalValue, "geo value must have both a latitude and longitude component", g, nil)
} else if lat, err := strconv.ParseFloat(latlng[0], 64); err != nil {
return utils.NewError(g.DecodeICalValue, "unable to decode latitude component", g, err)
} else if lng, err := strconv.ParseFloat(latlng[1], 64); err != nil {
return utils.NewError(g.DecodeICalValue, "unable to decode latitude component", g, err)
} else {
*g = Geo{coords: []float64{lat, lng}}
return nil
}
}

View File

@ -0,0 +1,78 @@
package values
import (
"github.com/dolanor/caldav-go/icalendar/properties"
"github.com/dolanor/caldav-go/utils"
"log"
"net/url"
)
var _ = log.Print
// Specific venues such as conference or meeting rooms may be explicitly specified using this property. An alternate
// representation may be specified that is a URI that points to directory information with more structured specification
// of the location. For example, the alternate representation may specify either an LDAP URI pointing to an LDAP server
// entry or a CID URI pointing to a MIME body part containing a vCard [RFC 2426] for the location.
type Location struct {
value string
altrep *url.URL
}
// creates a new icalendar location representation
func NewLocation(value string, altrep ...*url.URL) *Location {
loc := &Location{value: value}
if len(altrep) > 0 {
loc.altrep = altrep[0]
}
return loc
}
// returns an alternate representation for the location
// if one exists
func (l *Location) AltRep() *url.URL {
return l.altrep
}
// encodes the location for the iCalendar specification
func (l *Location) EncodeICalValue() (string, error) {
return l.value, nil
}
// decodes the location from the iCalendar specification
func (l *Location) DecodeICalValue(value string) error {
l.value = value
return nil
}
// encodes the location params for the iCalendar specification
func (l *Location) EncodeICalParams() (params properties.Params, err error) {
if l.altrep != nil {
params = properties.Params{properties.AlternateRepresentationName: l.altrep.String()}
}
return
}
// decodes the location params from the iCalendar specification
func (l *Location) DecodeICalParams(params properties.Params) error {
if rep, found := params[properties.AlternateRepresentationName]; !found {
return nil
} else if altrep, err := url.Parse(rep); err != nil {
return utils.NewError(l.DecodeICalValue, "unable to parse alternate representation", l, err)
} else {
l.altrep = altrep
return nil
}
}
// validates the location against the iCalendar specification
func (l *Location) ValidateICalValue() error {
if l.altrep != nil {
if _, err := url.Parse(l.altrep.String()); err != nil {
msg := "location alternate representation must be a valid url"
return utils.NewError(l.ValidateICalValue, msg, l, err)
}
}
return nil
}

View File

@ -0,0 +1,7 @@
package values
type Method string
const (
PublishMethod Method = "PUBLISH"
)

View File

@ -0,0 +1,434 @@
package values
import (
"fmt"
"github.com/dolanor/caldav-go/icalendar/properties"
"github.com/dolanor/caldav-go/utils"
"log"
"regexp"
"strconv"
"strings"
)
// The recurrence rule, if specified, is used in computing the recurrence set. The recurrence set is the complete set
// of recurrence instances for a calendar component. The recurrence set is generated by considering the initial
// "DTSTART" property along with the "RRULE", "RDATE", "EXDATE" and "EXRULE" properties contained within the iCalendar
// object. The "DTSTART" property defines the first instance in the recurrence set. Multiple instances of the "RRULE"
// and "EXRULE" properties can also be specified to define more sophisticated recurrence sets. The final recurrence
// set is generated by gathering all of the start date/times generated by any of the specified "RRULE" and "RDATE"
// properties, and excluding any start date/times which fall within the union of start date/times generated by any
// specified "EXRULE" and "EXDATE" properties. This implies that start date/times within exclusion related properties
// (i.e., "EXDATE" and "EXRULE") take precedence over those specified by inclusion properties
// (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only
// one recurrence is considered. Duplicate instances are ignored.
// The "DTSTART" and "DTEND" property pair or "DTSTART" and "DURATION" property pair, specified within the iCalendar
// object defines the first instance of the recurrence. When used with a recurrence rule, the "DTSTART" and "DTEND"
// properties MUST be specified in local time and the appropriate set of "VTIMEZONE" calendar components MUST be
// included. For detail on the usage of the "VTIMEZONE" calendar component, see the "VTIMEZONE" calendar component
// definition.
// Any duration associated with the iCalendar object applies to all members of the generated recurrence set. Any
// modified duration for specific recurrences MUST be explicitly specified using the "RDATE" property.
type RecurrenceRule struct {
Frequency RecurrenceFrequency
Until *DateTime
Count int
Interval int
BySecond []int
ByMinute []int
ByHour []int
ByDay []RecurrenceWeekday
ByMonthDay []int
ByYearDay []int
ByWeekNumber []int
ByMonth []int
BySetPosition []int
WeekStart RecurrenceWeekday
}
var _ = log.Print
// the frequency an event recurs
type RecurrenceFrequency string
const (
SecondRecurrenceFrequency RecurrenceFrequency = "SECONDLY"
MinuteRecurrenceFrequency = "MINUTELY"
HourRecurrenceFrequency = "HOURLY"
DayRecurrenceFrequency = "DAILY"
WeekRecurrenceFrequency = "WEEKLY"
MonthRecurrenceFrequency = "MONTHLY"
YearRecurrenceFrequency = "YEARLY"
)
// the frequency an event recurs
type RecurrenceWeekday string
const (
MondayRecurrenceWeekday RecurrenceWeekday = "MO"
TuesdayRecurrenceWeekday = "TU"
WednesdayRecurrenceWeekday = "WE"
ThursdayRecurrenceWeekday = "TH"
FridayRecurrenceWeekday = "FR"
SaturdayRecurrenceWeekday = "SA"
SundayRecurrenceWeekday = "SU"
)
// creates a new recurrence rule object for iCalendar
func NewRecurrenceRule(frequency RecurrenceFrequency) *RecurrenceRule {
return &RecurrenceRule{Frequency: frequency}
}
var weekdayRegExp = regexp.MustCompile("MO|TU|WE|TH|FR|SA|SU")
// returns true if weekday is a valid constant
func (r RecurrenceWeekday) IsValidWeekDay() bool {
return weekdayRegExp.MatchString(strings.ToUpper(string(r)))
}
var frequencyRegExp = regexp.MustCompile("SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY")
// returns true if weekday is a valid constant
func (r RecurrenceFrequency) IsValidFrequency() bool {
return frequencyRegExp.MatchString(strings.ToUpper(string(r)))
}
// returns the recurrence rule name for the iCalendar specification
func (r *RecurrenceRule) EncodeICalName() (properties.PropertyName, error) {
return properties.RecurrenceRulePropertyName, nil
}
// encodes the recurrence rule value for the iCalendar specification
func (r *RecurrenceRule) EncodeICalValue() (string, error) {
out := []string{fmt.Sprintf("FREQ=%s", strings.ToUpper(string(r.Frequency)))}
if r.Until != nil {
if encoded, err := r.Until.EncodeICalValue(); err != nil {
return "", utils.NewError(r.EncodeICalValue, "unable to encode until date", r, err)
} else {
out = append(out, fmt.Sprintf("UNTIL=%s", encoded))
}
}
if r.Count > 0 {
out = append(out, fmt.Sprintf("COUNT=%d", r.Count))
}
if r.Interval > 0 {
out = append(out, fmt.Sprintf("INTERVAL=%d", r.Interval))
}
if len(r.BySecond) > 0 {
if encoded, err := intsToCSV(r.BySecond); err != nil {
return "", utils.NewError(r.EncodeICalValue, "unable to encode by second value", r, err)
} else {
out = append(out, fmt.Sprintf("BYSECOND=%s", encoded))
}
}
if len(r.ByMinute) > 0 {
if encoded, err := intsToCSV(r.ByMinute); err != nil {
return "", utils.NewError(r.EncodeICalValue, "unable to encode by minute value", r, err)
} else {
out = append(out, fmt.Sprintf("BYMINUTE=%s", encoded))
}
}
if len(r.ByHour) > 0 {
if encoded, err := intsToCSV(r.ByHour); err != nil {
return "", utils.NewError(r.EncodeICalValue, "unable to encode by hour value", r, err)
} else {
out = append(out, fmt.Sprintf("BYHOUR=%s", encoded))
}
}
if len(r.ByDay) > 0 {
if encoded, err := daysToCSV(r.ByDay); err != nil {
return "", utils.NewError(r.EncodeICalValue, "unable to encode by day value", r, err)
} else {
out = append(out, fmt.Sprintf("BYDAY=%s", encoded))
}
}
if len(r.ByMonthDay) > 0 {
if encoded, err := intsToCSV(r.ByMonthDay); err != nil {
return "", utils.NewError(r.EncodeICalValue, "unable to encode by month day value", r, err)
} else {
out = append(out, fmt.Sprintf("BYMONTHDAY=%s", encoded))
}
}
if len(r.ByYearDay) > 0 {
if encoded, err := intsToCSV(r.ByYearDay); err != nil {
return "", utils.NewError(r.EncodeICalValue, "unable to encode by year day value", r, err)
} else {
out = append(out, fmt.Sprintf("BYYEARDAY=%s", encoded))
}
}
if len(r.ByWeekNumber) > 0 {
if encoded, err := intsToCSV(r.ByWeekNumber); err != nil {
return "", utils.NewError(r.EncodeICalValue, "unable to encode by week number value", r, err)
} else {
out = append(out, fmt.Sprintf("BYWEEKNO=%s", encoded))
}
}
if len(r.ByMonth) > 0 {
if encoded, err := intsToCSV(r.ByMonth); err != nil {
return "", utils.NewError(r.EncodeICalValue, "unable to encode by month value", r, err)
} else {
out = append(out, fmt.Sprintf("BYMONTH=%s", encoded))
}
}
if len(r.BySetPosition) > 0 {
if encoded, err := intsToCSV(r.BySetPosition); err != nil {
return "", utils.NewError(r.EncodeICalValue, "unable to encode by set position value", r, err)
} else {
out = append(out, fmt.Sprintf("BYSETPOS=%s", encoded))
}
}
if r.WeekStart != "" {
out = append(out, fmt.Sprintf("WKST=%s", r.WeekStart))
}
return strings.Join(out, ";"), nil
}
var rruleParamRegExp = regexp.MustCompile("(\\w+)\\s*=\\s*([^;]+)")
// decodes the recurrence rule value from the iCalendar specification
func (r *RecurrenceRule) DecodeICalValue(value string) error {
matches := rruleParamRegExp.FindAllStringSubmatch(value, -1)
if len(matches) <= 0 {
return utils.NewError(r.DecodeICalValue, "no recurrence rules found", r, nil)
}
for _, match := range matches {
if err := r.decodeICalValue(match[1], match[2]); err != nil {
msg := fmt.Sprintf("unable to decode %s value", match[1])
return utils.NewError(r.DecodeICalValue, msg, r, err)
}
}
return nil
}
func (r *RecurrenceRule) decodeICalValue(name string, value string) error {
switch name {
case "FREQ":
r.Frequency = RecurrenceFrequency(value)
case "UNTIL":
until := new(DateTime)
if err := until.DecodeICalValue(value); err != nil {
return utils.NewError(r.decodeICalValue, "invalid until value "+value, r, err)
} else {
r.Until = until
}
case "COUNT":
if count, err := strconv.ParseInt(value, 10, 64); err != nil {
return utils.NewError(r.decodeICalValue, "invalid count value "+value, r, err)
} else {
r.Count = int(count)
}
case "INTERVAL":
if interval, err := strconv.ParseInt(value, 10, 64); err != nil {
return utils.NewError(r.decodeICalValue, "invalid interval value "+value, r, err)
} else {
r.Interval = int(interval)
}
case "BYSECOND":
if ints, err := csvToInts(value); err != nil {
return utils.NewError(r.decodeICalValue, "invalid by second value "+value, r, err)
} else {
r.BySecond = ints
}
case "BYMINUTE":
if ints, err := csvToInts(value); err != nil {
return utils.NewError(r.decodeICalValue, "invalid by minute value "+value, r, err)
} else {
r.ByMinute = ints
}
case "BYHOUR":
if ints, err := csvToInts(value); err != nil {
return utils.NewError(r.decodeICalValue, "invalid by hour value "+value, r, err)
} else {
r.ByHour = ints
}
case "BYDAY":
if days, err := csvToDays(value); err != nil {
return utils.NewError(r.decodeICalValue, "invalid by day value "+value, r, err)
} else {
r.ByDay = days
}
case "BYMONTHDAY":
if ints, err := csvToInts(value); err != nil {
return utils.NewError(r.decodeICalValue, "invalid by month day value "+value, r, err)
} else {
r.ByMonthDay = ints
}
case "BYYEARDAY":
if ints, err := csvToInts(value); err != nil {
return utils.NewError(r.decodeICalValue, "invalid by year day value "+value, r, err)
} else {
r.ByYearDay = ints
}
case "BYWEEKNO":
if ints, err := csvToInts(value); err != nil {
return utils.NewError(r.decodeICalValue, "invalid by week number value "+value, r, err)
} else {
r.ByWeekNumber = ints
}
case "BYMONTH":
if ints, err := csvToInts(value); err != nil {
return utils.NewError(r.decodeICalValue, "unable to encode by month value "+value, r, err)
} else {
r.ByMonth = ints
}
case "BYSETPOS":
if ints, err := csvToInts(value); err != nil {
return utils.NewError(r.decodeICalValue, "unable to encode by set position value "+value, r, err)
} else {
r.BySetPosition = ints
}
case "WKST":
r.WeekStart = RecurrenceWeekday(value)
}
return nil
}
// validates the recurrence rule value against the iCalendar specification
func (r *RecurrenceRule) ValidateICalValue() error {
if !r.Frequency.IsValidFrequency() {
return utils.NewError(r.ValidateICalValue, "a frequency is required in all recurrence rules", r, nil)
} else if r.Until != nil && r.Count > 0 {
return utils.NewError(r.ValidateICalValue, "until and count values are mutually exclusive", r, nil)
} else if found, fine := intsInRange(r.BySecond, 59); !fine {
msg := fmt.Sprintf("by second value of %d is out of bounds", found)
return utils.NewError(r.ValidateICalValue, msg, r, nil)
} else if found, fine := intsInRange(r.ByMinute, 59); !fine {
msg := fmt.Sprintf("by minute value of %d is out of bounds", found)
return utils.NewError(r.ValidateICalValue, msg, r, nil)
} else if found, fine := intsInRange(r.ByHour, 23); !fine {
msg := fmt.Sprintf("by hour value of %d is out of bounds", found)
return utils.NewError(r.ValidateICalValue, msg, r, nil)
} else if err := daysInRange(r.ByDay); err != nil {
return utils.NewError(r.ValidateICalValue, "by day value not in range", r, err)
} else if found, fine := intsInRange(r.ByMonthDay, 31); !fine {
msg := fmt.Sprintf("by month day value of %d is out of bounds", found)
return utils.NewError(r.ValidateICalValue, msg, r, nil)
} else if found, fine := intsInRange(r.ByYearDay, 366); !fine {
msg := fmt.Sprintf("by year day value of %d is out of bounds", found)
return utils.NewError(r.ValidateICalValue, msg, r, nil)
} else if found, fine := intsInRange(r.ByMonth, 12); !fine {
msg := fmt.Sprintf("by month value of %d is out of bounds", found)
return utils.NewError(r.ValidateICalValue, msg, r, nil)
} else if found, fine := intsInRange(r.BySetPosition, 366); !fine {
msg := fmt.Sprintf("by month value of %d is out of bounds", found)
return utils.NewError(r.ValidateICalValue, msg, r, nil)
} else if err := dayInRange(r.WeekStart); r.WeekStart != "" && err != nil {
return utils.NewError(r.ValidateICalValue, "week start value not in range", r, err)
} else {
return nil
}
}
func intsToCSV(ints []int) (string, error) {
csv := new(CSV)
for _, i := range ints {
*csv = append(*csv, fmt.Sprintf("%d", i))
}
return csv.EncodeICalValue()
}
func csvToInts(value string) (ints []int, err error) {
csv := new(CSV)
if ierr := csv.DecodeICalValue(value); err != nil {
err = utils.NewError(csvToInts, "unable to decode CSV value", value, ierr)
return
}
for _, v := range *csv {
if i, ierr := strconv.ParseInt(v, 10, 64); err != nil {
err = utils.NewError(csvToInts, "unable to parse int value "+v, value, ierr)
return
} else {
ints = append(ints, int(i))
}
}
return
}
func intsInRange(ints []int, max int) (int, bool) {
for _, i := range ints {
if i < -max || i > max {
return i, false
}
}
return 0, true
}
func daysInRange(days []RecurrenceWeekday) error {
for _, day := range days {
if err := dayInRange(day); err != nil {
msg := fmt.Sprintf("day value %s is not in range", day)
return utils.NewError(dayInRange, msg, days, err)
}
}
return nil
}
var dayRegExp = regexp.MustCompile("(\\d{1,2})?(\\w{2})")
func dayInRange(day RecurrenceWeekday) error {
var ordinal, weekday string
if matches := dayRegExp.FindAllStringSubmatch(string(day), -1); len(matches) <= 0 {
msg := fmt.Sprintf("weekday value %s is not in valid format", day)
return utils.NewError(dayInRange, msg, day, nil)
} else if len(matches[0]) > 2 {
ordinal = matches[0][1]
weekday = matches[0][2]
} else {
weekday = matches[0][1]
}
if !RecurrenceWeekday(weekday).IsValidWeekDay() {
msg := fmt.Sprintf("weekday value %s is not valid", weekday)
return utils.NewError(dayInRange, msg, day, nil)
} else if i, err := strconv.ParseInt(ordinal, 10, 64); ordinal != "" && err != nil {
msg := fmt.Sprintf("weekday ordinal value %d is not valid", i)
return utils.NewError(dayInRange, msg, day, err)
} else if i < -53 || i > 53 {
msg := fmt.Sprintf("weekday ordinal value %d is not in range", i)
return utils.NewError(dayInRange, msg, day, nil)
} else {
return nil
}
}
func daysToCSV(days []RecurrenceWeekday) (string, error) {
csv := new(CSV)
for _, day := range days {
*csv = append(*csv, strings.ToUpper(string(day)))
}
return csv.EncodeICalValue()
}
func csvToDays(value string) (days []RecurrenceWeekday, err error) {
csv := new(CSV)
if ierr := csv.DecodeICalValue(value); err != nil {
err = utils.NewError(csvToInts, "unable to decode CSV value", value, ierr)
return
}
for _, v := range *csv {
days = append(days, RecurrenceWeekday(v))
}
return
}

View File

@ -0,0 +1,12 @@
package values
// Time Transparency is the characteristic of an event that determines whether it appears to consume time on a calendar.
// Events that consume actual time for the individual or resource associated with the calendar SHOULD be recorded as
// OPAQUE, allowing them to be detected by free-busy time searches. Other events, which do not take up the individual's
// (or resource's) time SHOULD be recorded as TRANSPARENT, making them invisible to free-busy time searches.
type TimeTransparency string
const (
OpaqueTimeTransparency TimeTransparency = "OPAQUE" // Blocks or opaque on busy time searches. DEFAULT
TransparentTimeTransparency = "TRANSPARENT" // Transparent on busy time searches.
)

View File

@ -0,0 +1,49 @@
package values
import (
"github.com/dolanor/caldav-go/icalendar/properties"
"github.com/dolanor/caldav-go/utils"
"net/url"
)
// a representation of duration for iCalendar
type Url struct {
u url.URL
}
// encodes the URL into iCalendar format
func (u *Url) EncodeICalValue() (string, error) {
return u.u.String(), nil
}
// encodes the url params for the iCalendar specification
func (u *Url) EncodeICalParams() (params properties.Params, err error) {
params = properties.Params{
properties.ValuePropertyName: "URI",
}
return
}
// decodes the URL from iCalendar format
func (u *Url) DecodeICalValue(value string) error {
if parsed, err := url.Parse(value); err != nil {
return utils.NewError(u.ValidateICalValue, "unable to parse url", u, err)
} else {
u.u = *parsed
return nil
}
}
// validates the URL for iCalendar format
func (u *Url) ValidateICalValue() error {
if _, err := url.Parse(u.u.String()); err != nil {
return utils.NewError(u.ValidateICalValue, "invalid URL object", u, err)
} else {
return nil
}
}
// creates a new iCalendar duration representation
func NewUrl(u url.URL) *Url {
return &Url{u: u}
}

37
vendor/github.com/dolanor/caldav-go/utils/error.go generated vendored Normal file
View File

@ -0,0 +1,37 @@
package utils
import (
"fmt"
"reflect"
"runtime"
)
type Error struct {
method interface{}
message string
context interface{}
cause error
}
func NewError(method interface{}, message string, context interface{}, cause error) *Error {
e := new(Error)
e.method = method
e.message = message
e.context = context
e.cause = cause
return e
}
func (e *Error) Error() string {
pc := reflect.ValueOf(e.method).Pointer()
fn := runtime.FuncForPC(pc).Name()
msg := fmt.Sprintf("error: %s\nfunc: %s", e.message, fn)
if e.context != nil {
tname := reflect.ValueOf(e.context).Type()
msg = fmt.Sprintf("%s\ncontext: %s", msg, tname.String())
}
if e.cause != nil {
msg = fmt.Sprintf("%s\ncause: %s", msg, e.cause.Error())
}
return msg
}

119
vendor/github.com/dolanor/caldav-go/webdav/client.go generated vendored Normal file
View File

@ -0,0 +1,119 @@
package webdav
import (
"fmt"
"github.com/dolanor/caldav-go/http"
"github.com/dolanor/caldav-go/utils"
"github.com/dolanor/caldav-go/webdav/entities"
nhttp "net/http"
)
const (
StatusMulti = 207
)
// a client for making WebDAV requests
type Client http.Client
// downcasts the client to the local HTTP interface
func (c *Client) Http() *http.Client {
return (*http.Client)(c)
}
// returns the embedded WebDav server reference
func (c *Client) Server() *Server {
return (*Server)(c.Http().Server())
}
// executes a WebDAV request
func (c *Client) Do(req *Request) (*Response, error) {
if resp, err := c.Http().Do((*http.Request)(req)); err != nil {
return nil, utils.NewError(c.Do, "unable to execute WebDAV request", c, err)
} else {
return NewResponse(resp), nil
}
}
// checks if a resource exists given a particular path
func (c *Client) Exists(path string) (bool, error) {
if req, err := c.Server().NewRequest("HEAD", path); err != nil {
return false, utils.NewError(c.Exists, "unable to create request", c, err)
} else if resp, err := c.Do(req); err != nil {
return false, utils.NewError(c.Exists, "unable to execute request", c, err)
} else {
return resp.StatusCode != nhttp.StatusNotFound, nil
}
}
// deletes a resource if it exists on a particular path
func (c *Client) Delete(path string) error {
if req, err := c.Server().NewRequest("DELETE", path); err != nil {
return utils.NewError(c.Delete, "unable to create request", c, err)
} else if resp, err := c.Do(req); err != nil {
return utils.NewError(c.Delete, "unable to execute request", c, err)
} else if resp.StatusCode != nhttp.StatusNoContent && resp.StatusCode != nhttp.StatusNotFound {
err := new(entities.Error)
resp.Decode(err)
msg := fmt.Sprintf("unexpected server response %s", resp.Status)
return utils.NewError(c.Delete, msg, c, err)
} else {
return nil
}
}
// fetches a list of WebDAV features supported by the server
// returns an error if the server does not support DAV
func (c *Client) Features(path string) ([]string, error) {
if req, err := c.Server().NewRequest("OPTIONS", path); err != nil {
return []string{}, utils.NewError(c.Features, "unable to create request", c, err)
} else if resp, err := c.Do(req); err != nil {
return []string{}, utils.NewError(c.Features, "unable to execute request", c, err)
} else {
return resp.Features(), nil
}
}
// returns an error if the server does not support WebDAV
func (c *Client) ValidateServer(path string) error {
if features, err := c.Features(path); err != nil {
return utils.NewError(c.ValidateServer, "feature detection failed", c, err)
} else if len(features) <= 0 {
return utils.NewError(c.ValidateServer, "no DAV headers found", c, err)
} else {
return nil
}
}
// executes a PROPFIND request against the WebDAV server
// returns a multistatus XML entity
func (c *Client) Propfind(path string, depth Depth, pf *entities.Propfind) (*entities.Multistatus, error) {
ms := new(entities.Multistatus)
if req, err := c.Server().NewRequest("PROPFIND", path, pf); err != nil {
return nil, utils.NewError(c.Propfind, "unable to create request", c, err)
} else if req.Http().Native().Header.Set("Depth", string(depth)); depth == "" {
return nil, utils.NewError(c.Propfind, "search depth must be defined", c, nil)
} else if resp, err := c.Do(req); err != nil {
return nil, utils.NewError(c.Propfind, "unable to execute request", c, err)
} else if resp.StatusCode != StatusMulti {
msg := fmt.Sprintf("unexpected status: %s", resp.Status)
return nil, utils.NewError(c.Propfind, msg, c, nil)
} else if err := resp.Decode(ms); err != nil {
return nil, utils.NewError(c.Propfind, "unable to decode response", c, err)
}
return ms, nil
}
// creates a new client for communicating with an WebDAV server
func NewClient(server *Server, native *nhttp.Client) *Client {
return (*Client)(http.NewClient((*http.Server)(server), native))
}
// creates a new client for communicating with a WebDAV server
// uses the default HTTP client from net/http
func NewDefaultClient(server *Server) *Client {
return NewClient(server, nhttp.DefaultClient)
}

9
vendor/github.com/dolanor/caldav-go/webdav/depth.go generated vendored Normal file
View File

@ -0,0 +1,9 @@
package webdav
type Depth string
const (
Depth0 Depth = "0"
Depth1 = "1"
DepthInfinity = "infinity"
)

View File

@ -0,0 +1,18 @@
package entities
import "encoding/xml"
// a WebDAV error
type Error struct {
XMLName xml.Name `xml:"DAV: error"`
Description string `xml:"error-description,omitempty"`
Message string `xml:"message,omitempty"`
}
func (e *Error) Error() string {
if e.Description != "" {
return e.Description
} else {
return e.Message
}
}

View File

@ -0,0 +1,23 @@
package entities
import "encoding/xml"
// metadata about a property
type PropStat struct {
XMLName xml.Name `xml:"propstat"`
Status string `xml:"status"`
Prop *Prop `xml:",omitempty"`
}
// a multistatus response entity
type Response struct {
XMLName xml.Name `xml:"response"`
Href string `xml:"href"`
PropStats []*PropStat `xml:"propstat,omitempty"`
}
// a request to find properties on an an entity or collection
type Multistatus struct {
XMLName xml.Name `xml:"DAV: multistatus"`
Responses []*Response `xml:"response,omitempty"`
}

View File

@ -0,0 +1,32 @@
package entities
import (
"encoding/xml"
)
// a property of a resource
type Prop struct {
XMLName xml.Name `xml:"DAV: prop"`
GetContentType string `xml:"getcontenttype,omitempty"`
DisplayName string `xml:"displayname,omitempty"`
ResourceType *ResourceType `xml:",omitempty"`
CTag string `xml:"http://calendarserver.org/ns/ getctag,omitempty"`
ETag string `xml:"http://calendarserver.org/ns/ getetag,omitempty"`
}
// the type of a resource
type ResourceType struct {
XMLName xml.Name `xml:"resourcetype"`
Collection *ResourceTypeCollection `xml:",omitempty"`
Calendar *ResourceTypeCalendar `xml:",omitempty"`
}
// A calendar resource type
type ResourceTypeCalendar struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav calendar"`
}
// A collection resource type
type ResourceTypeCollection struct {
XMLName xml.Name `xml:"collection"`
}

View File

@ -0,0 +1,20 @@
package entities
import "encoding/xml"
// a request to find properties on an an entity or collection
type Propfind struct {
XMLName xml.Name `xml:"DAV: propfind"`
AllProp *AllProp `xml:",omitempty"`
Props []*Prop `xml:"prop,omitempty"`
}
// a propfind property representing all properties
type AllProp struct {
XMLName xml.Name `xml:"allprop"`
}
// a convenience method for searching all properties
func NewAllPropsFind() *Propfind {
return &Propfind{AllProp: new(AllProp)}
}

56
vendor/github.com/dolanor/caldav-go/webdav/request.go generated vendored Normal file
View File

@ -0,0 +1,56 @@
package webdav
import (
"bytes"
"encoding/xml"
"github.com/dolanor/caldav-go/http"
"github.com/dolanor/caldav-go/utils"
"io"
"io/ioutil"
"log"
"strings"
)
var _ = log.Print
// an WebDAV request object
type Request http.Request
// downcasts the request to the local HTTP interface
func (r *Request) Http() *http.Request {
return (*http.Request)(r)
}
// creates a new WebDAV request object
func NewRequest(method string, urlstr string, xmldata ...interface{}) (*Request, error) {
if buffer, length, err := xmlToReadCloser(xmldata); err != nil {
return nil, utils.NewError(NewRequest, "unable to encode xml data", xmldata, err)
} else if r, err := http.NewRequest(method, urlstr, buffer); err != nil {
return nil, utils.NewError(NewRequest, "unable to create request", urlstr, err)
} else {
if buffer != nil {
// set the content type to XML if we have a body
r.Native().Header.Set("Content-Type", "text/xml; charset=UTF-8")
r.ContentLength = int64(length)
}
return (*Request)(r), nil
}
}
func xmlToReadCloser(xmldata ...interface{}) (io.ReadCloser, int, error) {
var buffer []string
for _, xmldatum := range xmldata {
if encoded, err := xml.Marshal(xmldatum); err != nil {
return nil, 0, utils.NewError(xmlToReadCloser, "unable to encode as xml", xmldatum, err)
} else {
buffer = append(buffer, string(encoded))
}
}
if len(buffer) > 0 {
var encoded = strings.Join(buffer, "\n")
// log.Printf("[WebDAV Request]\n%+v\n", encoded)
return ioutil.NopCloser(bytes.NewBuffer([]byte(encoded))), len(encoded), nil
} else {
return nil, 0, nil
}
}

54
vendor/github.com/dolanor/caldav-go/webdav/response.go generated vendored Normal file
View File

@ -0,0 +1,54 @@
package webdav
import (
"encoding/xml"
"github.com/dolanor/caldav-go/http"
"github.com/dolanor/caldav-go/utils"
"io/ioutil"
"log"
"strings"
)
var _ = log.Print
var _ = ioutil.ReadAll
// a WebDAV response object
type Response http.Response
// downcasts the response to the local HTTP interface
func (r *Response) Http() *http.Response {
return (*http.Response)(r)
}
// returns a list of WebDAV features found in the response
func (r *Response) Features() (features []string) {
if dav := r.Header.Get("DAV"); dav != "" {
features = strings.Split(dav, ", ")
}
return
}
// decodes a WebDAV XML response into the provided interface
func (r *Response) Decode(into interface{}) error {
// data, _ := ioutil.ReadAll(r.Body)
// log.Printf("[WebDAV Response]\n%+v\n", string(data))
// if err := xml.Unmarshal(data, into); err != nil {
// return utils.NewError(r.Decode, "unable to decode response body", r, err)
// } else {
// return nil
// }
if body := r.Body; body == nil {
return nil
} else if decoder := xml.NewDecoder(body); decoder == nil {
return nil
} else if err := decoder.Decode(into); err != nil {
return utils.NewError(r.Decode, "unable to decode response body", r, err)
} else {
return nil
}
}
// creates a new WebDAV response object
func NewResponse(response *http.Response) *Response {
return (*Response)(response)
}

28
vendor/github.com/dolanor/caldav-go/webdav/server.go generated vendored Normal file
View File

@ -0,0 +1,28 @@
package webdav
import (
"github.com/dolanor/caldav-go/http"
"github.com/dolanor/caldav-go/utils"
)
// a server that accepts WebDAV requests
type Server http.Server
// creates a reference to an WebDAV server
func NewServer(baseUrlStr string) (*Server, error) {
if s, err := http.NewServer(baseUrlStr); err != nil {
return nil, utils.NewError(NewServer, "unable to create WebDAV server", baseUrlStr, err)
} else {
return (*Server)(s), nil
}
}
// downcasts the server to the local HTTP interface
func (s *Server) Http() *http.Server {
return (*http.Server)(s)
}
// creates a new WebDAV request object
func (s *Server) NewRequest(method string, path string, xmldata ...interface{}) (*Request, error) {
return NewRequest(method, s.Http().AbsUrlStr(path), xmldata...)
}

13
vendor/modules.txt vendored
View File

@ -4,6 +4,19 @@ github.com/beorn7/perks/quantile
# github.com/cespare/xxhash/v2 v2.1.2
## explicit; go 1.11
github.com/cespare/xxhash/v2
# github.com/dolanor/caldav-go v0.2.1
## explicit; go 1.13
github.com/dolanor/caldav-go/caldav
github.com/dolanor/caldav-go/caldav/entities
github.com/dolanor/caldav-go/caldav/values
github.com/dolanor/caldav-go/http
github.com/dolanor/caldav-go/icalendar
github.com/dolanor/caldav-go/icalendar/components
github.com/dolanor/caldav-go/icalendar/properties
github.com/dolanor/caldav-go/icalendar/values
github.com/dolanor/caldav-go/utils
github.com/dolanor/caldav-go/webdav
github.com/dolanor/caldav-go/webdav/entities
# github.com/golang/protobuf v1.5.2
## explicit; go 1.9
github.com/golang/protobuf/proto