gojay

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

commit 251e643799caca0d89c83eb077bb5f299a282bce
parent 9501ba82a3fe8710f7a988cb6d98e1b5df664998
Author: francoispqt <francois@parquet.ninja>
Date:   Sat, 28 Apr 2018 20:29:14 +0800

update pooling for encoder, enhance Encode api, add tests for encoding and decoding

Diffstat:
Mbenchmarks/decoder/Makefile | 4++++
Mbenchmarks/encoder/Makefile | 4++++
Mdecode_bool_test.go | 24+++++++++++++++++++++++-
Mdecode_number_test.go | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Mdecode_pool.go | 2+-
Mdecode_stream_test.go | 14++++++++++++++
Mdecode_string_test.go | 23+++++++++++++++++++++++
Mencode.go | 39++++++++++++++++++---------------------
Mencode_array.go | 15+++++++++++++++
Mencode_array_test.go | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mencode_bool.go | 20+++++++++++++++++++-
Aencode_bool_test.go | 36++++++++++++++++++++++++++++++++++++
Mencode_interface.go | 47+++++++++++++++++++++++++++++++++++++++++++++++
Aencode_interface_test.go | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mencode_number.go | 16++++++++++++++++
Mencode_number_test.go | 28++++++++++++++++++++++++++++
Mencode_object.go | 15+++++++++++++++
Mencode_object_test.go | 20+++++++++++++++++---
Mencode_pool.go | 11+++++++++++
Aencode_pool_test.go | 20++++++++++++++++++++
Mencode_string.go | 13++++++++++---
Mencode_string_test.go | 14++++++++++++++
Merrors.go | 18++++++++++++++++++
23 files changed, 571 insertions(+), 30 deletions(-)

diff --git a/benchmarks/decoder/Makefile b/benchmarks/decoder/Makefile @@ -20,6 +20,10 @@ testtrace: .PHONY: benchgojay benchgojay: + go test -benchmem -run=^BenchmarkGoJay -bench=^BenchmarkGoJay -benchtime=30ms + +.PHONY: benchgojaycpu +benchgojaycpu: go test -benchmem -run=^BenchmarkGoJay -bench=^BenchmarkGoJay -benchtime=30ms -cpuprofile cpu.out .PHONY: benchjsoniter diff --git a/benchmarks/encoder/Makefile b/benchmarks/encoder/Makefile @@ -16,6 +16,10 @@ testtrace: .PHONY: benchgojay benchgojay: + go test -benchmem -run=^BenchmarkGoJay -bench=^BenchmarkGoJay -benchtime=30ms + +.PHONY: benchgojaycpu +benchgojaycpu: go test -benchmem -run=^BenchmarkGoJay -bench=^BenchmarkGoJay -benchtime=30ms -cpuprofile cpu.out .PHONY: benchjsoniter diff --git a/decode_bool_test.go b/decode_bool_test.go @@ -1,6 +1,7 @@ package gojay import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -35,7 +36,7 @@ func TestDecoderBoolNonBooleanJSONFalse(t *testing.T) { var v bool err := Unmarshal(json, &v) assert.Nil(t, err, "Err must be nil") - assert.Equal(t, false, v, "v must be equal to true") + assert.Equal(t, false, v, "v must be equal to false") } func TestDecoderBoolInvalidJSON(t *testing.T) { @@ -45,3 +46,24 @@ func TestDecoderBoolInvalidJSON(t *testing.T) { assert.NotNil(t, err, "Err must not be nil as JSON is invalid") assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") } +func TestDecoderBoolDecoderAPI(t *testing.T) { + var v bool + dec := NewDecoder(strings.NewReader("true")) + defer dec.Release() + err := dec.DecodeBool(&v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, true, v, "v must be equal to true") +} + +func TestDecoderBoolPoolError(t *testing.T) { + v := true + dec := NewDecoder(nil) + dec.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnot be nil") + assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledDecoderError") + }() + _ = dec.DecodeBool(&v) + assert.True(t, false, "should not be called as decoder should have panicked") +} diff --git a/decode_number_test.go b/decode_number_test.go @@ -1,6 +1,7 @@ package gojay import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -74,6 +75,14 @@ func TestDecoderIntOverfow2(t *testing.T) { assert.NotNil(t, err, "Err must not be nil as int is overflowing") assert.Equal(t, 0, v, "v must be equal to 0") } +func TestDecoderInttDecoderAPI(t *testing.T) { + var v int + dec := NewDecoder(strings.NewReader(`33`)) + defer dec.Release() + err := dec.DecodeInt(&v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, int(33), v, "v must be equal to 33") +} func TestDecoderInt32Basic(t *testing.T) { json := []byte(`124`) @@ -143,6 +152,14 @@ func TestDecoderInt32PoolError(t *testing.T) { _ = dec.DecodeInt32(&result) assert.True(t, false, "should not be called as decoder should have panicked") } +func TestDecoderInt32tDecoderAPI(t *testing.T) { + var v int32 + dec := NewDecoder(strings.NewReader(`33`)) + defer dec.Release() + err := dec.DecodeInt32(&v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, int32(33), v, "v must be equal to 33") +} func TestDecoderUint32Basic(t *testing.T) { json := []byte(`124`) @@ -206,6 +223,14 @@ func TestDecoderUint32PoolError(t *testing.T) { _ = dec.DecodeUint32(&result) assert.True(t, false, "should not be called as decoder should have panicked") } +func TestDecoderUint32tDecoderAPI(t *testing.T) { + var v uint32 + dec := NewDecoder(strings.NewReader(`33`)) + defer dec.Release() + err := dec.DecodeUint32(&v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, uint32(33), v, "v must be equal to 33") +} func TestDecoderInt64Basic(t *testing.T) { json := []byte(`124`) @@ -275,6 +300,14 @@ func TestDecoderInt64PoolError(t *testing.T) { _ = dec.DecodeInt64(&result) assert.True(t, false, "should not be called as decoder should have panicked") } +func TestDecoderInt64tDecoderAPI(t *testing.T) { + var v int64 + dec := NewDecoder(strings.NewReader(`33`)) + defer dec.Release() + err := dec.DecodeInt64(&v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, int64(33), v, "v must be equal to 33") +} func TestDecoderUint64Basic(t *testing.T) { json := []byte(`124`) var v uint64 @@ -336,6 +369,14 @@ func TestDecoderUint64PoolError(t *testing.T) { _ = dec.DecodeUint64(&result) assert.True(t, false, "should not be called as decoder should have panicked") } +func TestDecoderUint64tDecoderAPI(t *testing.T) { + var v uint64 + dec := NewDecoder(strings.NewReader(`33`)) + defer dec.Release() + err := dec.DecodeUint64(&v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, uint64(33), v, "v must be equal to 33") +} func TestDecoderFloatBasic(t *testing.T) { json := []byte(`100.11`) var v float64 @@ -367,6 +408,14 @@ func TestDecoderFloatInvalidJSON(t *testing.T) { assert.NotNil(t, err, "Err must not be nil as JSON is invalid") assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") } +func TestDecoderFloatDecoderAPI(t *testing.T) { + var v float64 + dec := NewDecoder(strings.NewReader(`1.25`)) + defer dec.Release() + err := dec.DecodeFloat64(&v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, 1.25, v, "v must be equal to 1.25") +} func TestDecoderFloatPoolError(t *testing.T) { result := float64(1) dec := NewDecoder(nil) diff --git a/decode_pool.go b/decode_pool.go @@ -19,7 +19,7 @@ func NewDecoder(r io.Reader) *Decoder { } } -// BorrowDecoder borrows a Decoder a decoder from the pool. +// BorrowDecoder borrows a Decoder from the pool. // It takes an io.Reader implementation as data input. // It initiates the done channel returned by Done(). func BorrowDecoder(r io.Reader, bufSize int) *Decoder { diff --git a/decode_stream_test.go b/decode_stream_test.go @@ -373,3 +373,17 @@ func TestStreamDecodingErrNotSet(t *testing.T) { dec := Stream.NewDecoder(&StreamReader{}) assert.Nil(t, dec.Err(), "dec.Err should be nim") } + +func TestStreamDecodingPoolError(t *testing.T) { + dec := Stream.BorrowDecoder(nil, 0) + dec.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnot be nil") + assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledEncoderError") + assert.Equal(t, "Invalid usage of pooled decoder", err.(InvalidUsagePooledDecoderError).Error(), "err should be of type InvalidUsagePooledDecoderError") + }() + testChan := ChannelStreamStrings(make(chan *string)) + _ = dec.DecodeStream(testChan) + assert.True(t, false, "should not be called as it should have panicked") +} diff --git a/decode_string_test.go b/decode_string_test.go @@ -1,6 +1,7 @@ package gojay import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -45,3 +46,25 @@ func TestDecoderStringInvalidType(t *testing.T) { assert.NotNil(t, err, "Err must not be nil as JSON is invalid") assert.IsType(t, InvalidTypeError(""), err, "err message must be 'Invalid JSON'") } + +func TestDecoderStringDecoderAPI(t *testing.T) { + var v string + dec := NewDecoder(strings.NewReader(`"hello world!"`)) + defer dec.Release() + err := dec.DecodeString(&v) + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, "hello world!", v, "v must be equal to 'hello world!'") +} + +func TestDecoderStringPoolError(t *testing.T) { + result := "" + dec := NewDecoder(nil) + dec.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnot be nil") + assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledDecoderError") + }() + _ = dec.DecodeString(&result) + assert.True(t, false, "should not be called as decoder should have panicked") +} diff --git a/encode.go b/encode.go @@ -24,12 +24,8 @@ package gojay // } func MarshalObject(v MarshalerObject) ([]byte, error) { enc := NewEncoder() - enc.grow(200) - enc.writeByte('{') - v.MarshalObject(enc) - enc.writeByte('}') defer enc.Release() - return enc.buf, nil + return enc.encodeObject(v) } // MarshalArray returns the JSON encoding of v. @@ -98,7 +94,7 @@ func Marshal(v interface{}) ([]byte, error) { var err error = InvalidTypeError("Unknown type to Marshal") switch vt := v.(type) { case MarshalerObject: - enc := NewEncoder() + enc := BorrowEncoder() enc.writeByte('{') vt.MarshalObject(enc) enc.writeByte('}') @@ -106,7 +102,7 @@ func Marshal(v interface{}) ([]byte, error) { defer enc.Release() return b, nil case MarshalerArray: - enc := NewEncoder() + enc := BorrowEncoder() enc.writeByte('[') vt.MarshalArray(enc) enc.writeByte(']') @@ -114,56 +110,56 @@ func Marshal(v interface{}) ([]byte, error) { defer enc.Release() return b, nil case string: - enc := NewEncoder() + enc := BorrowEncoder() b, err = enc.encodeString(vt) defer enc.Release() case bool: - enc := NewEncoder() + enc := BorrowEncoder() err = enc.AddBool(vt) b = enc.buf defer enc.Release() case int: - enc := NewEncoder() + enc := BorrowEncoder() b, err = enc.encodeInt(int64(vt)) defer enc.Release() case int64: - enc := NewEncoder() + enc := BorrowEncoder() defer enc.Release() return enc.encodeInt(vt) case int32: - enc := NewEncoder() + enc := BorrowEncoder() defer enc.Release() return enc.encodeInt(int64(vt)) case int16: - enc := NewEncoder() + enc := BorrowEncoder() defer enc.Release() return enc.encodeInt(int64(vt)) case int8: - enc := NewEncoder() + enc := BorrowEncoder() defer enc.Release() return enc.encodeInt(int64(vt)) case uint64: - enc := NewEncoder() + enc := BorrowEncoder() defer enc.Release() return enc.encodeInt(int64(vt)) case uint32: - enc := NewEncoder() + enc := BorrowEncoder() defer enc.Release() return enc.encodeInt(int64(vt)) case uint16: - enc := NewEncoder() + enc := BorrowEncoder() defer enc.Release() return enc.encodeInt(int64(vt)) case uint8: - enc := NewEncoder() + enc := BorrowEncoder() b, err = enc.encodeInt(int64(vt)) defer enc.Release() case float64: - enc := NewEncoder() + enc := BorrowEncoder() defer enc.Release() return enc.encodeFloat(vt) case float32: - enc := NewEncoder() + enc := BorrowEncoder() defer enc.Release() return enc.encodeFloat(float64(vt)) } @@ -184,7 +180,8 @@ type MarshalerArray interface { // An Encoder writes JSON values to an output stream. type Encoder struct { - buf []byte + buf []byte + isPooled byte } func (enc *Encoder) getPreviousRune() (byte, bool) { diff --git a/encode_array.go b/encode_array.go @@ -1,5 +1,20 @@ package gojay +// EncodeArray encodes an implementation of MarshalerArray to JSON +func (enc *Encoder) EncodeArray(v MarshalerArray) ([]byte, error) { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + return enc.encodeArray(v) +} +func (enc *Encoder) encodeArray(v MarshalerArray) ([]byte, error) { + enc.grow(200) + enc.writeByte('[') + v.MarshalArray(enc) + enc.writeByte(']') + return enc.buf, nil +} + // AddArray adds an array or slice to be encoded, must be used inside a slice or array encoding (does not encode a key) // value must implement Marshaler func (enc *Encoder) AddArray(value MarshalerArray) error { diff --git a/encode_array_test.go b/encode_array_test.go @@ -6,6 +6,14 @@ import ( "github.com/stretchr/testify/assert" ) +type TestEncodingArrStrings []string + +func (t TestEncodingArrStrings) MarshalArray(enc *Encoder) { + for _, e := range t { + enc.AddString(e) + } +} + type TestEncodingArr []*TestEncoding func (t TestEncodingArr) MarshalArray(enc *Encoder) { @@ -104,3 +112,51 @@ func TestEncoderArrayInterfaces(t *testing.T) { string(r), "Result of marshalling is different as the one expected") } + +func TestEncoderArrayInterfacesEncoderAPI(t *testing.T) { + v := &testEncodingArrInterfaces{ + 1, + int64(1), + int32(1), + int16(1), + int8(1), + uint64(1), + uint32(1), + uint16(1), + uint8(1), + float64(1.31), + // float32(1.31), + &TestEncodingArr{}, + true, + "test", + &TestEncoding{ + test: "hello world", + test2: "foobar", + testInt: 1, + testBool: true, + }, + } + enc := BorrowEncoder() + defer enc.Release() + r, err := enc.EncodeArray(v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `[1,1,1,1,1,1,1,1,1.31,[],true,"test",{"test":"hello world","test2":"foobar","testInt":1,"testBool":true,"testArr":[],"testF64":0,"testF32":0}]`, + string(r), + "Result of marshalling is different as the one expected") +} + +func TestEncoderArrayPooledError(t *testing.T) { + v := &testEncodingArrInterfaces{} + enc := BorrowEncoder() + 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.EncodeArray(v) + assert.True(t, false, "should not be called as it should have panicked") +} diff --git a/encode_bool.go b/encode_bool.go @@ -2,6 +2,24 @@ package gojay import "strconv" +// EncodeBool encodes a bool to JSON +func (enc *Encoder) EncodeBool(v bool) ([]byte, error) { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + return enc.encodeBool(v) +} + +// encodeBool encodes a bool to JSON +func (enc *Encoder) encodeBool(v bool) ([]byte, error) { + if v { + enc.writeString("true") + } else { + enc.writeString("false") + } + return enc.buf, nil +} + // AddBool adds a bool to be encoded, must be used inside a slice or array encoding (does not encode a key) func (enc *Encoder) AddBool(value bool) error { r, ok := enc.getPreviousRune() @@ -16,7 +34,7 @@ func (enc *Encoder) AddBool(value bool) error { return nil } -// AddBoolKey adds a bool to be encoded, must be used inside an object as it will encode a key +// AddBoolKey adds a bool to be encoded, must be used inside an object as it will encode a key. func (enc *Encoder) AddBoolKey(key string, value bool) error { r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { diff --git a/encode_bool_test.go b/encode_bool_test.go @@ -0,0 +1,36 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncoderBoolTrue(t *testing.T) { + enc := BorrowEncoder() + defer enc.Release() + b, err := enc.EncodeBool(true) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "true", string(b), "string(b) must be equal to 'true'") +} + +func TestEncoderBoolFalse(t *testing.T) { + enc := BorrowEncoder() + defer enc.Release() + b, err := enc.EncodeBool(false) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "false", string(b), "string(b) must be equal to 'false'") +} + +func TestEncoderBoolPoolError(t *testing.T) { + enc := BorrowEncoder() + 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 InvalidUsagePooledEncoderError") + }() + _, _ = enc.EncodeBool(false) + assert.True(t, false, "should not be called as it should have panicked") +} diff --git a/encode_interface.go b/encode_interface.go @@ -1,5 +1,52 @@ package gojay +import ( + "fmt" + "reflect" +) + +// Encode encodes a value to JSON. +// +// If Encode cannot find a way to encode the type to JSON +// it will return an InvalidMarshalError. +func (enc *Encoder) Encode(v interface{}) ([]byte, error) { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + switch vt := v.(type) { + case string: + return enc.encodeString(vt) + case bool: + return enc.encodeBool(vt) + case MarshalerArray: + return enc.encodeArray(vt) + case MarshalerObject: + return enc.encodeObject(vt) + case int: + return enc.encodeInt(int64(vt)) + case int64: + return enc.encodeInt(vt) + case int32: + return enc.encodeInt(int64(vt)) + case int8: + return enc.encodeInt(int64(vt)) + case uint64: + return enc.encodeInt(int64(vt)) + case uint32: + return enc.encodeInt(int64(vt)) + case uint16: + return enc.encodeInt(int64(vt)) + case uint8: + return enc.encodeInt(int64(vt)) + case float64: + return enc.encodeFloat(vt) + case float32: + return enc.encodeFloat(float64(vt)) + default: + return nil, InvalidMarshalError(fmt.Sprintf(invalidMarshalErrorMsg, reflect.TypeOf(vt).String())) + } +} + // AddInterface adds an interface{} to be encoded, must be used inside a slice or array encoding (does not encode a key) func (enc *Encoder) AddInterface(value interface{}) error { switch value.(type) { diff --git a/encode_interface_test.go b/encode_interface_test.go @@ -0,0 +1,113 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var encoderTestCases = []struct { + v interface{} + expectations func(t *testing.T, b []byte, err error) +}{ + { + v: int64(100), + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, "100", string(b), "string(b) should equal 100") + }, + }, + { + v: int32(100), + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, "100", string(b), "string(b) should equal 100") + }, + }, + { + v: int8(100), + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, "100", string(b), "string(b) should equal 100") + }, + }, + { + v: uint64(100), + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, "100", string(b), "string(b) should equal 100") + }, + }, + { + v: uint32(100), + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, "100", string(b), "string(b) should equal 100") + }, + }, + { + v: uint8(100), + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, "100", string(b), "string(b) should equal 100") + }, + }, + { + v: float64(100.12), + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, "100.12", string(b), "string(b) should equal 100.12") + }, + }, + { + v: true, + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, "true", string(b), "string(b) should equal true") + }, + }, + { + v: "hello world", + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, `"hello world"`, string(b), `string(b) should equal "hello world"`) + }, + }, + { + v: "hello world", + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, `"hello world"`, string(b), `string(b) should equal "hello world"`) + }, + }, + { + v: &TestEncodingArrStrings{"hello world", "foo bar"}, + expectations: func(t *testing.T, b []byte, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, `["hello world","foo bar"]`, string(b), `string(b) should equal ["hello world","foo bar"]`) + }, + }, + { + v: &testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true}, + expectations: func(t *testing.T, b []byte, 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}`) + }, + }, +} + +func TestEncoderInterfaceAllTypesDecoderAPI(t *testing.T) { + for _, test := range encoderTestCases { + enc := BorrowEncoder() + b, err := enc.Encode(test.v) + enc.Release() + test.expectations(t, b, err) + } +} + +func TestEncoderInterfaceAllTypesMarshalAPI(t *testing.T) { + for _, test := range encoderTestCases { + b, err := Marshal(test.v) + test.expectations(t, b, err) + } +} diff --git a/encode_number.go b/encode_number.go @@ -2,6 +2,14 @@ package gojay import "strconv" +// EncodeInt encodes an int to JSON +func (enc *Encoder) EncodeInt(n int64) ([]byte, error) { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + return enc.encodeInt(n) +} + // encodeInt encodes an int to JSON func (enc *Encoder) encodeInt(n int64) ([]byte, error) { s := strconv.Itoa(int(n)) @@ -9,6 +17,14 @@ func (enc *Encoder) encodeInt(n int64) ([]byte, error) { return enc.buf, nil } +// EncodeFloat encodes a float64 to JSON +func (enc *Encoder) EncodeFloat(n float64) ([]byte, error) { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + return enc.encodeFloat(n) +} + // encodeFloat encodes a float64 to JSON func (enc *Encoder) encodeFloat(n float64) ([]byte, error) { s := strconv.FormatFloat(n, 'f', -1, 64) diff --git a/encode_number_test.go b/encode_number_test.go @@ -101,3 +101,31 @@ func TestEncoderFloat(t *testing.T) { string(r), "Result of marshalling is different as the one expected") } + +func TestEncoderIntPooledError(t *testing.T) { + v := 1 + enc := BorrowEncoder() + 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.EncodeInt(int64(v)) + assert.True(t, false, "should not be called as it should have panicked") +} + +func TestEncoderFloatPooledError(t *testing.T) { + v := 1.1 + enc := BorrowEncoder() + 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.EncodeFloat(v) + assert.True(t, false, "should not be called as it should have panicked") +} diff --git a/encode_object.go b/encode_object.go @@ -5,6 +5,21 @@ var objKeyObj = []byte(`":{`) var objKeyArr = []byte(`":[`) var objKey = []byte(`":`) +// EncodeObject encodes an object to JSON +func (enc *Encoder) EncodeObject(v MarshalerObject) ([]byte, error) { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + return enc.encodeObject(v) +} +func (enc *Encoder) encodeObject(v MarshalerObject) ([]byte, error) { + enc.grow(200) + enc.writeByte('{') + v.MarshalObject(enc) + enc.writeByte('}') + return enc.buf, nil +} + // AddObject adds an object to be encoded, must be used inside a slice or array encoding (does not encode a key) // value must implement Marshaler func (enc *Encoder) AddObject(value MarshalerObject) error { diff --git a/encode_object_test.go b/encode_object_test.go @@ -42,7 +42,7 @@ func (t *testObject) MarshalObject(enc *Encoder) { enc.AddBoolKey("testBool", t.testBool) } -func TestEncodeBasicObject(t *testing.T) { +func TestEncoderObjectBasic(t *testing.T) { r, err := Marshal(&testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true}) assert.Nil(t, err, "Error should be nil") assert.Equal( @@ -101,7 +101,7 @@ func (t *SubObject) MarshalObject(enc *Encoder) { enc.AddObjectKey("sub", t.sub) } -func TestEncoderComplexObject(t *testing.T) { +func TestEncoderObjectComplex(t *testing.T) { v := &TestEncoding{ test: "hello world", test2: "foobar", @@ -149,7 +149,7 @@ func (t *testEncodingObjInterfaces) MarshalObject(enc *Encoder) { enc.AddInterfaceKey("interfaceVal", t.interfaceVal) } -func TestObjInterfaces(t *testing.T) { +func TestEncoderObjectInterfaces(t *testing.T) { v := testEncodingObjInterfaces{"string"} r, err := Marshal(&v) assert.Nil(t, err, "Error should be nil") @@ -247,3 +247,17 @@ func TestObjInterfaces(t *testing.T) { string(r), "Result of marshalling is different as the one expected") } + +func TestEncoderObjectPooledError(t *testing.T) { + v := &TestEncoding{} + enc := BorrowEncoder() + 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.EncodeObject(v) + assert.True(t, false, "should not be called as it should have panicked") +} diff --git a/encode_pool.go b/encode_pool.go @@ -4,18 +4,29 @@ var encObjPool = make(chan *Encoder, 16) // NewEncoder returns a new encoder or borrows one from the pool func NewEncoder() *Encoder { + return &Encoder{} +} +func newEncoder() *Encoder { + return &Encoder{} +} + +// BorrowEncoder borrows an Encoder from the pool. +func BorrowEncoder() *Encoder { select { case enc := <-encObjPool: + enc.isPooled = 0 return enc default: return &Encoder{} } } +// Release sends back a Encoder to the pool. func (enc *Encoder) Release() { enc.buf = nil select { case encObjPool <- enc: + enc.isPooled = 1 default: } } diff --git a/encode_pool_test.go b/encode_pool_test.go @@ -0,0 +1,20 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncoderNewFromPool(t *testing.T) { + // reset pool + encObjPool = make(chan *Encoder, 16) + // get new Encoder + enc := NewEncoder() + // add to pool + enc.Release() + // borrow encoder + nEnc := BorrowEncoder() + // make sure it's the same + assert.Equal(t, enc, nEnc, "enc and nEnc from pool should be the same") +} diff --git a/encode_string.go b/encode_string.go @@ -1,6 +1,14 @@ package gojay -// encodeString encodes a string to +// EncodeString encodes a string to +func (enc *Encoder) EncodeString(s string) ([]byte, error) { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + return enc.encodeString(s) +} + +// encodeString encodes a string to func (enc *Encoder) encodeString(s string) ([]byte, error) { enc.writeByte('"') enc.writeString(s) @@ -35,4 +43,4 @@ func (enc *Encoder) AddStringKey(key, value string) error { enc.writeByte('"') return nil -} -\ No newline at end of file +} diff --git a/encode_string_test.go b/encode_string_test.go @@ -25,3 +25,17 @@ func TestEncoderStringUTF8(t *testing.T) { string(r), "Result of marshalling is different as the one expected") } + +func TestEncoderStringPooledError(t *testing.T) { + v := "" + enc := BorrowEncoder() + 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.EncodeString(v) + assert.True(t, false, "should not be called as it should have panicked") +} diff --git a/errors.go b/errors.go @@ -26,6 +26,16 @@ func (err InvalidUnmarshalError) Error() string { return string(err) } +const invalidMarshalErrorMsg = "Invalid type %s provided to Marshal" + +// InvalidMarshalError is a type representing an error returned when +// Encoding did not find the proper way to encode +type InvalidMarshalError string + +func (err InvalidMarshalError) Error() string { + return string(err) +} + // NoReaderError is a type representing an error returned when // decoding requires a reader and none was given type NoReaderError string @@ -41,3 +51,11 @@ type InvalidUsagePooledDecoderError string func (err InvalidUsagePooledDecoderError) Error() string { return string(err) } + +// InvalidUsagePooledEncoderError is a type representing an error returned +// when decoding is called on a still pooled Encoder +type InvalidUsagePooledEncoderError string + +func (err InvalidUsagePooledEncoderError) Error() string { + return string(err) +}