gojay

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

commit 0d1a893740b14aedd39b5ade2e7567645a40eb0b
parent 491f8af345159c2f188f718fd05bef53aa3ef734
Author: Francois Parquet <francois.parquet@gmail.com>
Date:   Sun, 26 Aug 2018 23:14:50 +0800

Merge pull request #68 from francoispqt/afiune-afiune/add-interface-decoder

Afiune afiune/add interface decoder
Diffstat:
Mdecode.go | 23+++++++++++++++++++++++
Adecode_interface.go | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_interface_test.go | 531+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdecode_test.go | 16++++++++++++++++
Mencode_interface_test.go | 2+-
Mencode_object_test.go | 4++--
Mgojay_test.go | 9++++++++-
7 files changed, 692 insertions(+), 4 deletions(-)

diff --git a/decode.go b/decode.go @@ -215,6 +215,12 @@ func Unmarshal(data []byte, v interface{}) error { dec.data = make([]byte, len(data)) copy(dec.data, data) _, err = dec.decodeArray(vt) + case *interface{}: + dec = borrowDecoder(nil, 0) + dec.length = len(data) + dec.data = make([]byte, len(data)) + copy(dec.data, data) + err = dec.decodeInterface(vt) default: return InvalidUnmarshalError(fmt.Sprintf(invalidUnmarshalErrorMsg, reflect.TypeOf(vt).String())) } @@ -318,6 +324,8 @@ func (dec *Decoder) Decode(v interface{}) error { _, err = dec.decodeArray(vt) case *EmbeddedJSON: err = dec.decodeEmbeddedJSON(vt) + case *interface{}: + err = dec.decodeInterface(vt) default: return InvalidUnmarshalError(fmt.Sprintf(invalidUnmarshalErrorMsg, reflect.TypeOf(vt).String())) } @@ -523,6 +531,11 @@ func (dec *Decoder) AddArray(v UnmarshalerJSONArray) error { return dec.Array(v) } +// AddInterface decodes the next key to a interface{}. +func (dec *Decoder) AddInterface(v *interface{}) error { + return dec.Interface(v) +} + // Int decodes the next key to an *int. // If next key value overflows int, an InvalidUnmarshalError error will be returned. func (dec *Decoder) Int(v *int) error { @@ -868,6 +881,16 @@ func (dec *Decoder) Array(value UnmarshalerJSONArray) error { return nil } +// Interface decodes the next key to an interface{}. +func (dec *Decoder) Interface(value *interface{}) error { + err := dec.decodeInterface(value) + if err != nil { + return err + } + dec.called |= 1 + return nil +} + // Non exported func isDigit(b byte) bool { diff --git a/decode_interface.go b/decode_interface.go @@ -0,0 +1,111 @@ +package gojay + +// TODO @afiune for now we are using the standard json unmarshaling but in +// the future it would be great to implement one here inside this repo +import ( + "encoding/json" +) + +// DecodeInterface reads the next JSON-encoded value from its input and stores it in the value pointed to by i. +// +// i must be an interface poiter +func (dec *Decoder) DecodeInterface(i *interface{}) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + err := dec.decodeInterface(i) + return err +} + +func (dec *Decoder) decodeInterface(i *interface{}) error { + start, end, err := dec.getObject() + if err != nil { + dec.cursor = start + return err + } + + // if start & end are equal the object is a null, don't unmarshal + if start == end { + return nil + } + + object := dec.data[start:end] + if err = json.Unmarshal(object, i); err != nil { + return err + } + + dec.cursor = end + return nil +} + +// @afiune Maybe return the type as well? +func (dec *Decoder) getObject() (start int, end int, err error) { + // start cursor + start = dec.cursor + for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { + switch dec.data[dec.cursor] { + case ' ', '\n', '\t', '\r', ',': + continue + // is null + case 'n': + dec.cursor++ + err = dec.assertNull() + if err != nil { + return + } + // Set start & end to the same cursor to indicate the object + // is a null and should not be unmarshal + start = dec.cursor + end = dec.cursor + dec.cursor++ + return + case 't': + dec.cursor++ + err = dec.assertTrue() + if err != nil { + return + } + end = dec.cursor + dec.cursor++ + return + // is false + case 'f': + dec.cursor++ + err = dec.assertFalse() + if err != nil { + return + } + end = dec.cursor + dec.cursor++ + return + // is an object + case '{': + dec.cursor++ + end, err = dec.skipObject() + dec.cursor = end + return + // is string + case '"': + dec.cursor++ + start, end, err = dec.getString() + start-- + dec.cursor = end + return + // is array + case '[': + dec.cursor++ + end, err = dec.skipArray() + dec.cursor = end + return + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': + end, err = dec.skipNumber() + dec.cursor = end + return + default: + err = dec.raiseInvalidJSONErr(dec.cursor) + return + } + } + err = dec.raiseInvalidJSONErr(dec.cursor) + return +} diff --git a/decode_interface_test.go b/decode_interface_test.go @@ -0,0 +1,531 @@ +package gojay + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeInterfaceBasic(t *testing.T) { + testCases := []struct { + name string + json string + expectedResult interface{} + err bool + errType interface{} + skipCheckResult bool + }{ + { + name: "array", + json: `[1,2,3]`, + expectedResult: []interface{}([]interface{}{float64(1), float64(2), float64(3)}), + err: false, + }, + { + name: "object", + json: `{"testStr": "hello world!"}`, + expectedResult: map[string]interface{}(map[string]interface{}{"testStr": "hello world!"}), + err: false, + }, + { + name: "string", + json: `"hola amigos!"`, + expectedResult: interface{}("hola amigos!"), + err: false, + }, + { + name: "bool-true", + json: `true`, + expectedResult: interface{}(true), + err: false, + }, + { + name: "bool-false", + json: `false`, + expectedResult: interface{}(false), + err: false, + }, + { + name: "null", + json: `null`, + expectedResult: interface{}(nil), + err: false, + }, + { + name: "number", + json: `1234`, + expectedResult: interface{}(float64(1234)), + err: false, + }, + { + name: "array-error", + json: `["h""o","l","a"]`, + err: true, + errType: &json.SyntaxError{}, + skipCheckResult: true, + }, + { + name: "object-error", + json: `{"testStr" "hello world!"}`, + err: true, + errType: &json.SyntaxError{}, + skipCheckResult: true, + }, + { + name: "string-error", + json: `"hola amigos!`, + err: true, + errType: InvalidJSONError(""), + skipCheckResult: true, + }, + { + name: "bool-true-error", + json: `truee`, + err: true, + errType: InvalidJSONError(""), + skipCheckResult: true, + }, + { + name: "bool-false-error", + json: `fase`, + expectedResult: interface{}(false), + err: true, + errType: InvalidJSONError(""), + skipCheckResult: true, + }, + { + name: "null-error", + json: `nulllll`, + err: true, + errType: InvalidJSONError(""), + skipCheckResult: true, + }, + { + name: "number-error", + json: `1234"`, + err: true, + errType: InvalidJSONError(""), + skipCheckResult: true, + }, + { + name: "unknown-error", + json: `?`, + err: true, + errType: InvalidJSONError(""), + skipCheckResult: true, + }, + { + name: "empty-json-error", + json: ``, + err: true, + errType: InvalidJSONError(""), + skipCheckResult: true, + }, + } + + for _, testCase := range testCases { + t.Run("DecodeInterface()"+testCase.name, func(t *testing.T) { + var i interface{} + dec := BorrowDecoder(strings.NewReader(testCase.json)) + defer dec.Release() + err := dec.DecodeInterface(&i) + if testCase.err { + t.Log(err) + assert.NotNil(t, err, "err should not be nil") + if testCase.errType != nil { + assert.IsType(t, testCase.errType, err, "err should be of the given type") + } + return + } + assert.Nil(t, err, "err should be nil") + if !testCase.skipCheckResult { + assert.Equal(t, testCase.expectedResult, i, "value at given index should be the same as expected results") + } + }) + } + + for _, testCase := range testCases { + t.Run("Decode()"+testCase.name, func(t *testing.T) { + var i interface{} + dec := BorrowDecoder(strings.NewReader(testCase.json)) + defer dec.Release() + err := dec.Decode(&i) + if testCase.err { + t.Log(err) + assert.NotNil(t, err, "err should not be nil") + if testCase.errType != nil { + assert.IsType(t, testCase.errType, err, "err should be of the given type") + } + return + } + assert.Nil(t, err, "err should be nil") + if !testCase.skipCheckResult { + assert.Equal(t, testCase.expectedResult, i, "value at given index should be the same as expected results") + } + }) + } +} + +func TestDecodeInterfaceAsInterface(t *testing.T) { + testCases := []struct { + name string + json string + expectedResult interface{} + err bool + errType interface{} + skipCheckResult bool + }{ + { + name: "basic-array", + json: `{ + "testStr": "hola", + "testInterface": ["h","o","l","a"] + }`, + expectedResult: map[string]interface{}( + map[string]interface{}{ + "testStr": "hola", + "testInterface": []interface{}{"h", "o", "l", "a"}, + }), + err: false, + }, + { + name: "basic-string", + json: `{ + "testInterface": "漢字" + }`, + expectedResult: map[string]interface{}( + map[string]interface{}{ + "testInterface": "漢字", + }), + err: false, + }, + { + name: "basic-error", + json: `{ + "testInterface": ["a""d","i","o","s"] + }`, + err: true, + errType: &json.SyntaxError{}, + skipCheckResult: true, + }, + { + name: "basic-interface", + json: `{ + "testInterface": { + "string": "prost" + } + }`, + expectedResult: map[string]interface{}( + map[string]interface{}{ + "testInterface": map[string]interface{}{"string": "prost"}, + }), + err: false, + }, + { + name: "complex-interface", + json: `{ + "testInterface": { + "number": 1988, + "string": "prost", + "array": ["h","o","l","a"], + "object": { + "k": "v", + "a": [1,2,3] + }, + "array-of-objects": [ + {"k": "v"}, + {"a": "b"} + ] + } + }`, + expectedResult: map[string]interface{}( + map[string]interface{}{ + "testInterface": map[string]interface{}{ + "array-of-objects": []interface{}{ + map[string]interface{}{"k": "v"}, + map[string]interface{}{"a": "b"}, + }, + "number": float64(1988), + "string": "prost", + "array": []interface{}{"h", "o", "l", "a"}, + "object": map[string]interface{}{ + "k": "v", + "a": []interface{}{float64(1), float64(2), float64(3)}, + }, + }, + }), + err: false, + }, + } + + for _, testCase := range testCases { + t.Run("Decode()"+testCase.name, func(t *testing.T) { + var s interface{} + dec := BorrowDecoder(strings.NewReader(testCase.json)) + defer dec.Release() + err := dec.Decode(&s) + if testCase.err { + t.Log(err) + assert.NotNil(t, err, "err should not be nil") + if testCase.errType != nil { + assert.IsType(t, testCase.errType, err, "err should be of the given type") + } + return + } + assert.Nil(t, err, "err should be nil") + if !testCase.skipCheckResult { + assert.Equal(t, testCase.expectedResult, s, "value at given index should be the same as expected results") + } + }) + } + + for _, testCase := range testCases { + t.Run("DecodeInterface()"+testCase.name, func(t *testing.T) { + var s interface{} + dec := BorrowDecoder(strings.NewReader(testCase.json)) + defer dec.Release() + err := dec.DecodeInterface(&s) + if testCase.err { + t.Log(err) + assert.NotNil(t, err, "err should not be nil") + if testCase.errType != nil { + assert.IsType(t, testCase.errType, err, "err should be of the given type") + } + return + } + assert.Nil(t, err, "err should be nil") + if !testCase.skipCheckResult { + assert.Equal(t, testCase.expectedResult, s, "value at given index should be the same as expected results") + } + }) + } +} + +func TestDecodeAsTestObject(t *testing.T) { + testCases := []struct { + name string + json string + expectedResult testObject + err bool + errType interface{} + skipCheckResult bool + }{ + { + name: "basic-array", + json: `{ + "testStr": "hola", + "testInterface": ["h","o","l","a"] + }`, + expectedResult: testObject{ + testStr: "hola", + testInterface: []interface{}([]interface{}{"h", "o", "l", "a"}), + }, + err: false, + }, + { + name: "basic-string", + json: `{ + "testInterface": "漢字" + }`, + expectedResult: testObject{ + testInterface: interface{}("漢字"), + }, + err: false, + }, + { + name: "basic-error", + json: `{ + "testInterface": ["a""d","i","o","s"] + }`, + err: true, + errType: &json.SyntaxError{}, + skipCheckResult: true, + }, + { + name: "mull-interface", + json: `{ + "testInterface": null, + "testStr": "adios" + }`, + expectedResult: testObject{ + testInterface: interface{}(nil), + testStr: "adios", + }, + err: false, + }, + { + name: "basic-interface", + json: `{ + "testInterface": { + "string": "prost" + }, + }`, + expectedResult: testObject{ + testInterface: map[string]interface{}{"string": "prost"}, + }, + err: false, + }, + { + name: "complex-interface", + json: `{ + "testInterface": { + "number": 1988, + "string": "prost", + "array": ["h","o","l","a"], + "object": { + "k": "v", + "a": [1,2,3] + }, + "array-of-objects": [ + {"k": "v"}, + {"a": "b"} + ] + }, + }`, + expectedResult: testObject{ + testInterface: map[string]interface{}{ + "array-of-objects": []interface{}{ + map[string]interface{}{"k": "v"}, + map[string]interface{}{"a": "b"}, + }, + "number": float64(1988), + "string": "prost", + "array": []interface{}{"h", "o", "l", "a"}, + "object": map[string]interface{}{ + "k": "v", + "a": []interface{}{float64(1), float64(2), float64(3)}, + }, + }, + }, + err: false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + s := testObject{} + dec := BorrowDecoder(strings.NewReader(testCase.json)) + defer dec.Release() + err := dec.Decode(&s) + if testCase.err { + t.Log(err) + assert.NotNil(t, err, "err should not be nil") + if testCase.errType != nil { + assert.IsType(t, testCase.errType, err, "err should be of the given type") + } + return + } + assert.Nil(t, err, "err should be nil") + if !testCase.skipCheckResult { + assert.Equal(t, testCase.expectedResult, s, "value at given index should be the same as expected results") + } + }) + } +} + +func TestUnmarshalInterface(t *testing.T) { + json := []byte(`{ + "testInterface": { + "number": 1988, + "null": null, + "string": "prost", + "array": ["h","o","l","a"], + "object": { + "k": "v", + "a": [1,2,3] + }, + "array-of-objects": [ + {"k": "v"}, + {"a": "b"} + ] + } + }`) + v := &testObject{} + err := Unmarshal(json, v) + assert.Nil(t, err, "Err must be nil") + expectedInterface := map[string]interface{}{ + "array-of-objects": []interface{}{ + map[string]interface{}{"k": "v"}, + map[string]interface{}{"a": "b"}, + }, + "number": float64(1988), + "string": "prost", + "null": interface{}(nil), + "array": []interface{}{"h", "o", "l", "a"}, + "object": map[string]interface{}{ + "k": "v", + "a": []interface{}{float64(1), float64(2), float64(3)}, + }, + } + assert.Equal(t, expectedInterface, v.testInterface, "v.testInterface must be equal to the expected one") +} + +func TestUnmarshalInterfaceError(t *testing.T) { + testCases := []struct { + name string + json []byte + }{ + { + name: "basic", + json: []byte(`{"testInterface": {"number": 1bc4}}`), + }, + { + name: "syntax", + json: []byte(`{ + "testInterface": { + "array?": [1,"a", ?] + } + }`), + }, + { + name: "complex", + json: []byte(`{ + "testInterface": { + "number": 1988, + "string": "prost", + "array": ["h""o","l","a"], + "object": { + "k": "v", + "a": [1,2,3] + }, + "array-of-objects": [ + {"k": "v"}, + {"a": "b"} + ] + } + }`), + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + v := &testObject{} + err := Unmarshal(testCase.json, v) + assert.NotNil(t, err, "Err must be not nil") + t.Log(err) + assert.IsType(t, &json.SyntaxError{}, err, "err should be a json.SyntaxError{}") + }) + } +} + +func TestDecodeInterfacePoolError(t *testing.T) { + result := interface{}(1) + dec := NewDecoder(nil) + dec.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnt be nil") + assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledDecoderError") + }() + _ = dec.DecodeInterface(&result) + assert.True(t, false, "should not be called as decoder should have panicked") +} + +func TestDecodeNull(t *testing.T) { + var i interface{} + dec := BorrowDecoder(strings.NewReader("null")) + defer dec.Release() + err := dec.DecodeInterface(&i) + assert.Nil(t, err, "err should be nil") + assert.Equal(t, interface{}(nil), i, "value at given index should be the same as expected results") +} diff --git a/decode_test.go b/decode_test.go @@ -208,6 +208,22 @@ func allTypesTestCases() []allTypeDecodeTestCase { }, }, { + v: new(interface{}), + d: []byte(`[{"test":"test"},{"test":"test2"}]`), + name: "test decode interface", + expectations: func(err error, v interface{}, t *testing.T) { + assert.Nil(t, err, "err must be nil") + // v is a pointer to an interface{}, we need to extract the content + vCont := reflect.ValueOf(v).Elem().Interface() + vt := vCont.([]interface{}) + assert.Len(t, vt, 2, "len of vt must be 2") + vt1 := vt[0].(map[string]interface{}) + assert.Equal(t, "test", vt1["test"], "vt1['test'] must be equal to 'test'") + vt2 := vt[1].(map[string]interface{}) + assert.Equal(t, "test2", vt2["test"], "vt2['test'] must be equal to 'test2'") + }, + }, + { v: new(uint64), d: []byte(`-1`), name: "test decode uint64 negative", diff --git a/encode_interface_test.go b/encode_interface_test.go @@ -115,7 +115,7 @@ var encoderTestCases = []struct { v: &testObject{ "漢字", nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1.1, nil, 1.1, nil, true, nil, - &testObject{}, testSliceInts{}, + &testObject{}, testSliceInts{}, []interface{}{"h", "o", "l", "a"}, }, expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") diff --git a/encode_object_test.go b/encode_object_test.go @@ -86,7 +86,7 @@ func TestEncoderObjectEncodeAPI(t *testing.T) { err := enc.EncodeObject(&testObject{ "漢字", nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1.1, nil, 1.1, nil, true, nil, - &testObject{}, testSliceInts{}, + &testObject{}, testSliceInts{}, interface{}("test"), }) assert.Nil(t, err, "Error should be nil") assert.Equal( @@ -103,7 +103,7 @@ func TestEncoderObjectMarshalAPI(t *testing.T) { r, err := Marshal(&testObject{ "漢字", nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1, nil, 1.1, nil, 1.1, nil, true, nil, - &testObject{}, testSliceInts{}, + &testObject{}, testSliceInts{}, []interface{}{"h", "o", "l", "a"}, }) assert.Nil(t, err, "Error should be nil") assert.Equal( diff --git a/gojay_test.go b/gojay_test.go @@ -29,6 +29,7 @@ type testObject struct { testBoolNull *bool testSubObject *testObject testSubArray testSliceInts + testInterface interface{} } // make sure it implements interfaces @@ -109,12 +110,14 @@ func (t *testObject) UnmarshalJSONObject(dec *Decoder, k string) error { return dec.AddBool(&t.testBool) case "testBoolNull": return dec.AddBoolNull(&t.testBoolNull) + case "testInterface": + return dec.AddInterface(&t.testInterface) } return nil } func (t *testObject) NKeys() int { - return 28 + return 29 } type testObject0Keys struct { @@ -133,6 +136,7 @@ type testObject0Keys struct { testBool bool testSubObject *testObject0Keys testSubArray testSliceInts + testInterface interface{} } // make sure it implements interfaces @@ -157,6 +161,7 @@ func (t *testObject0Keys) MarshalJSONObject(enc *Encoder) { enc.AddFloatKey("testFloat64", t.testFloat64) enc.AddFloat32Key("testFloat32", t.testFloat32) enc.AddBoolKey("testBool", t.testBool) + enc.AddInterfaceKey("testInterface", t.testInterface) } func (t *testObject0Keys) UnmarshalJSONObject(dec *Decoder, k string) error { @@ -187,6 +192,8 @@ func (t *testObject0Keys) UnmarshalJSONObject(dec *Decoder, k string) error { return dec.AddFloat32(&t.testFloat32) case "testBool": return dec.AddBool(&t.testBool) + case "testInterface": + return dec.AddInterface(&t.testInterface) } return nil }