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:
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
}