gojay

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

commit 4c9828d686c0ea439ab0304b6f1c57760cc70e13
parent a5068885b61dc3bbf5c753d34a3e2647772d3a00
Author: Salim Afiune <afiune@chef.io>
Date:   Wed, 22 Aug 2018 15:10:25 +0200

Adds DecodeInterface for interface decoding

Temporal solution to the unmarshaling of interfaces in Go

Signed-off-by: Salim Afiune <afiune@chef.io>

Diffstat:
Mdecode.go | 17+++++++++++++++++
Adecode_interface.go | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_interface_test.go | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mencode_interface_test.go | 2+-
Mencode_object_test.go | 4++--
Mgojay_test.go | 9++++++++-
6 files changed, 300 insertions(+), 4 deletions(-)

diff --git a/decode.go b/decode.go @@ -227,6 +227,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())) } @@ -333,6 +335,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 { @@ -527,6 +534,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,103 @@ +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 + } + + 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 + } + 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,169 @@ +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: "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, + }, + } + + for _, testCase := range testCases { + t.Run(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 TestDecodeInterfaceObject(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-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") + } + }) + } +} diff --git a/encode_interface_test.go b/encode_interface_test.go @@ -112,7 +112,7 @@ var encoderTestCases = []struct { }, }, { - v: &testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true, &testObject{}, testSliceInts{}}, + v: &testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true, &testObject{}, testSliceInts{}, []interface{}{"h", "o", "l", "a"}}, expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") assert.Equal(t, `{"testStr":"漢字","testInt":1,"testInt64":1,"testInt32":1,"testInt16":1,"testInt8":1,"testUint64":1,"testUint32":1,"testUint16":1,"testUint8":1,"testFloat64":1.1,"testFloat32":1.1,"testBool":true}`, string(b), `string(b) should equal {"testStr":"漢字","testInt":1,"testInt64":1,"testInt32":1,"testInt16":1,"testInt8":1,"testUint64":1,"testUint32":1,"testUint16":1,"testUint8":1,"testFloat64":1.1,"testFloat32":1.1,"testBool":true}`) diff --git a/encode_object_test.go b/encode_object_test.go @@ -83,7 +83,7 @@ func TestEncoderObjectEncodeAPI(t *testing.T) { t.Run("encode-basic", func(t *testing.T) { builder := &strings.Builder{} enc := NewEncoder(builder) - err := enc.EncodeObject(&testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true, &testObject{}, testSliceInts{}}) + err := enc.EncodeObject(&testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true, &testObject{}, testSliceInts{}, interface{}("test")}) assert.Nil(t, err, "Error should be nil") assert.Equal( t, @@ -96,7 +96,7 @@ func TestEncoderObjectEncodeAPI(t *testing.T) { func TestEncoderObjectMarshalAPI(t *testing.T) { t.Run("marshal-basic", func(t *testing.T) { - r, err := Marshal(&testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true, &testObject{}, testSliceInts{}}) + r, err := Marshal(&testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true, &testObject{}, testSliceInts{}, []interface{}{"h", "o", "l", "a"}}) assert.Nil(t, err, "Error should be nil") assert.Equal( t, diff --git a/gojay_test.go b/gojay_test.go @@ -16,6 +16,7 @@ type testObject struct { testBool bool testSubObject *testObject testSubArray testSliceInts + testInterface interface{} } // make sure it implements interfaces @@ -70,12 +71,14 @@ func (t *testObject) 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 } func (t *testObject) NKeys() int { - return 13 + return 14 } type testObject0Keys struct { @@ -94,6 +97,7 @@ type testObject0Keys struct { testBool bool testSubObject *testObject0Keys testSubArray testSliceInts + testInterface interface{} } // make sure it implements interfaces @@ -118,6 +122,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 { @@ -148,6 +153,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 }