From d43a7ecd181b1eab16959e481e12a1339717989d Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 19:56:47 +0200 Subject: [PATCH] Initial commit --- sse-encoder.go | 77 +++++++++++++++++++++++++ sse_test.go | 148 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 sse-encoder.go create mode 100644 sse_test.go diff --git a/sse-encoder.go b/sse-encoder.go new file mode 100644 index 0000000..204e934 --- /dev/null +++ b/sse-encoder.go @@ -0,0 +1,77 @@ +package sse + +import ( + "encoding/json" + "fmt" + "io" + + "reflect" + "strings" +) + +type Event struct { + Event string + Id string + Retry uint + Data interface{} +} + +func Encode(w io.Writer, event Event) error { + writeId(w, event.Id) + writeEvent(w, event.Event) + writeRetry(w, event.Retry) + return writeData(w, event.Data) +} + +func writeId(w io.Writer, id string) { + if len(id) > 0 { + w.Write([]byte("id: ")) + w.Write([]byte(escape(id))) + w.Write([]byte("\n")) + } +} + +func writeEvent(w io.Writer, event string) { + if len(event) > 0 { + w.Write([]byte("event: ")) + w.Write([]byte(escape(event))) + w.Write([]byte("\n")) + } +} + +func writeRetry(w io.Writer, retry uint) { + if retry > 0 { + fmt.Fprintf(w, "retry: %d\n", retry) + } +} + +func writeData(w io.Writer, data interface{}) error { + w.Write([]byte("data: ")) + switch typeOfData(data) { + case reflect.Struct, reflect.Slice, reflect.Map: + err := json.NewEncoder(w).Encode(data) + if err != nil { + return err + } + w.Write([]byte("\n")) + default: + text := fmt.Sprint(data) + fmt.Fprint(w, escape(text), "\n\n") + } + return nil +} + +func typeOfData(data interface{}) reflect.Kind { + value := reflect.ValueOf(data) + valueType := value.Kind() + if valueType == reflect.Ptr { + valueType = value.Elem().Kind() + } + return valueType +} + +func escape(str string) string { + str = strings.Replace(str, "\n", "\\n", -1) + str = strings.Replace(str, "\r", "\\r", -1) + return str +} diff --git a/sse_test.go b/sse_test.go new file mode 100644 index 0000000..32a5beb --- /dev/null +++ b/sse_test.go @@ -0,0 +1,148 @@ +package sse + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncodeOnlyData(t *testing.T) { + w := new(bytes.Buffer) + err := Encode(w, Event{ + Data: "junk\n\njk\nid:fake", + }) + assert.NoError(t, err) + assert.Equal(t, w.String(), "data: junk\\n\\njk\\nid:fake\n\n") +} + +func TestEncodeWithEvent(t *testing.T) { + w := new(bytes.Buffer) + err := Encode(w, Event{ + Event: "t\n:<>\r\test", + Data: "junk\n\njk\nid:fake", + }) + assert.NoError(t, err) + assert.Equal(t, w.String(), "event: t\\n:<>\\r\test\ndata: junk\\n\\njk\\nid:fake\n\n") +} + +func TestEncodeWithId(t *testing.T) { + w := new(bytes.Buffer) + err := Encode(w, Event{ + Id: "t\n:<>\r\test", + 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") +} + +func TestEncodeWithRetry(t *testing.T) { + w := new(bytes.Buffer) + err := Encode(w, Event{ + Retry: 11, + 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") +} + +func TestEncodeWithEverything(t *testing.T) { + w := new(bytes.Buffer) + err := Encode(w, Event{ + Event: "abc", + Id: "12345", + Retry: 10, + Data: "some data", + }) + assert.NoError(t, err) + assert.Equal(t, w.String(), "id: 12345\nevent: abc\nretry: 10\ndata: some data\n\n") +} + +func TestEncodeMap(t *testing.T) { + w := new(bytes.Buffer) + err := Encode(w, Event{ + Event: "a map", + Data: map[string]interface{}{ + "foo": "b\n\rar", + "bar": "id: 2", + }, + }) + assert.NoError(t, err) + assert.Equal(t, w.String(), "event: a map\ndata: {\"bar\":\"id: 2\",\"foo\":\"b\\n\\rar\"}\n\n") +} + +func TestEncodeSlice(t *testing.T) { + w := new(bytes.Buffer) + err := Encode(w, Event{ + Event: "a slice", + 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") +} + +func TestEncodeStruct(t *testing.T) { + myStruct := struct { + A int + B string `json:"value"` + }{1, "number"} + + w := new(bytes.Buffer) + err := Encode(w, Event{ + Event: "a struct", + Data: myStruct, + }) + assert.NoError(t, err) + assert.Equal(t, w.String(), "event: a struct\ndata: {\"A\":1,\"value\":\"number\"}\n\n") + + w.Reset() + err = Encode(w, Event{ + Event: "a struct", + Data: &myStruct, + }) + assert.NoError(t, err) + assert.Equal(t, w.String(), "event: a struct\ndata: {\"A\":1,\"value\":\"number\"}\n\n") +} + +func TestEncodeInteger(t *testing.T) { + w := new(bytes.Buffer) + err := Encode(w, Event{ + Event: "an integer", + Data: 1, + }) + assert.NoError(t, err) + assert.Equal(t, w.String(), "event: an integer\ndata: 1\n\n") +} + +func TestEncodeFloat(t *testing.T) { + w := new(bytes.Buffer) + err := Encode(w, Event{ + Event: "Float", + Data: 1.5, + }) + assert.NoError(t, err) + assert.Equal(t, w.String(), "event: Float\ndata: 1.5\n\n") +} + +func TestEncodeStream(t *testing.T) { + w := new(bytes.Buffer) + + Encode(w, Event{ + Event: "float", + Data: 1.5, + }) + + Encode(w, Event{ + Id: "123", + Data: map[string]interface{}{"foo": "bar", "bar": "foo"}, + }) + + Encode(w, Event{ + Id: "124", + Event: "chat", + Data: "hi! dude", + }) + fmt.Println(w.String()) + 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") +}