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:
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", ¶ms2)
+ params3 := EmbeddedJSON([]byte(`"test"`))
+ enc.AddEmbeddedJSONKeyOmitEmpty("params3", ¶ms3)
+ 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()))
}