diff --git a/sse-decoder.go b/sse-decoder.go new file mode 100644 index 0000000..e1afc6f --- /dev/null +++ b/sse-decoder.go @@ -0,0 +1,115 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package sse + +import ( + "bytes" + "io" + "io/ioutil" +) + +type decoder struct { + events []Event +} + +func Decode(r io.Reader) ([]Event, error) { + var dec decoder + return dec.decode(r) +} + +func (d *decoder) dispatchEvent(event Event, data string) { + dataLength := len(data) + if dataLength > 0 { + //If the data buffer's last character is a U+000A LINE FEED (LF) character, then remove the last character from the data buffer. + data = data[:dataLength-1] + dataLength-- + } + if dataLength == 0 && event.Event == "" { + return + } + if event.Event == "" { + event.Event = "message" + } + event.Data = data + d.events = append(d.events, event) +} + +func (d *decoder) decode(r io.Reader) ([]Event, error) { + buf, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + + var currentEvent Event + var dataBuffer *bytes.Buffer = new(bytes.Buffer) + // TODO (and unit tests) + // Lines must be separated by either a U+000D CARRIAGE RETURN U+000A LINE FEED (CRLF) character pair, + // a single U+000A LINE FEED (LF) character, + // or a single U+000D CARRIAGE RETURN (CR) character. + lines := bytes.Split(buf, []byte{'\n'}) + for _, line := range lines { + if len(line) == 0 { + // If the line is empty (a blank line). Dispatch the event. + d.dispatchEvent(currentEvent, dataBuffer.String()) + + // reset current event and data buffer + currentEvent = Event{} + dataBuffer.Reset() + continue + } + if line[0] == byte(':') { + // If the line starts with a U+003A COLON character (:), ignore the line. + continue + } + + var field, value []byte + colonIndex := bytes.IndexRune(line, ':') + if colonIndex != -1 { + // If the line contains a U+003A COLON character character (:) + // Collect the characters on the line before the first U+003A COLON character (:), + // and let field be that string. + field = line[:colonIndex] + // Collect the characters on the line after the first U+003A COLON character (:), + // and let value be that string. + value = line[colonIndex+1:] + // If value starts with a single U+0020 SPACE character, remove it from value. + if len(value) > 0 && value[0] == ' ' { + value = value[1:] + } + } else { + // Otherwise, the string is not empty but does not contain a U+003A COLON character character (:) + // Use the whole line as the field name, and the empty string as the field value. + field = line + value = []byte{} + } + // The steps to process the field given a field name and a field value depend on the field name, + // as given in the following list. Field names must be compared literally, + // with no case folding performed. + switch string(field) { + case "event": + // Set the event name buffer to field value. + currentEvent.Event = string(value) + case "id": + // Set the event stream's last event ID to the field value. + currentEvent.Id = string(value) + case "retry": + // If the field value consists of only characters in the range U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9), + // then interpret the field value as an integer in base ten, and set the event stream's reconnection time to that integer. + // Otherwise, ignore the field. + currentEvent.Id = string(value) + case "data": + // Append the field value to the data buffer, + dataBuffer.Write(value) + // then append a single U+000A LINE FEED (LF) character to the data buffer. + dataBuffer.WriteString("\n") + default: + //Otherwise. The field is ignored. + continue + } + } + d.dispatchEvent(currentEvent, dataBuffer.String()) + + return d.events, nil +} diff --git a/sse-decoder_test.go b/sse-decoder_test.go new file mode 100644 index 0000000..068107b --- /dev/null +++ b/sse-decoder_test.go @@ -0,0 +1,116 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package sse + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeSingle1(t *testing.T) { + events, err := Decode(bytes.NewBufferString( + `data: this is a text +event: message +fake: +id: 123456789010 +: we can append data +: and multiple comments should not break it +data: a very nice one`)) + + assert.NoError(t, err) + assert.Len(t, events, 1) + assert.Equal(t, events[0].Event, "message") + assert.Equal(t, events[0].Id, "123456789010") +} + +func TestDecodeSingle2(t *testing.T) { + events, err := Decode(bytes.NewBufferString( + `: starting with a comment +fake: + +data:this is a \ntext +event:a message\n\n +fake +:and multiple comments\n should not break it\n\n +id:1234567890\n10 +:we can append data +data:a very nice one\n! + + +`)) + assert.NoError(t, err) + assert.Len(t, events, 1) + assert.Equal(t, events[0].Event, "a message\\n\\n") + assert.Equal(t, events[0].Id, "1234567890\\n10") +} + +func TestDecodeSingle3(t *testing.T) { + events, err := Decode(bytes.NewBufferString( + ` +id:123456ABCabc789010 +event: message123 +: we can append data +data:this is a text +data: a very nice one +data: +data +: ending with a comment`)) + + assert.NoError(t, err) + assert.Len(t, events, 1) + assert.Equal(t, events[0].Event, "message123") + assert.Equal(t, events[0].Id, "123456ABCabc789010") +} + +func TestDecodeMulti1(t *testing.T) { + events, err := Decode(bytes.NewBufferString( + ` +id: +event: weird event +data:this is a text +:data: this should NOT APER +data: second line + +: a comment +event: message +id:123 +data:this is a text +:data: this should NOT APER +data: second line + + +: a comment +event: message +id:123 +data:this is a text +data: second line + +:hola + +data + +event: + +id`)) + assert.NoError(t, err) + assert.Len(t, events, 3) + assert.Equal(t, events[0].Event, "weird event") + assert.Equal(t, events[0].Id, "") +} + +func TestDecodeW3C(t *testing.T) { + events, err := Decode(bytes.NewBufferString( + `data + +data +data + +data: +`)) + assert.NoError(t, err) + assert.Len(t, events, 1) +} diff --git a/sse-encoder.go b/sse-encoder.go index 16bab76..19a385e 100644 --- a/sse-encoder.go +++ b/sse-encoder.go @@ -22,7 +22,14 @@ const ContentType = "text/event-stream" var contentType = []string{ContentType} var noCache = []string{"no-cache"} -var replacer = strings.NewReplacer("\n", "\\n", "\r", "\\r") + +var fieldReplacer = strings.NewReplacer( + "\n", "\\n", + "\r", "\\r") + +var dataReplacer = strings.NewReplacer( + "\n", "\ndata:", + "\r", "\\r") type Event struct { Event string @@ -41,30 +48,30 @@ func Encode(writer io.Writer, event Event) error { func writeId(w stringWriter, id string) { if len(id) > 0 { - w.WriteString("id: ") - writeEscape(w, id) + w.WriteString("id:") + fieldReplacer.WriteString(w, id) w.WriteString("\n") } } func writeEvent(w stringWriter, event string) { if len(event) > 0 { - w.WriteString("event: ") - writeEscape(w, event) + w.WriteString("event:") + fieldReplacer.WriteString(w, event) w.WriteString("\n") } } func writeRetry(w stringWriter, retry uint) { if retry > 0 { - w.WriteString("retry: ") + w.WriteString("retry:") w.WriteString(strconv.FormatUint(uint64(retry), 10)) w.WriteString("\n") } } func writeData(w stringWriter, data interface{}) error { - w.WriteString("data: ") + w.WriteString("data:") switch kindOfData(data) { case reflect.Struct, reflect.Slice, reflect.Map: err := json.NewEncoder(w).Encode(data) @@ -73,8 +80,7 @@ func writeData(w stringWriter, data interface{}) error { } w.WriteString("\n") default: - text := fmt.Sprint(data) - writeEscape(w, text) + dataReplacer.WriteString(w, fmt.Sprint(data)) w.WriteString("\n\n") } return nil @@ -98,9 +104,3 @@ func kindOfData(data interface{}) reflect.Kind { } return valueType } - -func writeEscape(w stringWriter, str string) { - // any-char = %x0000-0009 / %x000B-000C / %x000E-10FFFF - // ; a Unicode character other than U+000A LINE FEED (LF) or U+000D CARRIAGE RETURN (CR) - replacer.WriteString(w, str) -} diff --git a/sse_test.go b/sse_test.go index b1c2d9b..61b685b 100644 --- a/sse_test.go +++ b/sse_test.go @@ -14,21 +14,42 @@ import ( func TestEncodeOnlyData(t *testing.T) { w := new(bytes.Buffer) - err := Encode(w, Event{ + event := Event{ Data: "junk\n\njk\nid:fake", - }) + } + err := Encode(w, event) assert.NoError(t, err) - assert.Equal(t, w.String(), "data: junk\\n\\njk\\nid:fake\n\n") + assert.Equal(t, w.String(), + `data:junk +data: +data:jk +data:id:fake + +`) + + decoded, _ := Decode(w) + assert.Equal(t, decoded, []Event{event}) } func TestEncodeWithEvent(t *testing.T) { w := new(bytes.Buffer) - err := Encode(w, Event{ + event := Event{ Event: "t\n:<>\r\test", Data: "junk\n\njk\nid:fake", - }) + } + err := Encode(w, event) assert.NoError(t, err) - assert.Equal(t, w.String(), "event: t\\n:<>\\r\test\ndata: junk\\n\\njk\\nid:fake\n\n") + assert.Equal(t, w.String(), + `event:t\n:<>\r est +data:junk +data: +data:jk +data:id:fake + +`) + + decoded, _ := Decode(w) + assert.Equal(t, decoded, []Event{event}) } func TestEncodeWithId(t *testing.T) { @@ -38,7 +59,14 @@ func TestEncodeWithId(t *testing.T) { Data: "junk\n\njk\nid:fa\rke", }) assert.NoError(t, err) - assert.Equal(t, w.String(), "id: t\\n:<>\\r\test\ndata: junk\\n\\njk\\nid:fa\\rke\n\n") + assert.Equal(t, w.String(), + `id:t\n:<>\r est +data:junk +data: +data:jk +data:id:fa\rke + +`) } func TestEncodeWithRetry(t *testing.T) { @@ -48,7 +76,15 @@ func TestEncodeWithRetry(t *testing.T) { Data: "junk\n\njk\nid:fake\n", }) assert.NoError(t, err) - assert.Equal(t, w.String(), "retry: 11\ndata: junk\\n\\njk\\nid:fake\\n\n\n") + assert.Equal(t, w.String(), + `retry:11 +data:junk +data: +data:jk +data:id:fake +data: + +`) } func TestEncodeWithEverything(t *testing.T) { @@ -60,7 +96,7 @@ func TestEncodeWithEverything(t *testing.T) { Data: "some data", }) assert.NoError(t, err) - assert.Equal(t, w.String(), "id: 12345\nevent: abc\nretry: 10\ndata: some data\n\n") + assert.Equal(t, w.String(), "id:12345\nevent:abc\nretry:10\ndata:some data\n\n") } func TestEncodeMap(t *testing.T) { @@ -73,7 +109,7 @@ func TestEncodeMap(t *testing.T) { }, }) assert.NoError(t, err) - assert.Equal(t, w.String(), "event: a map\ndata: {\"bar\":\"id: 2\",\"foo\":\"b\\n\\rar\"}\n\n") + assert.Equal(t, w.String(), "event:a map\ndata:{\"bar\":\"id: 2\",\"foo\":\"b\\n\\rar\"}\n\n") } func TestEncodeSlice(t *testing.T) { @@ -83,7 +119,7 @@ func TestEncodeSlice(t *testing.T) { Data: []interface{}{1, "text", map[string]interface{}{"foo": "bar"}}, }) assert.NoError(t, err) - assert.Equal(t, w.String(), "event: a slice\ndata: [1,\"text\",{\"foo\":\"bar\"}]\n\n") + assert.Equal(t, w.String(), "event:a slice\ndata:[1,\"text\",{\"foo\":\"bar\"}]\n\n") } func TestEncodeStruct(t *testing.T) { @@ -98,7 +134,7 @@ func TestEncodeStruct(t *testing.T) { Data: myStruct, }) assert.NoError(t, err) - assert.Equal(t, w.String(), "event: a struct\ndata: {\"A\":1,\"value\":\"number\"}\n\n") + assert.Equal(t, w.String(), "event:a struct\ndata:{\"A\":1,\"value\":\"number\"}\n\n") w.Reset() err = Encode(w, Event{ @@ -106,7 +142,7 @@ func TestEncodeStruct(t *testing.T) { Data: &myStruct, }) assert.NoError(t, err) - assert.Equal(t, w.String(), "event: a struct\ndata: {\"A\":1,\"value\":\"number\"}\n\n") + assert.Equal(t, w.String(), "event:a struct\ndata:{\"A\":1,\"value\":\"number\"}\n\n") } func TestEncodeInteger(t *testing.T) { @@ -116,7 +152,7 @@ func TestEncodeInteger(t *testing.T) { Data: 1, }) assert.NoError(t, err) - assert.Equal(t, w.String(), "event: an integer\ndata: 1\n\n") + assert.Equal(t, w.String(), "event:an integer\ndata:1\n\n") } func TestEncodeFloat(t *testing.T) { @@ -126,7 +162,7 @@ func TestEncodeFloat(t *testing.T) { Data: 1.5, }) assert.NoError(t, err) - assert.Equal(t, w.String(), "event: Float\ndata: 1.5\n\n") + assert.Equal(t, w.String(), "event:Float\ndata:1.5\n\n") } func TestEncodeStream(t *testing.T) { @@ -147,7 +183,7 @@ func TestEncodeStream(t *testing.T) { Event: "chat", Data: "hi! dude", }) - assert.Equal(t, w.String(), "event: float\ndata: 1.5\n\nid: 123\ndata: {\"bar\":\"foo\",\"foo\":\"bar\"}\n\nid: 124\nevent: chat\ndata: hi! dude\n\n") + assert.Equal(t, w.String(), "event:float\ndata:1.5\n\nid:123\ndata:{\"bar\":\"foo\",\"foo\":\"bar\"}\n\nid:124\nevent:chat\ndata:hi! dude\n\n") } func TestRenderSSE(t *testing.T) { @@ -159,7 +195,7 @@ func TestRenderSSE(t *testing.T) { }).Render(w) assert.NoError(t, err) - assert.Equal(t, w.Body.String(), "event: msg\ndata: hi! how are you?\n\n") + assert.Equal(t, w.Body.String(), "event:msg\ndata:hi! how are you?\n\n") assert.Equal(t, w.Header().Get("Content-Type"), "text/event-stream") assert.Equal(t, w.Header().Get("Cache-Control"), "no-cache") }