gojay

high performance JSON encoder/decoder with stream API for Golang
git clone git://git.lair.cx/gojay
Log | Files | Refs | README | LICENSE

commit 2f1addadc72bb7ba394d8cbdf4d7faaa14b25c2a
parent 514aec19fd89e723006ae08b3dcc094a206e85d4
Author: Francois Parquet <francois.parquet@gmail.com>
Date:   Sun,  6 May 2018 23:35:53 +0800

Merge pull request #19 from francoispqt/feature/support-for-embedded-json

add support for decoding embedded JSON
Diffstat:
Mdecode.go | 2++
Adecode_embedded_json.go | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_embedded_json_test.go | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mencode.go | 4++++
Aencode_embedded_json.go | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_embedded_json_test.go | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mencode_interface.go | 2++
7 files changed, 448 insertions(+), 0 deletions(-)

diff --git a/decode.go b/decode.go @@ -190,6 +190,8 @@ func (dec *Decoder) Decode(v interface{}) error { case UnmarshalerArray: _, err := dec.decodeArray(vt) return err + case *EmbeddedJSON: + return dec.decodeEmbeddedJSON(vt) default: return InvalidUnmarshalError(fmt.Sprintf(invalidUnmarshalErrorMsg, reflect.TypeOf(vt).String())) } diff --git a/decode_embedded_json.go b/decode_embedded_json.go @@ -0,0 +1,63 @@ +package gojay + +// EmbeddedJSON is a raw encoded JSON value. +// It can be used to delay JSON decoding or precompute a JSON encoding. +type EmbeddedJSON []byte + +func (dec *Decoder) decodeEmbeddedJSON(ej *EmbeddedJSON) error { + var err error + if ej == nil { + return InvalidUnmarshalError("Invalid nil pointer given") + } + var beginOfEmbeddedJSON int + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch dec.data[dec.cursor] { + case ' ', '\n', '\t', '\r', ',': + continue + // is null + case 'n', 't': + beginOfEmbeddedJSON = dec.cursor + dec.cursor = dec.cursor + 4 + // is false + case 'f': + beginOfEmbeddedJSON = dec.cursor + dec.cursor = dec.cursor + 5 + // is an object + case '{': + beginOfEmbeddedJSON = dec.cursor + dec.cursor = dec.cursor + 1 + dec.cursor, err = dec.skipObject() + // is string + case '"': + beginOfEmbeddedJSON = dec.cursor + dec.cursor = dec.cursor + 1 + err = dec.skipString() // why no new dec.cursor in result? + // is array + case '[': + beginOfEmbeddedJSON = dec.cursor + dec.cursor = dec.cursor + 1 + dec.cursor, err = dec.skipArray() + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': + beginOfEmbeddedJSON = dec.cursor + dec.cursor, err = dec.skipNumber() + } + break + } + if err == nil { + if dec.cursor-1 > beginOfEmbeddedJSON { + *ej = append(*ej, dec.data[beginOfEmbeddedJSON:dec.cursor]...) + } + } + return err +} + +// AddEmbeddedJSON adds an EmbeddedsJSON to the value pointed by v. +// It can be used to delay JSON decoding or precompute a JSON encoding. +func (dec *Decoder) AddEmbeddedJSON(v *EmbeddedJSON) error { + err := dec.decodeEmbeddedJSON(v) + if err != nil { + return err + } + dec.called |= 1 + return nil +} diff --git a/decode_embedded_json_test.go b/decode_embedded_json_test.go @@ -0,0 +1,157 @@ +package gojay + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +type Request struct { + id string + method string + params EmbeddedJSON + more int +} + +func (r *Request) UnmarshalObject(dec *Decoder, key string) error { + switch key { + case "id": + return dec.AddString(&r.id) + case "method": + return dec.AddString(&r.method) + case "params": + return dec.AddEmbeddedJSON(&r.params) + case "more": + dec.AddInt(&r.more) + } + return nil +} + +func (r *Request) NKeys() int { + return 4 +} + +func TestDecodeEmbeddedJSONUnmarshalAPI(t *testing.T) { + testCases := []struct { + name string + json []byte + expectedEmbedded string + }{ + { + name: "decode-basic-string", + json: []byte(`{"id":"someid","method":"getmydata","params":"raw data", "more":123}`), + expectedEmbedded: `"raw data"`, + }, + { + name: "decode-basic-int", + json: []byte(`{"id":"someid","method":"getmydata","params":12345, "more":123}`), + expectedEmbedded: `12345`, + }, + { + name: "decode-basic-int", + json: []byte(`{"id":"someid","method":"getmydata","params":true, "more":123}`), + expectedEmbedded: `true`, + }, + { + name: "decode-basic-int", + json: []byte(`{"id":"someid","method":"getmydata","params": false, "more":123}`), + expectedEmbedded: `false`, + }, + { + name: "decode-basic-int", + json: []byte(`{"id":"someid","method":"getmydata","params":null, "more":123}`), + expectedEmbedded: `null`, + }, + { + name: "decode-basic-object", + json: []byte(`{"id":"someid","method":"getmydata","params":{"example":"of raw data"}, "more":123}`), + expectedEmbedded: `{"example":"of raw data"}`, + }, + { + name: "decode-basic-object", + json: []byte(`{"id":"someid","method":"getmydata","params":[1,2,3], "more":123}`), + expectedEmbedded: `[1,2,3]`, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + req := &Request{} + err := Unmarshal(testCase.json, req) + t.Log(req) + t.Log(string(req.params)) + assert.Nil(t, err, "err should be nil") + assert.Equal(t, testCase.expectedEmbedded, string(req.params), "r.params should be equal to expectedEmbeddedResult") + }) + } +} + +func TestDecodeEmbeddedJSONDecodeAPI(t *testing.T) { + testCases := []struct { + name string + json []byte + expectedEmbedded string + }{ + { + name: "decode-basic-string", + json: []byte(`{"id":"someid","method":"getmydata","params":"raw data", "more":123}`), + expectedEmbedded: `"raw data"`, + }, + { + name: "decode-basic-int", + json: []byte(`{"id":"someid","method":"getmydata","params":12345, "more":123}`), + expectedEmbedded: `12345`, + }, + { + name: "decode-basic-int", + json: []byte(`{"id":"someid","method":"getmydata","params":true, "more":123}`), + expectedEmbedded: `true`, + }, + { + name: "decode-basic-int", + json: []byte(`{"id":"someid","method":"getmydata","params": false, "more":123}`), + expectedEmbedded: `false`, + }, + { + name: "decode-basic-int", + json: []byte(`{"id":"someid","method":"getmydata","params":null, "more":123}`), + expectedEmbedded: `null`, + }, + { + name: "decode-basic-object", + json: []byte(`{"id":"someid","method":"getmydata","params":{"example":"of raw data"}, "more":123}`), + expectedEmbedded: `{"example":"of raw data"}`, + }, + { + name: "decode-basic-object", + json: []byte(`{"id":"someid","method":"getmydata","params":[1,2,3], "more":123}`), + expectedEmbedded: `[1,2,3]`, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ej := EmbeddedJSON([]byte{}) + dec := BorrowDecoder(bytes.NewReader(testCase.json)) + err := dec.Decode(&ej) + assert.Nil(t, err, "err should be nil") + assert.Equal(t, string(testCase.json), string(ej), "r.params should be equal to expectedEmbeddedResult") + }) + } +} + +func TestDecodeEmbeededJSONNil(t *testing.T) { + dec := BorrowDecoder(strings.NewReader(`"bar"`)) + var ej *EmbeddedJSON + err := dec.decodeEmbeddedJSON(ej) + assert.NotNil(t, err, `err should not be nil a nil pointer is given`) + assert.IsType(t, InvalidUnmarshalError(""), err, `err should not be of type InvalidUnmarshalError`) +} + +func TestDecodeEmbeededJSONNil2(t *testing.T) { + dec := BorrowDecoder(strings.NewReader(`"bar"`)) + var ej *EmbeddedJSON + err := dec.AddEmbeddedJSON(ej) + assert.NotNil(t, err, `err should not be nil a nil pointer is given`) + assert.IsType(t, InvalidUnmarshalError(""), err, `err should not be of type InvalidUnmarshalError`) +} diff --git a/encode.go b/encode.go @@ -157,6 +157,10 @@ func Marshal(v interface{}) ([]byte, error) { enc := BorrowEncoder(nil) defer enc.Release() return enc.encodeFloat32(vt) + case *EmbeddedJSON: + enc := BorrowEncoder(nil) + defer enc.Release() + return enc.encodeEmbeddedJSON(vt) default: return nil, InvalidMarshalError(fmt.Sprintf(invalidMarshalErrorMsg, reflect.TypeOf(vt).String())) } diff --git a/encode_embedded_json.go b/encode_embedded_json.go @@ -0,0 +1,83 @@ +package gojay + +// EncodeEmbeddedJSON encodes an embedded JSON. +// is basically sets the internal buf as the value pointed by v and calls the io.Writer.Write() +func (enc *Encoder) EncodeEmbeddedJSON(v *EmbeddedJSON) error { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + enc.buf = *v + _, err := enc.write() + if err != nil { + return err + } + return nil +} + +func (enc *Encoder) encodeEmbeddedJSON(v *EmbeddedJSON) ([]byte, error) { + enc.writeBytes(*v) + return enc.buf, nil +} + +// AddEmbeddedJSON adds an EmbeddedJSON to be encoded. +// +// It basically blindly writes the bytes to the final buffer. Therefore, +// it expects the JSON to be of proper format. +func (enc *Encoder) AddEmbeddedJSON(v *EmbeddedJSON) { + enc.grow(len(*v) + 4) + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeBytes(*v) +} + +// AddEmbeddedJSONOmitEmpty adds an EmbeddedJSON to be encoded or skips it if nil pointer or empty. +// +// It basically blindly writes the bytes to the final buffer. Therefore, +// it expects the JSON to be of proper format. +func (enc *Encoder) AddEmbeddedJSONOmitEmpty(v *EmbeddedJSON) { + if v == nil || len(*v) == 0 { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeBytes(*v) +} + +// AddEmbeddedJSONKey adds an EmbeddedJSON and a key to be encoded. +// +// It basically blindly writes the bytes to the final buffer. Therefore, +// it expects the JSON to be of proper format. +func (enc *Encoder) AddEmbeddedJSONKey(key string, v *EmbeddedJSON) { + enc.grow(len(key) + len(*v) + 5) + r, ok := enc.getPreviousRune() + if ok && r != '{' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeStringEscape(key) + enc.writeBytes(objKey) + enc.writeBytes(*v) +} + +// AddEmbeddedJSONKeyOmitEmpty adds an EmbeddedJSON and a key to be encoded or skips it if nil pointer or empty. +// +// It basically blindly writes the bytes to the final buffer. Therefore, +// it expects the JSON to be of proper format. +func (enc *Encoder) AddEmbeddedJSONKeyOmitEmpty(key string, v *EmbeddedJSON) { + if v == nil || len(*v) == 0 { + return + } + enc.grow(len(key) + len(*v) + 5) + r, ok := enc.getPreviousRune() + if ok && r != '{' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeStringEscape(key) + enc.writeBytes(objKey) + enc.writeBytes(*v) +} diff --git a/encode_embedded_json_test.go b/encode_embedded_json_test.go @@ -0,0 +1,137 @@ +package gojay + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func (r *Request) MarshalObject(enc *Encoder) { + enc.AddStringKey("id", r.id) + enc.AddStringKey("method", r.method) + enc.AddEmbeddedJSONKey("params", &r.params) + params2 := EmbeddedJSON([]byte(``)) + enc.AddEmbeddedJSONKeyOmitEmpty("params2", &params2) + params3 := EmbeddedJSON([]byte(`"test"`)) + enc.AddEmbeddedJSONKeyOmitEmpty("params3", &params3) + enc.AddIntKey("more", r.more) +} + +func (r *Request) IsNil() bool { + return r == nil +} + +type EmbeddedJSONArr []EmbeddedJSON + +func (ear EmbeddedJSONArr) MarshalArray(enc *Encoder) { + for _, e := range ear { + enc.AddEmbeddedJSON(&e) + } +} + +func (ear EmbeddedJSONArr) IsNil() bool { + return len(ear) == 0 +} + +type EmbeddedJSONOmitEmptyArr []EmbeddedJSON + +func (ear EmbeddedJSONOmitEmptyArr) MarshalArray(enc *Encoder) { + for _, e := range ear { + enc.AddEmbeddedJSONOmitEmpty(&e) + } +} + +func (ear EmbeddedJSONOmitEmptyArr) IsNil() bool { + return len(ear) == 0 +} + +func TestEncodingEmbeddedJSON(t *testing.T) { + t.Run("basic-embedded-json", func(t *testing.T) { + ej := EmbeddedJSON([]byte(`"test"`)) + b := &strings.Builder{} + enc := BorrowEncoder(b) + err := enc.Encode(&ej) + assert.Nil(t, err, "err should be nil") + assert.Equal(t, b.String(), `"test"`, "b should be equal to content of EmbeddedJSON") + }) + t.Run("basic-embedded-json-marshal-api", func(t *testing.T) { + ej := EmbeddedJSON([]byte(`"test"`)) + b, err := Marshal(&ej) + assert.Nil(t, err, "err should be nil") + assert.Equal(t, string(b), `"test"`, "b should be equal to content of EmbeddedJSON") + }) + t.Run("object-embedded-json", func(t *testing.T) { + req := Request{ + id: "test", + method: "GET", + params: EmbeddedJSON([]byte(`"test"`)), + } + b := &strings.Builder{} + enc := BorrowEncoder(b) + err := enc.EncodeObject(&req) + assert.Nil(t, err, "err should be nil") + assert.Equal( + t, + b.String(), + `{"id":"test","method":"GET","params":"test","params3":"test","more":0}`, + "b should be equal to content of EmbeddedJSON", + ) + }) + t.Run("array-embedded-json", func(t *testing.T) { + ear := EmbeddedJSONArr{ + []byte(`"test"`), + []byte(`{"test":"test"}`), + } + b := &strings.Builder{} + enc := BorrowEncoder(b) + err := enc.EncodeArray(ear) + assert.Nil(t, err, "err should be nil") + assert.Equal( + t, + b.String(), + `["test",{"test":"test"}]`, + "b should be equal to content of EmbeddedJSON", + ) + }) + t.Run("array-embedded-json-omit-empty", func(t *testing.T) { + ear := EmbeddedJSONOmitEmptyArr{ + []byte(`"test"`), + []byte(``), + []byte(`{"test":"test"}`), + []byte(``), + []byte(`{"test":"test"}`), + } + b := &strings.Builder{} + enc := BorrowEncoder(b) + err := enc.EncodeArray(ear) + assert.Nil(t, err, "err should be nil") + assert.Equal( + t, + b.String(), + `["test",{"test":"test"},{"test":"test"}]`, + "b should be equal to content of EmbeddedJSON", + ) + }) + t.Run("write-error", func(t *testing.T) { + w := TestWriterError("") + v := EmbeddedJSON([]byte(`"test"`)) + enc := NewEncoder(w) + err := enc.EncodeEmbeddedJSON(&v) + assert.NotNil(t, err, "Error should not be nil") + assert.Equal(t, "Test Error", err.Error(), "err.Error() should be 'Test Error'") + }) + t.Run("pool-error", func(t *testing.T) { + v := EmbeddedJSON([]byte(`"test"`)) + enc := BorrowEncoder(nil) + enc.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnot be nil") + assert.IsType(t, InvalidUsagePooledEncoderError(""), err, "err should be of type InvalidUsagePooledEncoderError") + assert.Equal(t, "Invalid usage of pooled encoder", err.(InvalidUsagePooledEncoderError).Error(), "err should be of type InvalidUsagePooledDecoderError") + }() + _ = enc.EncodeEmbeddedJSON(&v) + assert.True(t, false, "should not be called as it should have panicked") + }) +} diff --git a/encode_interface.go b/encode_interface.go @@ -42,6 +42,8 @@ func (enc *Encoder) Encode(v interface{}) error { return enc.EncodeFloat(vt) case float32: return enc.EncodeFloat32(vt) + case *EmbeddedJSON: + return enc.EncodeEmbeddedJSON(vt) default: return InvalidMarshalError(fmt.Sprintf(invalidMarshalErrorMsg, reflect.TypeOf(vt).String())) }