├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── decoder.go ├── decoder_line.go ├── decoder_line_test.go ├── decoder_test.go ├── decoder_types.go ├── decoder_types_test.go ├── doc.go ├── encoder.go ├── encoder_test.go ├── examples └── mysqlsource │ ├── README.md │ ├── config.json │ ├── main.go │ └── models.go ├── fixtures ├── sunrise_google.ics └── test.ics ├── go.mod ├── go.sum ├── goics.go ├── goics_test.go ├── string.go └── string_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | matrix: 4 | include: 5 | - go: "tip" 6 | - go: "1.x" 7 | - go: "1.16" 8 | - go: "1.15" 9 | - go: "1.14" 10 | - go: "1.13" 11 | - go: "1.12" 12 | - go: "1.11" 13 | - go: "1.10" 14 | - go: "1.9" 15 | - go: "1.8" 16 | allow_failures: 17 | - go: tip 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jordi Collell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### A go toolkit for decoding, encoding icalendar ics ical files 3 | 4 | Alpha status 5 | 6 | [![Build Status](https://travis-ci.org/jordic/goics.svg?branch=master)](https://travis-ci.org/jordic/goics) 7 | 8 | After trying to decode some .ics files and review available go packages, I decided to start writing this pacakge. 9 | 10 | First attempt was from a fixed structure, similar to that needed. Later, I started to investigate the format and discovered that it is a pain, and has many variants, depending on who implements it. For this reason I evolved it to a tookit for decode and encode the format. 11 | 12 | **Check examples dir for user cases:** 13 | 14 | [Demo app encoding an ical, using a mysql DB source](examples/mysqlsource) 15 | 16 | 17 | Features implemented: 18 | 19 | - Parsing and writing of vevent, not completly.. 20 | - No recursive events, 21 | - And no alarms inside events 22 | 23 | 24 | Follows: 25 | http://tools.ietf.org/html/rfc5545 26 | 27 | 28 | TODO 29 | -- 30 | 31 | Integrate testing from: 32 | https://github.com/libical/libical 33 | 34 | 35 | CHANGELOG: 36 | -- 37 | 38 | - v00. First api traial 39 | - v01. Api evolves to a ical toolkit 40 | - v02. Added example of integration with a mysql db. 41 | 42 | Thanks to: 43 | Joe Shaw Reviewing first revision. 44 | 45 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package goics 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | keySep = ":" 12 | vBegin = "BEGIN" 13 | vCalendar = "VCALENDAR" 14 | vEnd = "END" 15 | vEvent = "VEVENT" 16 | 17 | maxLineRead = 65 18 | ) 19 | 20 | // Errors 21 | var ( 22 | ErrCalendarNotFound = errors.New("vCalendar not found") 23 | ErrParseEndCalendar = errors.New("wrong format END:VCALENDAR not Found") 24 | ) 25 | 26 | type decoder struct { 27 | scanner *bufio.Scanner 28 | err error 29 | Calendar *Calendar 30 | currentEvent *Event 31 | nextFn stateFn 32 | prevFn stateFn 33 | current string 34 | buffered string 35 | line int 36 | } 37 | 38 | type stateFn func(*decoder) 39 | 40 | // NewDecoder creates an instance of de decoder 41 | func NewDecoder(r io.Reader) *decoder { 42 | d := &decoder{ 43 | scanner: bufio.NewScanner(r), 44 | nextFn: decodeInit, 45 | line: 0, 46 | buffered: "", 47 | } 48 | return d 49 | } 50 | 51 | func (d *decoder) Decode(c ICalConsumer) error { 52 | d.next() 53 | if d.Calendar == nil { 54 | d.err = ErrCalendarNotFound 55 | d.Calendar = &Calendar{} 56 | } 57 | // If theres no error but, nextFn is not reset 58 | // last element not closed 59 | if d.nextFn != nil && d.err == nil { 60 | d.err = ErrParseEndCalendar 61 | } 62 | if d.err != nil { 63 | return d.err 64 | } 65 | 66 | d.err = c.ConsumeICal(d.Calendar, d.err) 67 | return d.err 68 | } 69 | 70 | // Lines processed. If Decoder reports an error. 71 | // Error 72 | func (d *decoder) Lines() int { 73 | return d.line 74 | } 75 | 76 | // Current Line content 77 | func (d *decoder) CurrentLine() string { 78 | return d.current 79 | } 80 | 81 | // Advances a new line in the decoder 82 | // And calls the next stateFunc 83 | // checks if next line is continuation line 84 | func (d *decoder) next() { 85 | // If there's not buffered line 86 | if d.buffered == "" { 87 | res := d.scanner.Scan() 88 | if true != res { 89 | d.err = d.scanner.Err() 90 | return 91 | } 92 | d.line++ 93 | d.current = d.scanner.Text() 94 | } else { 95 | d.current = d.buffered 96 | d.buffered = "" 97 | } 98 | 99 | for d.scanner.Scan() { 100 | d.line++ 101 | line := d.scanner.Text() 102 | if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { 103 | d.current = d.current + line[1:] 104 | } else { 105 | // If is not a continuation line, buffer it, for the 106 | // next call. 107 | d.buffered = line 108 | break 109 | } 110 | } 111 | 112 | d.err = d.scanner.Err() 113 | 114 | if d.nextFn != nil { 115 | d.nextFn(d) 116 | } 117 | } 118 | 119 | func decodeInit(d *decoder) { 120 | node := DecodeLine(d.current) 121 | if node.Key == vBegin && node.Val == vCalendar { 122 | d.Calendar = &Calendar{ 123 | Data: make(map[string]*IcsNode), 124 | } 125 | d.prevFn = decodeInit 126 | d.nextFn = decodeInsideCal 127 | d.next() 128 | return 129 | } 130 | d.next() 131 | } 132 | 133 | func decodeInsideCal(d *decoder) { 134 | node := DecodeLine(d.current) 135 | switch { 136 | case node.Key == vBegin && node.Val == vEvent: 137 | d.currentEvent = &Event{ 138 | Data: make(map[string]*IcsNode), 139 | List: make(map[string][]*IcsNode), 140 | } 141 | d.nextFn = decodeInsideEvent 142 | d.prevFn = decodeInsideCal 143 | case node.Key == vEnd && node.Val == vCalendar: 144 | d.nextFn = nil 145 | default: 146 | d.Calendar.Data[node.Key] = node 147 | } 148 | d.next() 149 | } 150 | 151 | func decodeInsideEvent(d *decoder) { 152 | 153 | node := DecodeLine(d.current) 154 | if node.Key == vEnd && node.Val == vEvent { 155 | // Come back to parent node 156 | d.nextFn = d.prevFn 157 | d.Calendar.Events = append(d.Calendar.Events, d.currentEvent) 158 | d.next() 159 | return 160 | } 161 | //@todo handle Valarm 162 | //@todo handle error if we found a startevent without closing pass one 163 | // #2 handle multiple equal keys. ej. Attendee 164 | // List keys already set 165 | if _, ok := d.currentEvent.List[node.Key]; ok { 166 | d.currentEvent.List[node.Key] = append(d.currentEvent.List[node.Key], node) 167 | } else { 168 | // Multiple key detected... 169 | if val, ok := d.currentEvent.Data[node.Key]; ok { 170 | d.currentEvent.List[node.Key] = []*IcsNode{val, node} 171 | delete(d.currentEvent.Data, node.Key) 172 | } else { 173 | d.currentEvent.Data[node.Key] = node 174 | } 175 | } 176 | d.next() 177 | 178 | } 179 | -------------------------------------------------------------------------------- /decoder_line.go: -------------------------------------------------------------------------------- 1 | package goics 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const ( 8 | vParamSep = ";" 9 | ) 10 | 11 | // IcsNode is a basic token.., with, key, val, and extra params 12 | // to define the type of val. 13 | type IcsNode struct { 14 | Key string 15 | Val string 16 | Params map[string]string 17 | } 18 | 19 | // ParamsLen returns how many params has a token 20 | func (n *IcsNode) ParamsLen() int { 21 | if n.Params == nil { 22 | return 0 23 | } 24 | return len(n.Params) 25 | } 26 | 27 | // GetOneParam resturns the first param found 28 | // usefull when you know that there is a only 29 | // one param token 30 | func (n *IcsNode) GetOneParam() (string, string) { 31 | if n.ParamsLen() == 0 { 32 | return "", "" 33 | } 34 | var key, val string 35 | for k, v := range n.Params { 36 | key, val = k, v 37 | break 38 | } 39 | return key, val 40 | } 41 | 42 | // DecodeLine extracts key, val and extra params from a line 43 | func DecodeLine(line string) *IcsNode { 44 | if strings.Contains(line, keySep) == false { 45 | return &IcsNode{} 46 | } 47 | key, val := getKeyVal(line) 48 | //@todo test if val containes , multipleparams 49 | if strings.Contains(key, vParamSep) == false { 50 | return &IcsNode{ 51 | Key: key, 52 | Val: val, 53 | } 54 | } 55 | // Extract key 56 | firstParam := strings.Index(key, vParamSep) 57 | realkey := key[0:firstParam] 58 | n := &IcsNode{ 59 | Key: realkey, 60 | Val: val, 61 | } 62 | // Extract params 63 | params := key[firstParam+1:] 64 | n.Params = decodeParams(params) 65 | return n 66 | } 67 | 68 | // decode extra params linked in key val in the form 69 | // key;param1=val1:val 70 | func decodeParams(arr string) map[string]string { 71 | 72 | p := make(map[string]string) 73 | var isQuoted = false 74 | var isParam = true 75 | var curParam string 76 | var curVal string 77 | for _, c := range arr { 78 | switch { 79 | // if string is quoted, wait till next quote 80 | // and capture content 81 | case c == '"': 82 | if isQuoted == false { 83 | isQuoted = true 84 | } else { 85 | p[curParam] = curVal 86 | isQuoted = false 87 | } 88 | case c == '=' && isQuoted == false: 89 | isParam = false 90 | case c == ';' && isQuoted == false: 91 | isParam = true 92 | p[curParam] = curVal 93 | curParam = "" 94 | curVal = "" 95 | default: 96 | if isParam { 97 | curParam = curParam + string(c) 98 | } else { 99 | curVal = curVal + string(c) 100 | } 101 | } 102 | } 103 | p[curParam] = curVal 104 | return p 105 | 106 | } 107 | 108 | // Returns a key, val... for a line.. 109 | func getKeyVal(s string) (key, value string) { 110 | p := strings.SplitN(s, keySep, 2) 111 | return p[0], icsReplacer.Replace(p[1]) 112 | } 113 | 114 | var icsReplacer = strings.NewReplacer( 115 | `\,`, ",", 116 | `\;`, ";", 117 | `\\`, `\`, 118 | `\n`, "\n", 119 | `\N`, "\n", 120 | ) 121 | -------------------------------------------------------------------------------- /decoder_line_test.go: -------------------------------------------------------------------------------- 1 | package goics_test 2 | 3 | import ( 4 | //"strings" 5 | "testing" 6 | 7 | goics "github.com/jordic/goics" 8 | ) 9 | 10 | var TLines = []string{ 11 | "BEGIN:VEVENT", 12 | `ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:jsmith@example.com`, 13 | `X-FOO;BAR=";hidden=value";FOO=baz:realvalue`, 14 | `ATTENDEE;ROLE="REQ-PARTICIPANT;foo";PARTSTAT=ACCEPTED;RSVP=TRUE:mailto:foo@bar.com`, 15 | } 16 | 17 | func TestDecodeLine(t *testing.T) { 18 | node := goics.DecodeLine(TLines[0]) 19 | if node.Key != "BEGIN" { 20 | t.Errorf("Wrong key parsing %s", node.Key) 21 | } 22 | if node.Val != "VEVENT" { 23 | t.Errorf("Wrong key parsing %s", node.Key) 24 | } 25 | if node.ParamsLen() != 0 { 26 | t.Error("No keys") 27 | } 28 | 29 | node = goics.DecodeLine(TLines[1]) 30 | if node.Key != "ATTENDEE" { 31 | t.Errorf("Wrong key parsing %s", node.Key) 32 | } 33 | if node.Val != "mailto:jsmith@example.com" { 34 | t.Errorf("Wrong val parsing %s", node.Val) 35 | } 36 | if node.ParamsLen() != 2 { 37 | t.Errorf("Wrong nmber of params %d", node.ParamsLen()) 38 | } 39 | node = goics.DecodeLine(TLines[2]) 40 | if node.Key != "X-FOO" { 41 | t.Errorf("Wrong key parsing %s", node.Key) 42 | } 43 | if node.ParamsLen() != 2 { 44 | t.Errorf("Incorrect quoted params count %d, %v", node.ParamsLen(), node.Params) 45 | 46 | } 47 | node = goics.DecodeLine(TLines[3]) 48 | if node.Key != "ATTENDEE" { 49 | t.Errorf("Wrong key parsing %s", node.Key) 50 | } 51 | if node.ParamsLen() != 3 { 52 | t.Errorf("Incorrect quoted params count %d, %v", node.ParamsLen(), node.Params) 53 | } 54 | if node.Params["ROLE"] != "REQ-PARTICIPANT;foo" { 55 | t.Errorf("Error extracting quoted params from line %s", node.Params["ROLE"]) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /decoder_test.go: -------------------------------------------------------------------------------- 1 | package goics_test 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | goics "github.com/jordic/goics" 10 | ) 11 | 12 | func TestTesting(t *testing.T) { 13 | if 1 != 1 { 14 | t.Error("Error setting up testing") 15 | } 16 | } 17 | 18 | type Calendar struct { 19 | Data map[string]string 20 | } 21 | 22 | func (e *Calendar) ConsumeICal(c *goics.Calendar, err error) error { 23 | for k, v := range c.Data { 24 | e.Data[k] = v.Val 25 | } 26 | return err 27 | } 28 | 29 | func NewCal() *Calendar { 30 | return &Calendar{ 31 | Data: make(map[string]string), 32 | } 33 | } 34 | 35 | var source = "asdf\nasdf\nasdf" 36 | 37 | func TestEndOfFile(t *testing.T) { 38 | a := goics.NewDecoder(strings.NewReader(source)) 39 | err := a.Decode(&Calendar{}) 40 | if err != goics.ErrCalendarNotFound { 41 | t.Errorf("Decode filed, decode raised %s", err) 42 | } 43 | if a.Lines() != 3 { 44 | t.Errorf("Decode should advance to %d", a.Lines()) 45 | } 46 | 47 | } 48 | 49 | var sourceEOFMultiline = `BEGIN:VCALENDAR 50 | VERSION:2.0 51 | FOO 52 | BAR 53 | BAR 54 | ` 55 | 56 | func TestEndOfFileMultiline(t *testing.T) { 57 | a := goics.NewDecoder(strings.NewReader(sourceEOFMultiline)) 58 | a.Decode(&Calendar{}) 59 | if expected := 5; a.Lines() != expected { 60 | t.Errorf("Decode should advance to %d but %d is returned", expected, a.Lines()) 61 | } 62 | 63 | } 64 | 65 | var test2 = `BEGIN:VCALENDAR 66 | PRODID;X-RICAL-TZSOURCE=TZINFO:-//test//EN 67 | CALSCALE:GREGORIAN 68 | VERSION:2.0 69 | END:VCALENDAR 70 | 71 | ` 72 | 73 | func TestInsideCalendar(t *testing.T) { 74 | a := goics.NewDecoder(strings.NewReader(test2)) 75 | consumer := NewCal() 76 | err := a.Decode(consumer) 77 | if err != nil { 78 | t.Errorf("Failed %s", err) 79 | } 80 | if consumer.Data["CALSCALE"] != "GREGORIAN" { 81 | t.Error("No extra keys for calendar decoded") 82 | } 83 | if consumer.Data["VERSION"] != "2.0" { 84 | t.Error("No extra keys for calendar decoded") 85 | } 86 | } 87 | 88 | var test3 = `BEGIN:VCALENDAR 89 | PRODID;X-RICAL-TZSOURCE=TZINFO:-//test//EN 90 | CALSCALE:GREGORIAN 91 | VERSION:2.` 92 | 93 | func TestDetectIncompleteCalendar(t *testing.T) { 94 | a := goics.NewDecoder(strings.NewReader(test3)) 95 | err := a.Decode(&Calendar{}) 96 | if err != goics.ErrParseEndCalendar { 97 | t.Error("Test failed") 98 | } 99 | 100 | } 101 | 102 | var testlonglines = `BEGIN:VCALENDAR 103 | PRODID;X-RICAL-TZSOURCE=TZINFO:-//test//EN 104 | CALSCALE:GREGORIANGREGORIANGREGORIANGREGORIANGREGORIANGREGORIANGREGORIAN 105 | GREGORIANGREGORIAN 106 | VERSION:2.0 107 | END:VCALENDAR 108 | ` 109 | 110 | func TestParseLongLines(t *testing.T) { 111 | a := goics.NewDecoder(strings.NewReader(testlonglines)) 112 | cons := NewCal() 113 | _ = a.Decode(cons) 114 | str := cons.Data["CALSCALE"] 115 | if len(str) != 81 { 116 | t.Errorf("Multiline test failed %d", len(cons.Data["CALSCALE"])) 117 | } 118 | if strings.Contains("str", " ") { 119 | t.Error("Not handling correct begining of line") 120 | } 121 | 122 | } 123 | 124 | var testlonglinestab = `BEGIN:VCALENDAR 125 | PRODID;X-RICAL-TZSOURCE=TZINFO:-//test//EN 126 | CALSCALE:GREGORIANGREGORIANGREGORIANGREGORIANGREGORIANGREGORIANGREGORIAN 127 | GREGORIANGREGORIAN 128 | VERSION:2.0 129 | END:VCALENDAR 130 | ` 131 | 132 | func TestParseLongLinesTab(t *testing.T) { 133 | a := goics.NewDecoder(strings.NewReader(testlonglinestab)) 134 | cons := NewCal() 135 | _ = a.Decode(cons) 136 | str := cons.Data["CALSCALE"] 137 | 138 | if len(str) != 81 { 139 | t.Errorf("Multiline tab field test failed %d", len(str)) 140 | } 141 | if strings.Contains("str", "\t") { 142 | t.Error("Not handling correct begining of line") 143 | } 144 | 145 | } 146 | 147 | var testlonglinestab3 = `BEGIN:VCALENDAR 148 | PRODID;X-RICAL-TZSOURCE=TZINFO:-//test//EN 149 | CALSCALE:GREGORIANGREGORIANGREGORIANGREGORIANGREGORIANGREGORIANGREGORIAN 150 | GREGORIANGREGORIANGREGORIANGREGORIANGREGORIANGREGORIANGREGORIANGREGORIANGG 151 | GRESESERSERSER 152 | VERSION:2.0 153 | END:VCALENDAR 154 | ` 155 | 156 | func TestParseLongLinesMultilinethree(t *testing.T) { 157 | a := goics.NewDecoder(strings.NewReader(testlonglinestab3)) 158 | cons := NewCal() 159 | _ = a.Decode(cons) 160 | str := cons.Data["CALSCALE"] 161 | if len(str) != 151 { 162 | t.Errorf("Multiline (3lines) tab field test failed %d", len(str)) 163 | } 164 | if strings.Contains("str", "\t") { 165 | t.Error("Not handling correct begining of line") 166 | } 167 | 168 | } 169 | 170 | var valarmCt = `BEGIN:VCALENDAR 171 | BEGIN:VEVENT 172 | STATUS:CONFIRMED 173 | CREATED:20131205T115046Z 174 | UID:1ar5d7dlf0ddpcih9jum017tr4@google.com 175 | DTEND;VALUE=DATE:20140111 176 | TRANSP:OPAQUE 177 | SUMMARY:PASTILLA Cu cs 178 | DTSTART;VALUE=DATE:20140110 179 | DTSTAMP:20131205T115046Z 180 | LAST-MODIFIED:20131205T115046Z 181 | SEQUENCE:0 182 | DESCRIPTION: 183 | BEGIN:VALARM 184 | X-WR-ALARMUID:E283310A-82B3-47CF-A598-FD36634B21F3 185 | UID:E283310A-82B3-47CF-A598-FD36634B21F3 186 | TRIGGER:-PT15H 187 | X-APPLE-DEFAULT-ALARM:TRUE 188 | ATTACH;VALUE=URI:Basso 189 | ACTION:AUDIO 190 | END:VALARM 191 | END:VEVENT 192 | END:VCALENDAR` 193 | 194 | func TestNotParsingValarm(t *testing.T) { 195 | a := goics.NewDecoder(strings.NewReader(valarmCt)) 196 | cons := NewCal() 197 | err := a.Decode(cons) 198 | 199 | if err != nil { 200 | t.Errorf("Error decoding %s", err) 201 | } 202 | } 203 | 204 | var testEscapeValue = `BEGIN:VCALENDAR 205 | X-TEST:one\, two\, three\n\Nfour\;five\\six 206 | END:VCALENDAR` 207 | 208 | func TestUnescapeValue(t *testing.T) { 209 | a := goics.NewDecoder(strings.NewReader(testEscapeValue)) 210 | cons := NewCal() 211 | err := a.Decode(cons) 212 | 213 | if err != nil { 214 | t.Errorf("Error decoding %s", err) 215 | } 216 | 217 | expected := "one, two, three\n\nfour;five\\six" 218 | if actual := cons.Data["X-TEST"]; expected != actual { 219 | t.Errorf("unexpected summary: %q", actual) 220 | } 221 | } 222 | 223 | func TestReadingRealFile(t *testing.T) { 224 | 225 | file, err := os.Open("fixtures/test.ics") 226 | if err != nil { 227 | t.Error("Can't read file") 228 | } 229 | defer file.Close() 230 | 231 | cal := goics.NewDecoder(file) 232 | cons := NewCal() 233 | err = cal.Decode(cons) 234 | if err != nil { 235 | t.Error("Cant decode a complete file") 236 | } 237 | 238 | if len(cal.Calendar.Events) != 28 { 239 | t.Errorf("Wrong number of events detected %d", len(cal.Calendar.Events)) 240 | } 241 | 242 | } 243 | 244 | // Multiple keys with same name... 245 | // From libical tests 246 | // https://github.com/libical/libical/blob/master/test-data/incoming.ics 247 | 248 | var dataMultipleAtendee = `BEGIN:VCALENDAR 249 | PRODID:-//ACME/DesktopCalendar//EN 250 | METHOD:REQUEST 251 | X-LIC-NOTE:#I3. Updates C1 252 | X-LIC-EXPECT:REQUEST-UPDATE 253 | VERSION:2.0 254 | BEGIN:VEVENT 255 | ORGANIZER:Mailto:B@example.com 256 | ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=BIG A:Mailto:A@example.com 257 | ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:Mailto:B@example.com 258 | ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=C:Mailto:C@example.com 259 | ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal:Mailto:D@example.com 260 | ATTENDEE;RSVP=FALSE;CUTYPE=ROOM:conf_Big@example.com 261 | ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE:Mailto:E@example.com 262 | DTSTAMP:19970611T193000Z 263 | DTSTART:19970701T190000Z 264 | DTEND:19970701T193000Z 265 | SUMMARY: Pool party 266 | UID:calsrv.example.com-873970198738777@example.com 267 | SEQUENCE:2 268 | STATUS:CONFIRMED 269 | END:VEVENT 270 | END:VCALENDAR` 271 | 272 | type EventA struct { 273 | Start, End time.Time 274 | ID, Summary string 275 | Attendees []string 276 | } 277 | 278 | type EventsA []EventA 279 | 280 | func (e *EventsA) ConsumeICal(c *goics.Calendar, err error) error { 281 | for _, el := range c.Events { 282 | node := el.Data 283 | dtstart, err := node["DTSTART"].DateDecode() 284 | if err != nil { 285 | return err 286 | } 287 | dtend, err := node["DTEND"].DateDecode() 288 | if err != nil { 289 | return err 290 | } 291 | d := EventA{ 292 | Start: dtstart, 293 | End: dtend, 294 | ID: node["UID"].Val, 295 | Summary: node["SUMMARY"].Val, 296 | } 297 | // Get Atendees 298 | if val, ok := el.List["ATTENDEE"]; ok { 299 | d.Attendees = make([]string, 0) 300 | for _, n := range val { 301 | 302 | d.Attendees = append(d.Attendees, n.Val) 303 | } 304 | } 305 | 306 | *e = append(*e, d) 307 | } 308 | return nil 309 | } 310 | 311 | func TestDataMultipleAtendee(t *testing.T) { 312 | 313 | d := goics.NewDecoder(strings.NewReader(dataMultipleAtendee)) 314 | consumer := EventsA{} 315 | err := d.Decode(&consumer) 316 | if err != nil { 317 | t.Error("Error decoding events") 318 | } 319 | if len(consumer) != 1 { 320 | t.Error("Wrong size of consumer list..") 321 | } 322 | 323 | if len(consumer[0].Attendees) != 6 { 324 | t.Errorf("Wrong list of atendees detectet %d", len(consumer[0].Attendees)) 325 | } 326 | 327 | att := consumer[0].Attendees[0] 328 | if att != "Mailto:A@example.com" { 329 | t.Errorf("Atendee list should be %s", att) 330 | } 331 | 332 | } 333 | 334 | var dataMultiline = `BEGIN:VCALENDAR 335 | BEGIN:VEVENT 336 | ORGANIZER:Mailto:B@example.com 337 | ATTENDEE; 338 | ROLE=CHAIR; 339 | PARTSTAT 340 | =ACCEPTED;CN= 341 | BIG A 342 | :Mailto:A@example.com 343 | ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL; 344 | CN=B:Mailto:B@example.com 345 | ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=C:Mailto:C@example.com 346 | DTSTAMP:19970611T193000Z 347 | DTSTART:19970701T190000Z 348 | DTEND:19970701T193000Z 349 | UID:calsrv.example.com-873970198738777@example.com 350 | SUMMARY: Pool party 351 | END:VEVENT 352 | END:VCALENDAR` 353 | 354 | func TestMulitine(t *testing.T) { 355 | 356 | d := goics.NewDecoder(strings.NewReader(dataMultiline)) 357 | consumer := EventsA{} 358 | err := d.Decode(&consumer) 359 | if err != nil { 360 | t.Errorf("Error decoding events %+v", err) 361 | } 362 | if len(consumer) != 1 { 363 | t.Error("Wrong size of consumer list..") 364 | } 365 | 366 | if size := len(consumer[0].Attendees); size != 3 { 367 | t.Errorf("Wrong size of attendees detected %d", size) 368 | } 369 | 370 | if att := consumer[0].Attendees[0]; att != "Mailto:A@example.com" { 371 | t.Errorf("Attendees list should be %s", att) 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /decoder_types.go: -------------------------------------------------------------------------------- 1 | package goics 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // DateDecode Decodes a date in the distincts formats 8 | func (n *IcsNode) DateDecode() (time.Time, error) { 9 | 10 | // DTEND;VALUE=DATE:20140406 11 | if val, ok := n.Params["VALUE"]; ok { 12 | switch { 13 | case val == "DATE": 14 | t, err := time.Parse("20060102", n.Val) 15 | if err != nil { 16 | return time.Time{}, err 17 | } 18 | return t, nil 19 | case val == "DATE-TIME": 20 | t, err := time.Parse("20060102T150405", n.Val) 21 | if err != nil { 22 | return time.Time{}, err 23 | } 24 | return t, nil 25 | } 26 | } 27 | // DTSTART;TZID=Europe/Paris:20140116T120000 28 | if val, ok := n.Params["TZID"]; ok { 29 | loc, err := time.LoadLocation(val) 30 | if err != nil { 31 | return time.Time{}, err 32 | } 33 | t, err := time.ParseInLocation("20060102T150405", n.Val, loc) 34 | if err != nil { 35 | return time.Time{}, err 36 | } 37 | return t, nil 38 | } 39 | //DTSTART:19980119T070000Z utf datetime 40 | if len(n.Val) == 16 { 41 | t, err := time.Parse("20060102T150405Z", n.Val) 42 | if err != nil { 43 | return time.Time{}, err 44 | } 45 | return t, nil 46 | } 47 | 48 | return time.Time{}, nil 49 | } 50 | -------------------------------------------------------------------------------- /decoder_types_test.go: -------------------------------------------------------------------------------- 1 | package goics 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var datesTested = []string{ 9 | "DTEND;VALUE=DATE:20140406", 10 | "DTSTART;TZID=Europe/Paris:20140116T120000", 11 | "X-MYDATETIME;VALUE=DATE-TIME:20120901T130000", 12 | //"RDATE:20131210Z", 13 | "DTSTART:19980119T070000Z", 14 | } 15 | 16 | func getTimezone(zone string) *time.Location { 17 | t, _ := time.LoadLocation(zone) 18 | return t 19 | } 20 | 21 | var timesExpected = []time.Time{ 22 | time.Date(2014, time.April, 06, 0, 0, 0, 0, time.UTC), 23 | time.Date(2014, time.January, 16, 12, 0, 0, 0, getTimezone("Europe/Paris")), 24 | time.Date(2012, time.September, 01, 13, 0, 0, 0, time.UTC), 25 | time.Date(1998, time.January, 19, 07, 0, 0, 0, time.UTC), 26 | } 27 | 28 | func TestDateDecode(t *testing.T) { 29 | 30 | for i, d := range datesTested { 31 | 32 | node := DecodeLine(d) 33 | res, err := node.DateDecode() 34 | if err != nil { 35 | t.Errorf("Error decoding time %s", err) 36 | } 37 | if res.Equal(timesExpected[i]) == false { 38 | t.Errorf("Error parsing time %s expected %s", res, timesExpected[i]) 39 | } 40 | if res.String() != timesExpected[i].String() { 41 | t.Errorf("Error parsing time %s expected %s", res, timesExpected[i]) 42 | } 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package goics is a toolkit for encoding and decoding ics/Ical/icalendar files. 3 | 4 | This is a work in progress project, that will try to incorporate as many exceptions and variants of the format. 5 | 6 | This is a toolkit because user has to define the way it needs the data. The idea is builded with something similar to the consumer/provider pattern. 7 | 8 | We want to decode a stream of vevents from a .ics file into a custom type Events 9 | 10 | type Event struct { 11 | Start, End time.Time 12 | Id, Summary string 13 | } 14 | 15 | type Events []Event 16 | 17 | func (e *Events) ConsumeICal(c *goics.Calendar, err error) error { 18 | for _, el := range c.Events { 19 | node := el.Data 20 | dtstart, err := node["DTSTART"].DateDecode() 21 | if err != nil { 22 | return err 23 | } 24 | dtend, err := node["DTEND"].DateDecode() 25 | if err != nil { 26 | return err 27 | } 28 | d := Event{ 29 | Start: dtstart, 30 | End: dtend, 31 | Id: node["UID"].Val, 32 | Summary: node["SUMMARY"].Val, 33 | } 34 | *e = append(*e, d) 35 | } 36 | return nil 37 | } 38 | 39 | Our custom type, will need to implement ICalConsumer interface, where, 40 | the type will pick up data from the format. 41 | The decoding process will be somthing like this: 42 | 43 | 44 | d := goics.NewDecoder(strings.NewReader(testConsumer)) 45 | consumer := Events{} 46 | err := d.Decode(&consumer) 47 | 48 | 49 | I have choosed this model, because, this format is a pain and also I don't like a lot the reflect package. 50 | 51 | For encoding objects to iCal format, something similar has to be done: 52 | 53 | The object emitting elements for the encoder, will have to implement the ICalEmiter, returning a Component structure to be encoded. 54 | This also had been done, because every package could require to encode vals and keys their way. Just for encoding time, I found more than 55 | three types of lines. 56 | 57 | type EventTest struct { 58 | component goics.Componenter 59 | } 60 | 61 | func (evt *EventTest) EmitICal() goics.Componenter { 62 | return evt.component 63 | } 64 | 65 | 66 | The Componenter, is an interface that every Component that can be encoded to ical implements. 67 | 68 | c := goics.NewComponent() 69 | c.SetType("VCALENDAR") 70 | c.AddProperty("CALSCAL", "GREGORIAN") 71 | c.AddProperty("PRODID", "-//tmpo.io/src/goics") 72 | 73 | m := goics.NewComponent() 74 | m.SetType("VEVENT") 75 | m.AddProperty("UID", "testing") 76 | c.AddComponent(m) 77 | 78 | Properties, had to be stored as strings, the conversion from origin type to string format, must be done, 79 | on the emmiter. There are some helpers for date conversion and on the future I will add more, for encoding 80 | params on the string, and also for handling lists and recurrent events. 81 | 82 | A simple example not functional used for testing: 83 | 84 | c := goics.NewComponent() 85 | c.SetType("VCALENDAR") 86 | c.AddProperty("CALSCAL", "GREGORIAN") 87 | 88 | ins := &EventTest{ 89 | component: c, 90 | } 91 | 92 | w := &bytes.Buffer{} 93 | enc := goics.NewICalEncode(w) 94 | enc.Encode(ins) 95 | 96 | 97 | */ 98 | package goics 99 | -------------------------------------------------------------------------------- /encoder.go: -------------------------------------------------------------------------------- 1 | package goics 2 | 3 | import ( 4 | "io" 5 | "sort" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // Line endings 11 | const ( 12 | CRLF = "\r\n" 13 | CRLFSP = "\r\n " 14 | ) 15 | 16 | // NewComponent returns a new Component and setups 17 | // and setups Properties map for the component 18 | // and also allows more Components inside it. 19 | // VCALENDAR is a Component that has VEVENTS, 20 | // VEVENTS can hold VALARMS 21 | func NewComponent() *Component { 22 | return &Component{ 23 | Elements: make([]Componenter, 0), 24 | Properties: make(map[string][]string), 25 | } 26 | } 27 | 28 | // Component is the base type for holding a 29 | // ICal datatree before serilizing it 30 | type Component struct { 31 | Tipo string 32 | Elements []Componenter 33 | Properties map[string][]string 34 | } 35 | 36 | // Writes the component to the Writer 37 | func (c *Component) Write(w *ICalEncode) { 38 | w.WriteLine("BEGIN:" + c.Tipo + CRLF) 39 | 40 | // Iterate over component properties 41 | var keys []string 42 | for k := range c.Properties { 43 | keys = append(keys, k) 44 | } 45 | sort.Strings(keys) 46 | for _, key := range keys { 47 | vals := c.Properties[key] 48 | for _, val := range vals { 49 | w.WriteLine(WriteStringField(key, val)) 50 | } 51 | } 52 | 53 | for _, xc := range c.Elements { 54 | xc.Write(w) 55 | } 56 | 57 | w.WriteLine("END:" + c.Tipo + CRLF) 58 | } 59 | 60 | // SetType of the component, as 61 | // VCALENDAR VEVENT... 62 | func (c *Component) SetType(t string) { 63 | c.Tipo = t 64 | } 65 | 66 | // AddComponent to the base component, just for building 67 | // the component tree 68 | func (c *Component) AddComponent(cc Componenter) { 69 | c.Elements = append(c.Elements, cc) 70 | } 71 | 72 | // AddProperty adds a property to the component. 73 | func (c *Component) AddProperty(key string, val string) { 74 | c.Properties[key] = append(c.Properties[key], val) 75 | } 76 | 77 | // ICalEncode is the real writer, that wraps every line, 78 | // in 75 chars length... Also gets the component from the emmiter 79 | // and starts the iteration. 80 | type ICalEncode struct { 81 | w io.Writer 82 | } 83 | 84 | // NewICalEncode generates a new encoder, and needs a writer 85 | func NewICalEncode(w io.Writer) *ICalEncode { 86 | return &ICalEncode{ 87 | w: w, 88 | } 89 | } 90 | 91 | // Encode the Component into the ical format 92 | func (enc *ICalEncode) Encode(c ICalEmiter) { 93 | component := c.EmitICal() 94 | component.Write(enc) 95 | } 96 | 97 | // LineSize of the ics format 98 | var LineSize = 75 99 | 100 | // WriteLine in ics format max length = LineSize 101 | // continuation lines start with a space. 102 | func (enc *ICalEncode) WriteLine(s string) { 103 | if len(s) <= LineSize { 104 | io.WriteString(enc.w, s) 105 | return 106 | } 107 | 108 | // The first line does not begin with a space. 109 | firstLine := truncateString(s, LineSize-len(CRLF)) 110 | io.WriteString(enc.w, firstLine+CRLF) 111 | 112 | // Reserve three bytes for space + CRLF. 113 | lines := splitLength(s[len(firstLine):], LineSize-len(CRLFSP)) 114 | for i, line := range lines { 115 | if i < len(lines)-1 { 116 | io.WriteString(enc.w, " "+line+CRLF) 117 | } else { 118 | // This is the last line, don't append CRLF. 119 | io.WriteString(enc.w, " "+line) 120 | } 121 | } 122 | } 123 | 124 | // FormatDateField returns a formated date: "DTEND;VALUE=DATE:20140406" 125 | func FormatDateField(key string, val time.Time) (string, string) { 126 | return key + ";VALUE=DATE", val.Format("20060102") 127 | } 128 | 129 | // FormatDateTimeField in the form "X-MYDATETIME;VALUE=DATE-TIME:20120901T130000" 130 | func FormatDateTimeField(key string, val time.Time) (string, string) { 131 | return key + ";VALUE=DATE-TIME", val.Format("20060102T150405") 132 | } 133 | 134 | // FormatDateTime as "DTSTART:19980119T070000Z" 135 | func FormatDateTime(key string, val time.Time) (string, string) { 136 | return key, val.UTC().Format("20060102T150405Z") 137 | } 138 | 139 | // WriteStringField UID:asdfasdfаs@dfasdf.com 140 | func WriteStringField(key string, val string) string { 141 | return strings.ToUpper(key) + ":" + quoteString(val) + CRLF 142 | } 143 | 144 | func quoteString(s string) string { 145 | s = strings.Replace(s, "\\", "\\\\", -1) 146 | s = strings.Replace(s, ";", "\\;", -1) 147 | s = strings.Replace(s, ",", "\\,", -1) 148 | s = strings.Replace(s, "\n", "\\n", -1) 149 | 150 | return s 151 | } 152 | -------------------------------------------------------------------------------- /encoder_test.go: -------------------------------------------------------------------------------- 1 | package goics_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | goics "github.com/jordic/goics" 10 | ) 11 | 12 | func TestComponentCreation(t *testing.T) { 13 | 14 | c := goics.NewComponent() 15 | c.SetType("VCALENDAR") 16 | c.AddProperty("CALSCAL", "GREGORIAN") 17 | c.AddProperty("PRODID", "-//tmpo.io/src/goics") 18 | 19 | if c.Properties["CALSCAL"][0] != "GREGORIAN" { 20 | t.Error("Error adding property") 21 | } 22 | 23 | m := goics.NewComponent() 24 | m.SetType("VEVENT") 25 | m.AddProperty("UID", "testing1") 26 | m.AddProperty("UID", "testing2") // Not that you'd ever _want_ to have multiple UIDs but for testing this is fine. 27 | 28 | c.AddComponent(m) 29 | 30 | if len(c.Elements) != 1 { 31 | t.Error("Error adding a component") 32 | } 33 | 34 | ins := &EventTest{ 35 | component: c, 36 | } 37 | 38 | w := &bytes.Buffer{} 39 | enc := goics.NewICalEncode(w) 40 | enc.Encode(ins) 41 | 42 | want := "BEGIN:VCALENDAR\r\nCALSCAL:GREGORIAN\r\nPRODID:-//tmpo.io/src/goics\r\nBEGIN:VEVENT\r\nUID:testing1\r\nUID:testing2\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n" 43 | got := w.String() 44 | 45 | if got != want { 46 | t.Errorf("encoded value mismatch:\ngot:\n%s\n\nwant:\n%s", got, want) 47 | } 48 | } 49 | 50 | type EventTest struct { 51 | component goics.Componenter 52 | } 53 | 54 | func (evt *EventTest) EmitICal() goics.Componenter { 55 | return evt.component 56 | } 57 | 58 | func TestWritingSimple(t *testing.T) { 59 | c := goics.NewComponent() 60 | c.SetType("VCALENDAR") 61 | c.AddProperty("CALSCAL", "GREGORIAN") 62 | 63 | ins := &EventTest{ 64 | component: c, 65 | } 66 | 67 | w := &bytes.Buffer{} 68 | enc := goics.NewICalEncode(w) 69 | enc.Encode(ins) 70 | 71 | result := &bytes.Buffer{} 72 | fmt.Fprintf(result, "BEGIN:VCALENDAR"+goics.CRLF) 73 | fmt.Fprintf(result, "CALSCAL:GREGORIAN"+goics.CRLF) 74 | fmt.Fprintf(result, "END:VCALENDAR"+goics.CRLF) 75 | 76 | res := bytes.Compare(w.Bytes(), result.Bytes()) 77 | if res != 0 { 78 | t.Errorf("%s!=%s %d", w, result, res) 79 | 80 | } 81 | 82 | } 83 | 84 | func TestFormatDateFieldFormat(t *testing.T) { 85 | rkey := "DTEND;VALUE=DATE" 86 | rval := "20140406" 87 | ti := time.Date(2014, time.April, 06, 0, 0, 0, 0, time.UTC) 88 | key, val := goics.FormatDateField("DTEND", ti) 89 | if rkey != key { 90 | t.Error("Expected", rkey, "Result", key) 91 | } 92 | if rval != val { 93 | t.Error("Expected", rval, "Result", val) 94 | } 95 | } 96 | 97 | func TestFormatDateTimeFieldFormat(t *testing.T) { 98 | rkey := "X-MYDATETIME;VALUE=DATE-TIME" 99 | rval := "20120901T130000" 100 | ti := time.Date(2012, time.September, 01, 13, 0, 0, 0, time.UTC) 101 | key, val := goics.FormatDateTimeField("X-MYDATETIME", ti) 102 | if rkey != key { 103 | t.Error("Expected", rkey, "Result", key) 104 | } 105 | if rval != val { 106 | t.Error("Expected", rval, "Result", val) 107 | } 108 | } 109 | 110 | func TestDateTimeFormat(t *testing.T) { 111 | rkey := "DTSTART" 112 | rval := "19980119T070000Z" 113 | ti := time.Date(1998, time.January, 19, 07, 0, 0, 0, time.UTC) 114 | key, val := goics.FormatDateTime("DTSTART", ti) 115 | if rkey != key { 116 | t.Error("Expected", rkey, "Result", key) 117 | } 118 | if rval != val { 119 | t.Error("Expected", rval, "Result", val) 120 | } 121 | } 122 | 123 | var shortLine = `asdf defined is a test\n\r` 124 | 125 | func TestLineWriter(t *testing.T) { 126 | 127 | w := &bytes.Buffer{} 128 | 129 | result := &bytes.Buffer{} 130 | fmt.Fprintf(result, shortLine) 131 | 132 | encoder := goics.NewICalEncode(w) 133 | encoder.WriteLine(shortLine) 134 | 135 | res := bytes.Compare(w.Bytes(), result.Bytes()) 136 | 137 | if res != 0 { 138 | t.Errorf("%s!=%s", w, result) 139 | } 140 | 141 | } 142 | 143 | var longLine = `As returned by NewWriter, a Writer writes records terminated by thisisat test that is expanded in multi lines` + goics.CRLF 144 | 145 | func TestLineWriterLongLine(t *testing.T) { 146 | 147 | w := &bytes.Buffer{} 148 | 149 | result := &bytes.Buffer{} 150 | fmt.Fprintf(result, "As returned by NewWriter, a Writer writes records terminated by thisisat ") 151 | fmt.Fprintf(result, goics.CRLFSP) 152 | fmt.Fprintf(result, "test that is expanded in multi lines") 153 | fmt.Fprintf(result, goics.CRLF) 154 | 155 | encoder := goics.NewICalEncode(w) 156 | encoder.WriteLine(longLine) 157 | 158 | res := bytes.Compare(w.Bytes(), result.Bytes()) 159 | 160 | if res != 0 { 161 | t.Errorf("%s!=%s %d", w, result, res) 162 | } 163 | } 164 | 165 | func Test2ongLineWriter(t *testing.T) { 166 | goics.LineSize = 10 167 | 168 | w := &bytes.Buffer{} 169 | 170 | result := &bytes.Buffer{} 171 | fmt.Fprintf(result, "12345678") 172 | fmt.Fprintf(result, goics.CRLF) 173 | fmt.Fprintf(result, " 2345678") 174 | fmt.Fprintf(result, goics.CRLF) 175 | fmt.Fprintf(result, " 2345678") 176 | 177 | var str = `1234567823456782345678` 178 | encoder := goics.NewICalEncode(w) 179 | encoder.WriteLine(str) 180 | 181 | res := bytes.Compare(w.Bytes(), result.Bytes()) 182 | 183 | if res != 0 { 184 | t.Errorf("%s!=%s %d", w, result, res) 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /examples/mysqlsource/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This example demostrates the use of goics, with a Mysql data source. 4 | Obviously it will not work instead you have the correct tables setup 5 | on the mysql side. 6 | 7 | The interesting part is in models.go -------------------------------------------------------------------------------- /examples/mysqlsource/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "dsn": "root:jordi@tcp(docker:3306)/test?charset=utf8&parseTime=true", 3 | "ServerAddress": "localhost:9000" 4 | } 5 | -------------------------------------------------------------------------------- /examples/mysqlsource/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | 11 | _ "github.com/go-sql-driver/mysql" 12 | "github.com/gorilla/mux" 13 | "github.com/jmoiron/sqlx" 14 | "github.com/jordic/goics" 15 | ) 16 | 17 | var ( 18 | configFile = flag.String("config", "config.json", "Path to config file") 19 | Db *sqlx.DB 20 | ) 21 | 22 | var config struct { 23 | Dsn string `json:"dsn"` 24 | ServerAddress string 25 | } 26 | 27 | const version = "0.1" 28 | 29 | func main() { 30 | 31 | flag.Parse() 32 | load_config() 33 | //fmt.Printf("%v", config) 34 | var err error 35 | Db, err = sqlx.Connect("mysql", config.Dsn) 36 | if err != nil { 37 | panic("Cant connect to database") 38 | } 39 | 40 | m := mux.NewRouter() 41 | m.Handle("/version", http.HandlerFunc(Version)) 42 | m.Handle("/limpieza", http.HandlerFunc(LimpiezaHandler)) 43 | 44 | http.Handle("/", m) 45 | log.Print("Server Started") 46 | log.Fatal(http.ListenAndServe(config.ServerAddress, nil)) 47 | } 48 | 49 | func Version(w http.ResponseWriter, r *http.Request) { 50 | fmt.Fprintf(w, "version %s", version) 51 | return 52 | } 53 | 54 | func LimpiezaHandler(w http.ResponseWriter, r *http.Request) { 55 | 56 | log.Print("Calendar request") 57 | // Setup headers for the calendar 58 | w.Header().Set("Content-type", "text/calendar") 59 | w.Header().Set("charset", "utf-8") 60 | w.Header().Set("Content-Disposition", "inline") 61 | w.Header().Set("filename", "calendar.ics") 62 | // Get the Collection models 63 | collection := GetReservas() 64 | // Encode it. 65 | goics.NewICalEncode(w).Encode(collection) 66 | } 67 | 68 | // Load and parse config json file 69 | func load_config() { 70 | file, err := ioutil.ReadFile(*configFile) 71 | if err != nil { 72 | log.Fatalf("Error reading config file %s", err) 73 | } 74 | 75 | err = json.Unmarshal(file, &config) 76 | if err != nil { 77 | log.Fatalf("Error decoding config file %s", err) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/mysqlsource/models.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | "github.com/jordic/goics" 11 | ) 12 | 13 | // We define our struct as mysql row 14 | type Reserva struct { 15 | Clau string `db:"clau"` 16 | Data time.Time `db:"data"` 17 | Apartament string `db:"apartament"` 18 | Nits int `db:"nits"` 19 | Extra sql.NullString `db:"limpieza"` 20 | Llegada sql.NullString `db:"llegada"` 21 | Client string `db:"client"` 22 | } 23 | 24 | // A collection of rous 25 | type ReservasCollection []*Reserva 26 | 27 | // We implement ICalEmiter interface that will return a goics.Componenter. 28 | func (rc ReservasCollection) EmitICal() goics.Componenter { 29 | 30 | c := goics.NewComponent() 31 | c.SetType("VCALENDAR") 32 | c.AddProperty("CALSCAL", "GREGORIAN") 33 | c.AddProperty("PRODID;X-RICAL-TZSOURCE=TZINFO", "-//tmpo.io") 34 | 35 | for _, ev := range rc { 36 | s := goics.NewComponent() 37 | s.SetType("VEVENT") 38 | dtend := ev.Data.AddDate(0, 0, ev.Nits) 39 | k, v := goics.FormatDateField("DTEND", dtend) 40 | s.AddProperty(k, v) 41 | k, v = goics.FormatDateField("DTSTART", ev.Data) 42 | s.AddProperty(k, v) 43 | s.AddProperty("UID", ev.Clau+"@whotells.com") 44 | des := fmt.Sprintf("%s Llegada: %s", ev.Extra.String, ev.Llegada.String) 45 | s.AddProperty("DESCRIPTION", des) 46 | s.AddProperty("SUMMARY", fmt.Sprintf("Reserva de %s", ev.Client)) 47 | s.AddProperty("LOCATION", ev.Apartament) 48 | 49 | c.AddComponent(s) 50 | } 51 | 52 | return c 53 | 54 | } 55 | 56 | // Get data from database populating ReservasCollection 57 | func GetReservas() ReservasCollection { 58 | 59 | t := time.Now() 60 | q := `SELECT clau, data, nits, limpieza, llegada, 61 | CONCAT(b.tipus, " ", b.title) as apartament, 62 | CONCAT(c.nom, " ", c.cognom) as client 63 | FROM reserves_reserva as a LEFT join crm_client as c on a.client_id = c.id 64 | LEFT JOIN reserves_apartament as b on a.apartament_id = b.id 65 | WHERE data>=? and a.status<=3` 66 | 67 | reservas := ReservasCollection{} 68 | err := Db.Select(&reservas, q, t.Format("2006-01-02")) 69 | _ = Db.Unsafe() 70 | if err != nil { 71 | log.Println(err) 72 | } 73 | return reservas 74 | } 75 | -------------------------------------------------------------------------------- /fixtures/test.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID;X-RICAL-TZSOURCE=TZINFO:-//XXX Inc//Hosting Calendar 0.8.8//EN 3 | CALSCALE:GREGORIAN 4 | VERSION:2.0 5 | BEGIN:VEVENT 6 | DTEND;VALUE=DATE:20140406 7 | DTSTART;VALUE=DATE:20140404 8 | UID:l2o37cf3aid3--x6kcdynfaswk@zzz.com 9 | DESCRIPTION:CHECKIN: 04/04/2014\nCHECKOUT: 06/04/2014\nNIGHTS: 2\nPHON 10 | E: +33 0 12 00 00 34\nEMAIL: (no se ha facilitado ningún correo e 11 | lectrónico)\nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 12 | SUMMARY:Clarisse De (AZfTDA) 13 | LOCATION:Apartamento XXX 6-8 pax en Centro 14 | END:VEVENT 15 | BEGIN:VEVENT 16 | DTEND;VALUE=DATE:20140506 17 | DTSTART;VALUE=DATE:20140501 18 | UID:l2o37cf3aid3--87bbs2gixrau@zzz.com 19 | DESCRIPTION:CHECKIN: 01/05/2014\nCHECKOUT: 06/05/2014\nNIGHTS: 5\nPHON 20 | E: \nEMAIL: (no se ha facilitado ningún correo electrónico)\nPRO 21 | PERTY: Apartamento XXX 6-8 pax en Centro\n 22 | SUMMARY:ccccc ccccc (FYSPZN) 23 | LOCATION:Apartamento XXX 6-8 pax en Centro 24 | END:VEVENT 25 | BEGIN:VEVENT 26 | DTEND;VALUE=DATE:20140602 27 | DTSTART;VALUE=DATE:20140528 28 | UID:l2o37cf3aid3-h0pgz73k4xeq@zzz.com 29 | DESCRIPTION:CHECKIN: 28/05/2014\nCHECKOUT: 02/06/2014\nNIGHTS: 5\nPHON 30 | E: +444 54441219\nEMAIL: (no se ha facilitado ningún correo elec 31 | trónico)\nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 32 | SUMMARY:Tomer Zoltar (ZNESDR) 33 | LOCATION:Apartamento XXX 6-8 pax en Centro 34 | END:VEVENT 35 | BEGIN:VEVENT 36 | DTEND;VALUE=DATE:20140615 37 | DTSTART;VALUE=DATE:20140610 38 | UID:l2o37cf3aid3-jc5hibmpkqoa@zzz.com 39 | DESCRIPTION:CHECKIN: 10/06/2014\nCHECKOUT: 15/06/2014\nNIGHTS: 5\nPHON 40 | E: +852 44444077\nEMAIL: (no se ha facilitado ningún correo elect 41 | rónico)\nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 42 | SUMMARY:Rosa Kinley (44YM3T) 43 | LOCATION:Apartamento XXX 6-8 pax en Centro 44 | END:VEVENT 45 | BEGIN:VEVENT 46 | DTEND;VALUE=DATE:20140717 47 | DTSTART;VALUE=DATE:20140710 48 | UID:l2o37cf3aid3-kufsplp6r5dy@zzz.com 49 | DESCRIPTION:CHECKIN: 10/07/2014\nCHECKOUT: 17/07/2014\nNIGHTS: 7\nPHON 50 | E: +47 444 15 4\nEMAIL: (no se ha facilitado ningún correo elec 51 | trónico)\nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 52 | SUMMARY:Daniel Apoleon (ZXYCWH) 53 | LOCATION:Apartamento XXX 6-8 pax en Centro 54 | END:VEVENT 55 | BEGIN:VEVENT 56 | DTEND;VALUE=DATE:20141015 57 | DTSTART;VALUE=DATE:20141006 58 | UID:l2o37cf3aid3-cuvz5guzgw9a@zzz.com 59 | DESCRIPTION:CHECKIN: 06/10/2014\nCHECKOUT: 15/10/2014\nNIGHTS: 9\nPHON 60 | E: +34 61 444 444\nEMAIL: (no se ha facilitado ningún correo ele 61 | ctrónico)\nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 62 | SUMMARY:Thomas Soulie (SQSNXN) 63 | LOCATION:Apartamento XXX 6-8 pax en Centro 64 | END:VEVENT 65 | BEGIN:VEVENT 66 | DTEND;VALUE=DATE:20141223 67 | DTSTART;VALUE=DATE:20141219 68 | UID:l2o37cf3aid3-t6btz3a1quiu@zzz.com 69 | DESCRIPTION:CHECKIN: 19/12/2014\nCHECKOUT: 23/12/2014\nNIGHTS: 4\nPHON 70 | E: +972 44444444\nEMAIL: (no se ha facilitado ningún correo elec 71 | trónico)\nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 72 | SUMMARY:Ronit zxcvzcxv (D8ZTD9) 73 | LOCATION:Apartamento XXX 6-8 pax en Centro 74 | END:VEVENT 75 | BEGIN:VEVENT 76 | DTEND;VALUE=DATE:20150103 77 | DTSTART;VALUE=DATE:20141230 78 | UID:l2o37cf3aid3-dpzith26c456@zzz.com 79 | DESCRIPTION:CHECKIN: 30/12/2014\nCHECKOUT: 03/01/2015\nNIGHTS: 4\nPHON 80 | E: +1 444 444 444\nEMAIL: (no se ha facilitado ningún correo ele 81 | ctrónico)\nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 82 | SUMMARY:Brad asdfds (WP2TE5) 83 | LOCATION:Apartamento XXX 6-8 pax en Centro 84 | END:VEVENT 85 | BEGIN:VEVENT 86 | DTEND;VALUE=DATE:20150301 87 | DTSTART;VALUE=DATE:20150227 88 | UID:l2o37cf3aid3-fvtxszm1knx4@zzz.com 89 | DESCRIPTION:CHECKIN: 27/02/2015\nCHECKOUT: 01/03/2015\nNIGHTS: 2\nPHON 90 | E: +31 4 444444\nEMAIL: oscar-kkcm9vqca6n4klq5@guest.zzz.com\ 91 | nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 92 | SUMMARY:Oscar xxxx (F8EJ94) 93 | LOCATION:Apartamento XXX 6-8 pax en Centro 94 | END:VEVENT 95 | BEGIN:VEVENT 96 | DTEND;VALUE=DATE:20150305 97 | DTSTART;VALUE=DATE:20150302 98 | UID:l2o37cf3aid3-xascnu6ripjy@zzz.com 99 | DESCRIPTION:CHECKIN: 02/03/2015\nCHECKOUT: 05/03/2015\nNIGHTS: 3\nPHON 100 | E: +1 444 444 4444\nEMAIL: landon-sti5y4vol8r0wxo3@guest.zzz.co 101 | m\nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 102 | SUMMARY:Landon qwer (W9ZZXE) 103 | LOCATION:Apartamento XXX 6-8 pax en Centro 104 | END:VEVENT 105 | BEGIN:VEVENT 106 | DTEND;VALUE=DATE:20150316 107 | DTSTART;VALUE=DATE:20150312 108 | UID:l2o37cf3aid3--1feqevs1akxw@zzz.com 109 | DESCRIPTION:CHECKIN: 12/03/2015\nCHECKOUT: 16/03/2015\nNIGHTS: 4\nPHON 110 | E: +44 4444 444444\nEMAIL: michelle-jpheif6egc915ni2@guest.zzz. 111 | com\nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 112 | SUMMARY:Michelle afds (JP2NQZ) 113 | LOCATION:Apartamento XXX 6-8 pax en Centro 114 | END:VEVENT 115 | BEGIN:VEVENT 116 | DTEND;VALUE=DATE:20150323 117 | DTSTART;VALUE=DATE:20150318 118 | UID:l2o37cf3aid3-tziqshhwyy8k@zzz.com 119 | DESCRIPTION:CHECKIN: 18/03/2015\nCHECKOUT: 23/03/2015\nNIGHTS: 5\nPHON 120 | E: +444 44444444\nEMAIL: khalid-lhb71l7xkq4iy8op@guest.zzz.com 121 | \nPROPERTY: Apartamento XXX 6-8 pax en Centro\n 122 | SUMMARY:Khalid asdffds (2YH9AY) 123 | LOCATION:Apartamento XXX 6-8 pax en Centro 124 | END:VEVENT 125 | BEGIN:VEVENT 126 | DTEND;VALUE=DATE:20140309 127 | DTSTART;VALUE=DATE:20140307 128 | UID:ft3q5uy7xlvr-kuce70shwdci@zzz.com 129 | SUMMARY:Not available 130 | END:VEVENT 131 | BEGIN:VEVENT 132 | DTEND;VALUE=DATE:20140404 133 | DTSTART;VALUE=DATE:20140403 134 | UID:ft3q5uy7xlvr-9vi9i2ibztdu@zzz.com 135 | SUMMARY:Not available 136 | END:VEVENT 137 | BEGIN:VEVENT 138 | DTEND;VALUE=DATE:20140421 139 | DTSTART;VALUE=DATE:20140416 140 | UID:ft3q5uy7xlvr-7aipn2ib38k2@zzz.com 141 | SUMMARY:Not available 142 | END:VEVENT 143 | BEGIN:VEVENT 144 | DTEND;VALUE=DATE:20140511 145 | DTSTART;VALUE=DATE:20140508 146 | UID:ft3q5uy7xlvr--u5ucm5fiof06@zzz.com 147 | SUMMARY:Not available 148 | END:VEVENT 149 | BEGIN:VEVENT 150 | DTEND;VALUE=DATE:20140520 151 | DTSTART;VALUE=DATE:20140516 152 | UID:ft3q5uy7xlvr--xc0fgfscr8m@zzz.com 153 | SUMMARY:Not available 154 | END:VEVENT 155 | BEGIN:VEVENT 156 | DTEND;VALUE=DATE:20140526 157 | DTSTART;VALUE=DATE:20140524 158 | UID:ft3q5uy7xlvr-2ov8j0tebljw@zzz.com 159 | SUMMARY:Not available 160 | END:VEVENT 161 | BEGIN:VEVENT 162 | DTEND;VALUE=DATE:20140727 163 | DTSTART;VALUE=DATE:20140725 164 | UID:ft3q5uy7xlvr-qfflc24xxtq0@zzz.com 165 | SUMMARY:Not available 166 | END:VEVENT 167 | BEGIN:VEVENT 168 | DTEND;VALUE=DATE:20140813 169 | DTSTART;VALUE=DATE:20140806 170 | UID:ft3q5uy7xlvr--cjilagc94nh8@zzz.com 171 | SUMMARY:Not available 172 | END:VEVENT 173 | BEGIN:VEVENT 174 | DTEND;VALUE=DATE:20140818 175 | DTSTART;VALUE=DATE:20140815 176 | UID:ft3q5uy7xlvr-t48ojr4hogb0@zzz.com 177 | SUMMARY:Not available 178 | END:VEVENT 179 | BEGIN:VEVENT 180 | DTEND;VALUE=DATE:20140828 181 | DTSTART;VALUE=DATE:20140825 182 | UID:ft3q5uy7xlvr-vmghwved20gc@zzz.com 183 | SUMMARY:Not available 184 | END:VEVENT 185 | BEGIN:VEVENT 186 | DTEND;VALUE=DATE:20141019 187 | DTSTART;VALUE=DATE:20141017 188 | UID:ft3q5uy7xlvr-6f5ub3po1who@zzz.com 189 | SUMMARY:Not available 190 | END:VEVENT 191 | BEGIN:VEVENT 192 | DTEND;VALUE=DATE:20150215 193 | DTSTART;VALUE=DATE:20150214 194 | UID:ft3q5uy7xlvr--ea2ac4odduxo@zzz.com 195 | SUMMARY:Not available 196 | END:VEVENT 197 | BEGIN:VEVENT 198 | DTEND;VALUE=DATE:20150221 199 | DTSTART;VALUE=DATE:20150220 200 | UID:ft3q5uy7xlvr-w4tywzlg2qa0@zzz.com 201 | SUMMARY:Not available 202 | END:VEVENT 203 | BEGIN:VEVENT 204 | DTEND;VALUE=DATE:20150302 205 | DTSTART;VALUE=DATE:20150301 206 | UID:ft3q5uy7xlvr--ff5t4agu9ne8@zzz.com 207 | SUMMARY:Not available 208 | END:VEVENT 209 | BEGIN:VEVENT 210 | DTEND;VALUE=DATE:20150307 211 | DTSTART;VALUE=DATE:20150306 212 | UID:ft3q5uy7xlvr--qwubwlngwjte@zzz.com 213 | SUMMARY:Not available 214 | END:VEVENT 215 | BEGIN:VEVENT 216 | DTEND;VALUE=DATE:20150401 217 | DTSTART;VALUE=DATE:20150331 218 | UID:ft3q5uy7xlvr-q4hh4gwb1adi@zzz.com 219 | SUMMARY:Not available 220 | END:VEVENT 221 | END:VCALENDAR 222 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jordic/goics 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.5.0 7 | github.com/gorilla/mux v1.8.0 8 | github.com/jmoiron/sqlx v1.3.1 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 2 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 3 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 4 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 5 | github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE= 6 | github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= 7 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 8 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 9 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= 10 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 11 | -------------------------------------------------------------------------------- /goics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 jordi collell j@tmpo.io. All rights reserved 2 | // Package goics implements an ical encoder and decoder. 3 | // First release will include decode and encoder of Event types 4 | 5 | package goics 6 | 7 | // ICalConsumer is the realy important part of the decoder lib 8 | // The decoder is organized around the Provider/Consumer pattern. 9 | // the decoder acts as a consummer producing IcsNode's and 10 | // Every data type that wants to receive data, must implement 11 | // the consumer pattern. 12 | type ICalConsumer interface { 13 | ConsumeICal(d *Calendar, err error) error 14 | } 15 | 16 | // ICalEmiter must be implemented in order to allow objects to be serialized 17 | // It should return a *goics.Calendar and optional a map of fields and 18 | // their serializers, if no serializer is defined, it will serialize as 19 | // string.. 20 | type ICalEmiter interface { 21 | EmitICal() Componenter 22 | } 23 | 24 | // Componenter defines what should be a component that can be rendered with 25 | // others components inside and some properties 26 | // CALENDAR >> VEVENT ALARM VTODO 27 | type Componenter interface { 28 | Write(w *ICalEncode) 29 | AddComponent(c Componenter) 30 | SetType(t string) 31 | AddProperty(string, string) 32 | } 33 | 34 | // Calendar holds the base struct for a Component VCALENDAR 35 | type Calendar struct { 36 | Data map[string]*IcsNode // map of every property found on ics file 37 | Events []*Event // slice of events founds in file 38 | } 39 | 40 | // Event holds the base struct for a Event Component during decoding 41 | type Event struct { 42 | Data map[string]*IcsNode 43 | Alarms []*map[string]*IcsNode 44 | // Stores multiple keys for the same property... ( a list ) 45 | List map[string][]*IcsNode 46 | } 47 | -------------------------------------------------------------------------------- /goics_test.go: -------------------------------------------------------------------------------- 1 | package goics_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | 8 | goics "github.com/jordic/goics" 9 | ) 10 | 11 | type Event struct { 12 | Start, End time.Time 13 | ID, Summary string 14 | } 15 | 16 | type Events []Event 17 | 18 | func (e *Events) ConsumeICal(c *goics.Calendar, err error) error { 19 | for _, el := range c.Events { 20 | node := el.Data 21 | dtstart, err := node["DTSTART"].DateDecode() 22 | if err != nil { 23 | return err 24 | } 25 | dtend, err := node["DTEND"].DateDecode() 26 | if err != nil { 27 | return err 28 | } 29 | d := Event{ 30 | Start: dtstart, 31 | End: dtend, 32 | ID: node["UID"].Val, 33 | Summary: node["SUMMARY"].Val, 34 | } 35 | *e = append(*e, d) 36 | } 37 | return nil 38 | } 39 | 40 | func TestConsumer(t *testing.T) { 41 | d := goics.NewDecoder(strings.NewReader(testConsumer)) 42 | consumer := Events{} 43 | err := d.Decode(&consumer) 44 | if err != nil { 45 | t.Error("Unable to consume ics") 46 | } 47 | if len(consumer) != 1 { 48 | t.Error("Incorrect length decoding container", len(consumer)) 49 | } 50 | 51 | if consumer[0].Start != time.Date(2014, time.April, 06, 0, 0, 0, 0, time.UTC) { 52 | t.Error("Expected", consumer[0].Start) 53 | } 54 | if consumer[0].ID != "-kpd6p8pqal11-n66f1wk1tw76@xxxx.com" { 55 | t.Errorf("Error decoding text") 56 | } 57 | } 58 | 59 | var testConsumer = `BEGIN:VCALENDAR 60 | PRODID;X-RICAL-TZSOURCE=TZINFO:-//test//EN 61 | CALSCALE:GREGORIAN 62 | VERSION:2.0 63 | BEGIN:VEVENT 64 | DTEND;VALUE=DATE:20140506 65 | DTSTART;VALUE=DATE:20140406 66 | UID:-kpd6p8pqal11-n66f1wk1tw76@xxxx.com 67 | DESCRIPTION:CHECKIN: 01/05/2014\nCHECKOUT: 06/05/2014\nNIGHTS: 5\nPHON 68 | E: \nEMAIL: (no se ha facilitado ningún correo electrónico)\nPRO 69 | PERTY: Apartamento xxx 6-8 pax en Centro\n 70 | SUMMARY:Luigi Carta (FYSPZN) 71 | LOCATION:Apartamento xxx 6-8 pax en Centro 72 | END:VEVENT 73 | END:VCALENDAR 74 | ` 75 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package goics 2 | 3 | // splitLength returns a slice of strings, each string is at most length bytes long. 4 | // Does not break UTF-8 codepoints. 5 | func splitLength(s string, length int) []string { 6 | var ret []string 7 | 8 | for len(s) > 0 { 9 | tmp := truncateString(s, length) 10 | if len(tmp) == 0 { 11 | // too short length, or invalid UTF-8 string 12 | break 13 | } 14 | ret = append(ret, tmp) 15 | s = s[len(tmp):] 16 | } 17 | 18 | return ret 19 | } 20 | 21 | // truncateString truncates s to a maximum of length bytes without breaking UTF-8 codepoints. 22 | func truncateString(s string, length int) string { 23 | if len(s) <= length { 24 | return s 25 | } 26 | 27 | // UTF-8 continuation bytes start with 10xx xxxx: 28 | // 0xc0 = 1100 0000 29 | // 0x80 = 1000 0000 30 | cutoff := length 31 | for s[cutoff]&0xc0 == 0x80 { 32 | cutoff-- 33 | if cutoff < 0 { 34 | cutoff = 0 35 | break 36 | } 37 | } 38 | 39 | return s[0:cutoff] 40 | } 41 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package goics 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestTruncateString(t *testing.T) { 10 | tests := []struct { 11 | input string 12 | want string 13 | length int 14 | }{ 15 | {"Anders", "And", 3}, 16 | {"åååååå", "ååå", 6}, 17 | // Hiragana a times 4 18 | {"\u3042\u3042\u3042\u3042", "\u3042", 4}, 19 | {"\U0001F393", "", 1}, 20 | // Continuation bytes 21 | {"\x80\x80", "", 1}, 22 | } 23 | for _, test := range tests { 24 | if got := truncateString(test.input, test.length); got != test.want { 25 | t.Errorf("expected %q, got %q", test.want, got) 26 | } 27 | } 28 | } 29 | 30 | func TestSplitLength(t *testing.T) { 31 | tests := []struct { 32 | input string 33 | len int 34 | want []string 35 | }{ 36 | { 37 | "AndersSrednaFoobarBazbarX", 38 | 6, 39 | []string{"Anders", "Sredna", "Foobar", "Bazbar", "X"}, 40 | }, 41 | { 42 | "AAAA\u00c5\u00c5\u00c5\u00c5\u3042\u3042\u3042\u3042\U0001F393\U0001F393\U0001F393\U0001F393", 43 | 4, 44 | []string{ 45 | "AAAA", // 1 byte 46 | "\u00c5\u00c5", "\u00c5\u00c5", // 2 bytes 47 | "\u3042", "\u3042", "\u3042", "\u3042", // 3 bytes 48 | "\U0001F393", "\U0001F393", "\U0001F393", "\U0001F393", // 4 bytes 49 | }, 50 | }, 51 | { 52 | "\u3042\u3042\u3042\u3042", 53 | 8, 54 | []string{"\u3042\u3042", "\u3042\u3042"}, 55 | }, 56 | { 57 | "\u3042", 58 | 2, 59 | nil, 60 | }, 61 | } 62 | for _, tt := range tests { 63 | if got := splitLength(tt.input, tt.len); !reflect.DeepEqual(got, tt.want) { 64 | t.Errorf("splitLength() = %v, want %v", got, tt.want) 65 | } 66 | } 67 | } 68 | 69 | func BenchmarkTruncateString(b *testing.B) { 70 | longString := strings.Repeat("\u3042", 100) 71 | for i := 0; i < b.N; i++ { 72 | truncateString(longString, 150) 73 | } 74 | } 75 | --------------------------------------------------------------------------------