gojay

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

commit 076bacd6bee0bd367dc621e9789fbb9f219c459a
parent 663ab7248ff6fee90c4f7b3359f2cc414fce9a91
Author: Francois Parquet <francois.parquet@gmail.com>
Date:   Sat, 28 Apr 2018 21:27:02 +0800

Merge pull request #8 from francoispqt/update/make-pooling-safer-to-use

Update/make pooling safer to use
Diffstat:
MREADME.md | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mbenchmarks/decoder/Makefile | 4++++
Mbenchmarks/encoder/Makefile | 4++++
Mdecode.go | 109++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mdecode_array.go | 9++++++++-
Mdecode_array_test.go | 20++++++++++++++++++++
Mdecode_bool.go | 6++++++
Mdecode_bool_test.go | 24+++++++++++++++++++++++-
Mdecode_number.go | 38++++++++++++++++++++++++++++++++++++++
Mdecode_number_test.go | 122++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mdecode_object.go | 12+++++++++++-
Mdecode_object_test.go | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mdecode_pool.go | 29+++++++++++++++++++++++------
Mdecode_stream.go | 22+++++++++-------------
Adecode_stream_pool.go | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_stream_pool_test.go | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdecode_stream_test.go | 14++++++++++++++
Mdecode_string.go | 6++++++
Mdecode_string_test.go | 23+++++++++++++++++++++++
Mdecode_unsafe.go | 54+++++++++++++++++++++++++++---------------------------
Mencode.go | 73+++++++++++++++++++++++++++++++++++--------------------------------------
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 | 13++++++++++++-
Aencode_pool_test.go | 20++++++++++++++++++++
Mencode_string.go | 13++++++++++---
Mencode_string_test.go | 14++++++++++++++
Merrors.go | 26++++++++++++++++++++++++++
36 files changed, 1202 insertions(+), 173 deletions(-)

diff --git a/README.md b/README.md @@ -10,9 +10,10 @@ GoJay is a performant JSON encoder/decoder for Golang (currently the most performant, [see benchmarks](#benchmark-results)). -It has a simple API and doesn't use reflection. It relies on small interfaces to decode/encode structures and slices. +It has a simple API and doesn't use reflection. It relies on small interfaces to decode/encode structures and slices. + +Gojay also comes with powerful stream decoding features and an even faster [Unsafe](#unsafe-api) API. -Gojay also comes with powerful stream decoding features. # Get started @@ -22,7 +23,12 @@ go get github.com/francoispqt/gojay ## Decoding -Example of basic stucture decoding: +Decoding is done through two different API similar to standard `encoding/json`: +* [Unmarshal](#unmarshal-api) +* [Decode](#decode-api) + + +Example of basic stucture decoding with Unmarshal: ```go import "github.com/francoispqt/gojay" @@ -57,18 +63,79 @@ func main() { } ``` -Or with the Decoder API (which takes an io.Reader): +with Decode: ```go func main() { u := &user{} - dec := gojay.NewDecoder(strings.NewReader(`{"id":1,"name":"gojay","email":"gojay@email.com"}`)) - err := dec.Decode(u) + dec := gojay.NewDecoder(bytes.NewReader([]byte(`{"id":1,"name":"gojay","email":"gojay@email.com"}`))) + err := dec.DecodeObject(d, u) if err != nil { log.Fatal(err) } } ``` +### Unmarshal API + +Unmarshal API decodes a `[]byte` to a given pointer with a single function. + +Behind the doors, Unmarshal API borrows a `*gojay.Decoder` resets its settings and decodes the data to the given pointer and releases the `*gojay.Decoder` to the pool when it finishes, whether it encounters an error or not. + +If it cannot find the right Decoding strategy for the type of the given pointer, it returns an `InvalidUnmarshalError`. You can test the error returned by doing `if ok := err.(InvalidUnmarshalError); ok {}`. + +Unmarshal API comes with three functions: +* Unmarshal +```go +func Unmarshal(data []byte, v Interface{}) error +``` + +* UnmarshalObject +```go +func UnmarshalObject(data []byte, v UnmarshalerObject) error +``` + +* UnmarshalArray +```go +func UnmarshalArray(data []byte, v UnmarshalerArray) error +``` + + +### Decode API + +Decode API decodes a `[]byte` to a given pointer by creating or borrowing a `*gojay.Decoder` with an `io.Reader` and calling `Decode` methods. + +*Getting a *gojay.Decoder or Borrowing* + +You can either get a fresh `*gojay.Decoder` calling `dec := gojay.NewDecoder(io.Reader)` or borrow one from the pool by calling `dec := gojay.BorrowDecoder(io.Reader)`. + +After using a decoder, you can release it by calling `dec.Release()`. Beware, if you reuse the decoder after releasing it, it will panic with an error of type `InvalidUsagePooledDecoderError`. If you want to fully benefit from the pooling, you must release your decoders after using. + +`*gojay.Decoder` has multiple methods to decode to specific types: +* Decode +```go +func (dec *Decoder) DecodeInt(v *int) error +``` +* DecodeObject +```go +func (dec *Decoder) DecodeObject(v UnmarshalerObject) error +``` +* DecodeArray +```go +func (dec *Decoder) DecodeArray(v UnmarshalerArray) error +``` +* DecodeInt +```go +func (dec *Decoder) DecodeInt(v *int) error +``` +* DecodeBool +```go +func (dec *Decoder) DecodeBool(v *bool) error +``` +* DecodeString +```go +func (dec *Decoder) DecodeString(v *string) error +``` + ### Structs #### UnmarshalerObject Interface @@ -300,6 +367,19 @@ func main() { } ``` +# Unsafe API + +Unsafe API has the same functions than the regular API, it only has `Unmarshal API` for now. It is unsafe because it makes assumptions on the quality of the given JSON. + +If you are not sure if you're JSON is valid, don't use the Unsafe API. + +Also, the `Unsafe` API does not copy the buffer when using Unmarshal API, which, in case of string decoding, can lead to data corruption if a byte buffer is reused. Using the `Decode` API makes `Unsafe` API safer as the io.Reader relies on `copy` builtin method and `Decoder` will have its own internal buffer :) + +Access the `Unsafe` API this way: +```go +gojay.Unsafe.Unmarshal(b, v) +``` + # Benchmarks 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.go b/decode.go @@ -13,12 +13,12 @@ import ( // If a JSON value is not appropriate for a given target type, or if a JSON number // overflows the target type, UnmarshalArray skips that field and completes the unmarshaling as best it can. func UnmarshalArray(data []byte, v UnmarshalerArray) error { - dec := newDecoder(nil, 0) + dec := BorrowDecoder(nil, 0) + defer dec.Release() dec.data = make([]byte, len(data)) copy(dec.data, data) dec.length = len(data) - _, err := dec.DecodeArray(v) - dec.addToPool() + _, err := dec.decodeArray(v) if err != nil { return err } @@ -35,12 +35,12 @@ func UnmarshalArray(data []byte, v UnmarshalerArray) error { // If a JSON value is not appropriate for a given target type, or if a JSON number // overflows the target type, UnmarshalObject skips that field and completes the unmarshaling as best it can. func UnmarshalObject(data []byte, v UnmarshalerObject) error { - dec := newDecoder(nil, 0) + dec := BorrowDecoder(nil, 0) + defer dec.Release() dec.data = make([]byte, len(data)) copy(dec.data, data) dec.length = len(data) - _, err := dec.DecodeObject(v) - dec.addToPool() + _, err := dec.decodeObject(v) if err != nil { return err } @@ -73,61 +73,61 @@ func Unmarshal(data []byte, v interface{}) error { var dec *Decoder switch vt := v.(type) { case *string: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeString(vt) + err = dec.decodeString(vt) case *int: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeInt(vt) + err = dec.decodeInt(vt) case *int32: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeInt32(vt) + err = dec.decodeInt32(vt) case *uint32: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeUint32(vt) + err = dec.decodeUint32(vt) case *int64: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeInt64(vt) + err = dec.decodeInt64(vt) case *uint64: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeUint64(vt) + err = dec.decodeUint64(vt) case *float64: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeFloat64(vt) + err = dec.decodeFloat64(vt) case *bool: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeBool(vt) + err = dec.decodeBool(vt) case UnmarshalerObject: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = make([]byte, len(data)) copy(dec.data, data) - _, err = dec.DecodeObject(vt) + _, err = dec.decodeObject(vt) case UnmarshalerArray: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = make([]byte, len(data)) copy(dec.data, data) - _, err = dec.DecodeArray(vt) + _, err = dec.decodeArray(vt) default: return InvalidUnmarshalError(fmt.Sprintf(invalidUnmarshalErrorMsg, reflect.TypeOf(vt).String())) } - defer dec.addToPool() + defer dec.Release() if err != nil { return err } @@ -147,12 +147,6 @@ type UnmarshalerArray interface { UnmarshalArray(*Decoder) error } -// UnmarshalerStream is the interface to implement for a slice, an array or a slice -// to decode a line delimited JSON to. -type UnmarshalerStream interface { - UnmarshalStream(*StreamDecoder) error -} - // A Decoder reads and decodes JSON values from an input stream. type Decoder struct { data []byte @@ -163,34 +157,38 @@ type Decoder struct { child byte err error r io.Reader + isPooled byte } // Decode reads the next JSON-encoded value from its input and stores it in the value pointed to by v. // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. func (dec *Decoder) Decode(v interface{}) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } switch vt := v.(type) { case *string: - return dec.DecodeString(vt) + return dec.decodeString(vt) case *int: - return dec.DecodeInt(vt) + return dec.decodeInt(vt) case *int32: - return dec.DecodeInt32(vt) + return dec.decodeInt32(vt) case *uint32: - return dec.DecodeUint32(vt) + return dec.decodeUint32(vt) case *int64: - return dec.DecodeInt64(vt) + return dec.decodeInt64(vt) case *uint64: - return dec.DecodeUint64(vt) + return dec.decodeUint64(vt) case *float64: - return dec.DecodeFloat64(vt) + return dec.decodeFloat64(vt) case *bool: - return dec.DecodeBool(vt) + return dec.decodeBool(vt) case UnmarshalerObject: - _, err := dec.DecodeObject(vt) + _, err := dec.decodeObject(vt) return err case UnmarshalerArray: - _, err := dec.DecodeArray(vt) + _, err := dec.decodeArray(vt) return err default: return InvalidUnmarshalError(fmt.Sprintf(invalidUnmarshalErrorMsg, reflect.TypeOf(vt).String())) @@ -202,7 +200,7 @@ func (dec *Decoder) Decode(v interface{}) error { // AddInt decodes the next key to an *int. // If next key value overflows int, an InvalidTypeError error will be returned. func (dec *Decoder) AddInt(v *int) error { - err := dec.DecodeInt(v) + err := dec.decodeInt(v) if err != nil { return err } @@ -213,7 +211,7 @@ func (dec *Decoder) AddInt(v *int) error { // AddFloat decodes the next key to a *float64. // If next key value overflows float64, an InvalidTypeError error will be returned. func (dec *Decoder) AddFloat(v *float64) error { - err := dec.DecodeFloat64(v) + err := dec.decodeFloat64(v) if err != nil { return err } @@ -225,7 +223,7 @@ func (dec *Decoder) AddFloat(v *float64) error { // If next key is neither null nor a JSON boolean, an InvalidTypeError will be returned. // If next key is null, bool will be false. func (dec *Decoder) AddBool(v *bool) error { - err := dec.DecodeBool(v) + err := dec.decodeBool(v) if err != nil { return err } @@ -236,7 +234,7 @@ func (dec *Decoder) AddBool(v *bool) error { // AddString decodes the next key to a *string. // If next key is not a JSON string nor null, InvalidTypeError will be returned. func (dec *Decoder) AddString(v *string) error { - err := dec.DecodeString(v) + err := dec.decodeString(v) if err != nil { return err } @@ -251,7 +249,7 @@ func (dec *Decoder) AddObject(value UnmarshalerObject) error { dec.keysDone = 0 dec.called = 0 dec.child |= 1 - newCursor, err := dec.DecodeObject(value) + newCursor, err := dec.decodeObject(value) if err != nil { return err } @@ -264,7 +262,7 @@ func (dec *Decoder) AddObject(value UnmarshalerObject) error { // AddArray decodes the next key to a UnmarshalerArray. func (dec *Decoder) AddArray(value UnmarshalerArray) error { - newCursor, err := dec.DecodeArray(value) + newCursor, err := dec.decodeArray(value) if err != nil { return err } @@ -286,9 +284,18 @@ func isDigit(b byte) bool { func (dec *Decoder) read() bool { if dec.r != nil { - // idea is to append data from reader at the end + // if we reach the end, double the buffer to ensure there's always more space + if len(dec.data) == dec.length { + nLen := dec.length * 2 + Buf := make([]byte, nLen, nLen) + copy(Buf, dec.data) + dec.data = Buf + } n, err := dec.r.Read(dec.data[dec.length:]) - if err != nil || n == 0 { + if err != nil { + dec.err = err + return false + } else if n == 0 { return false } dec.length = dec.length + n diff --git a/decode_array.go b/decode_array.go @@ -9,7 +9,14 @@ import ( // v must implement UnmarshalerArray. // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. -func (dec *Decoder) DecodeArray(arr UnmarshalerArray) (int, error) { +func (dec *Decoder) DecodeArray(arr UnmarshalerArray) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + _, err := dec.decodeArray(arr) + return err +} +func (dec *Decoder) decodeArray(arr UnmarshalerArray) (int, error) { // not an array not an error, but do not know what to do // do not check syntax for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { diff --git a/decode_array_test.go b/decode_array_test.go @@ -1,6 +1,7 @@ package gojay import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -169,3 +170,22 @@ func TestDecoderSliceInvalidJSON(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 TestDecoderSliceDecoderAPI(t *testing.T) { + json := `["string","string1"]` + testArr := testSliceStrings{} + dec := NewDecoder(strings.NewReader(json)) + err := dec.DecodeArray(&testArr) + assert.Nil(t, err, "Err must be nil") + assert.Len(t, testArr, 2, "testArr should be of len 2") + assert.Equal(t, "string", testArr[0], "testArr[0] should be 'string'") + assert.Equal(t, "string1", testArr[1], "testArr[1] should be 'string1'") +} + +func TestDecoderSliceDecoderAPIError(t *testing.T) { + testArr := testSliceInts{} + dec := NewDecoder(strings.NewReader(`hello`)) + err := dec.DecodeArray(&testArr) + assert.NotNil(t, err, "Err must not be nil as JSON is invalid") + assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") +} diff --git a/decode_bool.go b/decode_bool.go @@ -6,6 +6,12 @@ import "fmt" // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. func (dec *Decoder) DecodeBool(v *bool) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeBool(v) +} +func (dec *Decoder) decodeBool(v *bool) error { for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { switch dec.data[dec.cursor] { case ' ', '\n', '\t', '\r', ',': 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.go b/decode_number.go @@ -57,6 +57,12 @@ func init() { // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. func (dec *Decoder) DecodeInt(v *int) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeInt(v) +} +func (dec *Decoder) decodeInt(v *int) error { for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { switch c := dec.data[dec.cursor]; c { case ' ', '\n', '\t', '\r', ',': @@ -101,6 +107,12 @@ func (dec *Decoder) DecodeInt(v *int) error { // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. func (dec *Decoder) DecodeInt32(v *int32) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeInt32(v) +} +func (dec *Decoder) decodeInt32(v *int32) error { for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { switch c := dec.data[dec.cursor]; c { case ' ', '\n', '\t', '\r', ',': @@ -145,6 +157,13 @@ func (dec *Decoder) DecodeInt32(v *int32) error { // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. func (dec *Decoder) DecodeUint32(v *uint32) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeUint32(v) +} + +func (dec *Decoder) decodeUint32(v *uint32) error { for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { switch c := dec.data[dec.cursor]; c { case ' ', '\n', '\t', '\r', ',': @@ -190,6 +209,13 @@ func (dec *Decoder) DecodeUint32(v *uint32) error { // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. func (dec *Decoder) DecodeInt64(v *int64) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeInt64(v) +} + +func (dec *Decoder) decodeInt64(v *int64) error { for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { switch c := dec.data[dec.cursor]; c { case ' ', '\n', '\t', '\r', ',': @@ -234,6 +260,12 @@ func (dec *Decoder) DecodeInt64(v *int64) error { // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. func (dec *Decoder) DecodeUint64(v *uint64) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeUint64(v) +} +func (dec *Decoder) decodeUint64(v *uint64) error { for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { switch c := dec.data[dec.cursor]; c { case ' ', '\n', '\t', '\r', ',': @@ -279,6 +311,12 @@ func (dec *Decoder) DecodeUint64(v *uint64) error { // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. func (dec *Decoder) DecodeFloat64(v *float64) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeFloat64(v) +} +func (dec *Decoder) decodeFloat64(v *float64) error { for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { switch c := dec.data[dec.cursor]; c { case ' ', '\n', '\t', '\r', ',': 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" @@ -55,6 +56,18 @@ func TestDecoderIntOverfow(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 TestDecoderIntPoolError(t *testing.T) { + result := int(1) + 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.DecodeInt(&result) + assert.True(t, false, "should not be called as decoder should have panicked") +} func TestDecoderIntOverfow2(t *testing.T) { json := []byte(`92233720368547758089`) var v int @@ -62,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`) @@ -119,6 +140,26 @@ func TestDecoderInt32Overflow2(t *testing.T) { assert.NotNil(t, err, "err must not be nil as int32 overflows") assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") } +func TestDecoderInt32PoolError(t *testing.T) { + result := int32(1) + 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.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`) @@ -170,6 +211,26 @@ func TestDecoderUint32Overflow2(t *testing.T) { assert.NotNil(t, err, "err must not be nil as uint32 overflows") assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") } +func TestDecoderUint32PoolError(t *testing.T) { + result := uint32(1) + 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.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`) @@ -227,6 +288,26 @@ func TestDecoderInt64Overflow2(t *testing.T) { assert.NotNil(t, err, "err must not be nil as int64 overflows") assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") } +func TestDecoderInt64PoolError(t *testing.T) { + result := int64(1) + 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.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 @@ -276,7 +357,26 @@ func TestDecoderUint64Overflow2(t *testing.T) { assert.NotNil(t, err, "err must not be nil as int32 overflows") assert.IsType(t, InvalidTypeError(""), err, "err must be of type InvalidTypeError") } - +func TestDecoderUint64PoolError(t *testing.T) { + result := uint64(1) + 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.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 @@ -308,3 +408,23 @@ 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) + 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.DecodeFloat64(&result) + assert.True(t, false, "should not be called as decoder should have panicked") +} diff --git a/decode_object.go b/decode_object.go @@ -10,7 +10,17 @@ import ( // v must implement UnmarshalerObject. // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. -func (dec *Decoder) DecodeObject(j UnmarshalerObject) (int, error) { +func (dec *Decoder) DecodeObject(j UnmarshalerObject) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + _, err := dec.decodeObject(j) + return err +} +func (dec *Decoder) decodeObject(j UnmarshalerObject) (int, error) { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } keys := j.NKeys() for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { switch dec.data[dec.cursor] { diff --git a/decode_object_test.go b/decode_object_test.go @@ -1,6 +1,7 @@ package gojay import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -75,6 +76,27 @@ func (t *TestObj) NKeys() int { return 8 } +func assertResult(t *testing.T, v *TestObj, err error) { + assert.Nil(t, err, "Err must be nil") + assert.Equal(t, 245, v.test, "v.test must be equal to 245") + assert.Equal(t, 246, v.test2, "v.test2 must be equal to 246") + assert.Equal(t, "string", v.test3, "v.test3 must be equal to 'string'") + assert.Equal(t, "complex string with spaces and some slashes\"", v.test4, "v.test4 must be equal to 'string'") + assert.Equal(t, -1.15657654376543, v.test5, "v.test5 must be equal to 1.15") + assert.Len(t, v.testArr, 2, "v.testArr must be of len 2") + + assert.Equal(t, 121, v.testSubObj.test3, "v.testSubObj.test3 must be equal to 121") + assert.Equal(t, 122, v.testSubObj.test4, "v.testSubObj.test4 must be equal to 122") + assert.Equal(t, "string", v.testSubObj.test5, "v.testSubObj.test5 must be equal to 'string'") + assert.Equal(t, 150, v.testSubObj.testSubSubObj.test3, "v.testSubObj.testSubSubObj.test3 must be equal to 150") + assert.Equal(t, 150, v.testSubObj.testSubSubObj2.test3, "v.testSubObj.testSubSubObj2.test3 must be equal to 150") + + assert.Equal(t, 122, v.testSubObj2.test3, "v.testSubObj2.test3 must be equal to 121") + assert.Equal(t, 123, v.testSubObj2.test4, "v.testSubObj2.test4 must be equal to 122") + assert.Equal(t, "string", v.testSubObj2.test5, "v.testSubObj2.test5 must be equal to 'string'") + assert.Equal(t, 151, v.testSubObj2.testSubSubObj.test3, "v.testSubObj2.testSubSubObj.test must be equal to 150") +} + func TestDecoderObject(t *testing.T) { json := []byte(`{ "test": 245, @@ -118,24 +140,7 @@ func TestDecoderObject(t *testing.T) { }`) v := &TestObj{} err := Unmarshal(json, v) - assert.Nil(t, err, "Err must be nil") - assert.Equal(t, 245, v.test, "v.test must be equal to 245") - assert.Equal(t, 246, v.test2, "v.test2 must be equal to 246") - assert.Equal(t, "string", v.test3, "v.test3 must be equal to 'string'") - assert.Equal(t, "complex string with spaces and some slashes\"", v.test4, "v.test4 must be equal to 'string'") - assert.Equal(t, -1.15657654376543, v.test5, "v.test5 must be equal to 1.15") - assert.Len(t, v.testArr, 2, "v.testArr must be of len 2") - - assert.Equal(t, 121, v.testSubObj.test3, "v.testSubObj.test3 must be equal to 121") - assert.Equal(t, 122, v.testSubObj.test4, "v.testSubObj.test4 must be equal to 122") - assert.Equal(t, "string", v.testSubObj.test5, "v.testSubObj.test5 must be equal to 'string'") - assert.Equal(t, 150, v.testSubObj.testSubSubObj.test3, "v.testSubObj.testSubSubObj.test3 must be equal to 150") - assert.Equal(t, 150, v.testSubObj.testSubSubObj2.test3, "v.testSubObj.testSubSubObj2.test3 must be equal to 150") - - assert.Equal(t, 122, v.testSubObj2.test3, "v.testSubObj2.test3 must be equal to 121") - assert.Equal(t, 123, v.testSubObj2.test4, "v.testSubObj2.test4 must be equal to 122") - assert.Equal(t, "string", v.testSubObj2.test5, "v.testSubObj2.test5 must be equal to 'string'") - assert.Equal(t, 151, v.testSubObj2.testSubSubObj.test3, "v.testSubObj2.testSubSubObj.test must be equal to 150") + assertResult(t, v, err) } func TestDecodeObjectNull(t *testing.T) { @@ -245,7 +250,7 @@ func TestDecodeObjectPartial(t *testing.T) { "testSkipNumber": 123.23 }`) dec.length = len(dec.data) - _, err := dec.DecodeObject(&result) + err := dec.DecodeObject(&result) assert.Nil(t, err, "err should be nil") assert.NotEqual(t, len(dec.data), dec.cursor) } @@ -259,7 +264,75 @@ func TestDecoderObjectInvalidJSON(t *testing.T) { "testSkipString": "testInvalidJSON\\\\ }`) dec.length = len(dec.data) - _, err := dec.DecodeObject(&result) + err := dec.DecodeObject(&result) + 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 TestDecoderObjectPoolError(t *testing.T) { + result := jsonDecodePartial{} + 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.DecodeObject(&result) + assert.True(t, false, "should not be called as decoder should have panicked") +} + +func TestDecoderObjectDecoderAPI(t *testing.T) { + json := `{ + "test": 245, + "test2": 246, + "test3": "string", + "test4": "complex string with spaces and some slashes\"", + "test5": -1.15657654376543, + "testNull": null, + "testArr": [ + { + "test": 245, + "test2": 246 + }, + { + "test": 245, + "test2": 246 + } + ], + "testSubObj": { + "test": 121, + "test2": 122, + "testNull": null, + "testSubSubObj": { + "test": 150, + "testNull": null + }, + "testSubSubObj2": { + "test": 150 + }, + "test3": "string" + "testNull": null, + }, + "testSubObj2": { + "test": 122, + "test3": "string" + "testSubSubObj": { + "test": 151 + }, + "test2": 123 + } + }` + v := &TestObj{} + dec := NewDecoder(strings.NewReader(json)) + err := dec.DecodeObject(v) + assertResult(t, v, err) +} + +func TestDecoderObjectDecoderAPIError(t *testing.T) { + testArr := testSliceInts{} + dec := NewDecoder(strings.NewReader(`hello`)) + err := dec.DecodeArray(&testArr) assert.NotNil(t, err, "Err must not be nil as JSON is invalid") assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") } diff --git a/decode_pool.go b/decode_pool.go @@ -4,13 +4,25 @@ import "io" var decPool = make(chan *Decoder, 16) -// NewDecoder returns a new decoder or borrows one from the pool -// it takes an io.Reader implementation as data input +// NewDecoder returns a new decoder. +// It takes an io.Reader implementation as data input. func NewDecoder(r io.Reader) *Decoder { - return newDecoder(r, 512) + return &Decoder{ + called: 0, + cursor: 0, + keysDone: 0, + err: nil, + r: r, + data: make([]byte, 512), + length: 0, + isPooled: 0, + } } -func newDecoder(r io.Reader, bufSize int) *Decoder { +// 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 { select { case dec := <-decPool: dec.called = 0 @@ -19,6 +31,7 @@ func newDecoder(r io.Reader, bufSize int) *Decoder { dec.err = nil dec.r = r dec.length = 0 + dec.isPooled = 0 if bufSize > 0 { dec.data = make([]byte, bufSize) } @@ -30,16 +43,20 @@ func newDecoder(r io.Reader, bufSize int) *Decoder { keysDone: 0, err: nil, r: r, + isPooled: 0, } if bufSize > 0 { dec.data = make([]byte, bufSize) - dec.length = 0 } return dec } } -func (dec *Decoder) addToPool() { +// Release sends back a Decoder to the pool. +// If a decoder is used after calling Release +// a panic will be raised with an InvalidUsagePooledDecoderError error. +func (dec *Decoder) Release() { + dec.isPooled = 1 select { case decPool <- dec: default: diff --git a/decode_stream.go b/decode_stream.go @@ -1,10 +1,15 @@ package gojay import ( - "io" "time" ) +// UnmarshalerStream is the interface to implement for a slice, an array or a slice +// to decode a line delimited JSON to. +type UnmarshalerStream interface { + UnmarshalStream(*StreamDecoder) error +} + // Stream is a struct holding the Stream api var Stream = stream{} @@ -19,24 +24,15 @@ type StreamDecoder struct { deadline *time.Time } -// NewDecoder returns a new decoder or borrows one from the pool. -// It takes an io.Reader implementation as data input. -// It initiates the done channel returned by Done(). -func (s stream) NewDecoder(r io.Reader) *StreamDecoder { - dec := newDecoder(r, 512) - streamDec := &StreamDecoder{ - Decoder: dec, - done: make(chan struct{}, 1), - } - return streamDec -} - // DecodeStream reads the next line delimited JSON-encoded value from its input and stores it in the value pointed to by c. // // c must implement UnmarshalerStream. Ideally c is a channel. See example for implementation. // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. func (dec *StreamDecoder) DecodeStream(c UnmarshalerStream) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } if dec.r == nil { dec.err = NoReaderError("No reader given to decode stream") close(dec.done) diff --git a/decode_stream_pool.go b/decode_stream_pool.go @@ -0,0 +1,49 @@ +package gojay + +import "io" + +var streamDecPool = make(chan *StreamDecoder, 16) + +// NewDecoder returns a new decoder. +// It takes an io.Reader implementation as data input. +// It initiates the done channel returned by Done(). +func (s stream) NewDecoder(r io.Reader) *StreamDecoder { + dec := NewDecoder(r) + streamDec := &StreamDecoder{ + Decoder: dec, + done: make(chan struct{}, 1), + } + return streamDec +} + +// BorrowDecoder borrows a StreamDecoder a decoder from the pool. +// It takes an io.Reader implementation as data input. +// It initiates the done channel returned by Done(). +func (s stream) BorrowDecoder(r io.Reader, bufSize int) *StreamDecoder { + select { + case streamDec := <-streamDecPool: + streamDec.called = 0 + streamDec.keysDone = 0 + streamDec.cursor = 0 + streamDec.err = nil + streamDec.r = r + streamDec.length = 0 + streamDec.isPooled = 0 + streamDec.done = make(chan struct{}, 1) + if bufSize > 0 { + streamDec.data = make([]byte, bufSize) + } + return streamDec + default: + dec := NewDecoder(r) + if bufSize > 0 { + dec.data = make([]byte, bufSize) + dec.length = 0 + } + streamDec := &StreamDecoder{ + Decoder: dec, + done: make(chan struct{}, 1), + } + return streamDec + } +} diff --git a/decode_stream_pool_test.go b/decode_stream_pool_test.go @@ -0,0 +1,100 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeStreamBorrow(t *testing.T) { + // we override the pool chan + streamDecPool = make(chan *StreamDecoder, 1) + // add one decoder to the channel + dec := Stream.NewDecoder(nil) + streamDecPool <- dec + // borrow one decoder to the channel + nDec := Stream.BorrowDecoder(nil, 0) + // make sure they are the same + assert.Equal(t, dec, nDec, "decoder added to the pool and new decoder should be the same") +} + +func TestDecodeStreamBorrow1(t *testing.T) { + // we override the pool chan + streamDecPool = make(chan *StreamDecoder, 1) + // add one decoder to the channel + dec := Stream.NewDecoder(nil) + streamDecPool <- dec + // reset streamDecPool + streamDecPool = make(chan *StreamDecoder, 1) + // borrow one decoder to the channel + nDec := Stream.BorrowDecoder(nil, 0) + // make sure they are the same + assert.NotEqual(t, dec, nDec, "decoder added to the pool and new decoder should be the same") +} +func TestDecodeStreamBorrow2(t *testing.T) { + // we override the pool chan + streamDecPool = make(chan *StreamDecoder, 1) + // add one decoder to the channel + dec := Stream.NewDecoder(nil) + dec.data = make([]byte, 128) + streamDecPool <- dec + // borrow one decoder to the channel + nDec := Stream.BorrowDecoder(nil, 512) + // make sure they are the same + assert.Equal(t, dec, nDec, "decoder added to the pool and new decoder should be the same") + assert.Equal(t, 512, len(nDec.data), "len of dec.data should be 512") +} +func TestDecodeStreamBorrow3(t *testing.T) { + // we override the pool chan + streamDecPool = make(chan *StreamDecoder, 16) + // borrow one decoder to the channel + nDec := Stream.BorrowDecoder(nil, 512) + // make sure they are the same + assert.Equal(t, 512, len(nDec.data), "len of dec.data should be 512") +} + +func TestDecodeStreamDecodePooledDecoderError(t *testing.T) { + // we override the pool chan + dec := Stream.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") + }() + var v = 0 + dec.Decode(&v) + // make sure it fails if this is called + assert.True(t, false, "should not be called as decoder should have panicked") +} + +func TestDecodeStreamDecodePooledDecoderError1(t *testing.T) { + // we override the pool chan + dec := Stream.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") + }() + var v = testSliceStrings{} + dec.DecodeArray(&v) + // make sure they are the same + assert.True(t, false, "should not be called as decoder should have panicked") +} + +func TestDecodeStreamDecodePooledDecoderError2(t *testing.T) { + // we override the pool chan + dec := Stream.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") + assert.Equal(t, "Invalid usage of pooled decoder", err.(InvalidUsagePooledDecoderError).Error(), "err should be of type InvalidUsagePooledDecoderError") + }() + var v = TestObj{} + dec.DecodeObject(&v) + // make sure they are the same + assert.True(t, false, "should not be called as decoder should have panicked") +} 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.go b/decode_string.go @@ -9,6 +9,12 @@ import ( // // See the documentation for Unmarshal for details about the conversion of JSON into a Go value. func (dec *Decoder) DecodeString(v *string) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeString(v) +} +func (dec *Decoder) decodeString(v *string) error { for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { switch dec.data[dec.cursor] { case ' ', '\n', '\t', '\r', ',': 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/decode_unsafe.go b/decode_unsafe.go @@ -14,11 +14,11 @@ var Unsafe = decUnsafe{} type decUnsafe struct{} func (u decUnsafe) UnmarshalArray(data []byte, v UnmarshalerArray) error { - dec := newDecoder(nil, 0) + dec := BorrowDecoder(nil, 0) + defer dec.Release() dec.data = data dec.length = len(data) - _, err := dec.DecodeArray(v) - dec.addToPool() + _, err := dec.decodeArray(v) if err != nil { return err } @@ -29,11 +29,11 @@ func (u decUnsafe) UnmarshalArray(data []byte, v UnmarshalerArray) error { } func (u decUnsafe) UnmarshalObject(data []byte, v UnmarshalerObject) error { - dec := newDecoder(nil, 0) + dec := BorrowDecoder(nil, 0) + defer dec.Release() dec.data = data dec.length = len(data) - _, err := dec.DecodeObject(v) - dec.addToPool() + _, err := dec.decodeObject(v) if err != nil { return err } @@ -48,59 +48,59 @@ func (u decUnsafe) Unmarshal(data []byte, v interface{}) error { var dec *Decoder switch vt := v.(type) { case *string: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeString(vt) + err = dec.decodeString(vt) case *int: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeInt(vt) + err = dec.decodeInt(vt) case *int32: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeInt32(vt) + err = dec.decodeInt32(vt) case *uint32: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeUint32(vt) + err = dec.decodeUint32(vt) case *int64: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeInt64(vt) + err = dec.decodeInt64(vt) case *uint64: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeUint64(vt) + err = dec.decodeUint64(vt) case *float64: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeFloat64(vt) + err = dec.decodeFloat64(vt) case *bool: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - err = dec.DecodeBool(vt) + err = dec.decodeBool(vt) case UnmarshalerObject: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - _, err = dec.DecodeObject(vt) + _, err = dec.decodeObject(vt) case UnmarshalerArray: - dec = newDecoder(nil, 0) + dec = BorrowDecoder(nil, 0) dec.length = len(data) dec.data = data - _, err = dec.DecodeArray(vt) + _, err = dec.decodeArray(vt) default: return InvalidUnmarshalError(fmt.Sprintf(invalidUnmarshalErrorMsg, reflect.TypeOf(vt).String())) } - defer dec.addToPool() + defer dec.Release() if err != nil { return err } 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.addToPool() - return enc.buf, nil + defer enc.Release() + return enc.encodeObject(v) } // MarshalArray returns the JSON encoding of v. @@ -59,7 +55,7 @@ func MarshalArray(v MarshalerArray) ([]byte, error) { enc.writeByte('[') v.(MarshalerArray).MarshalArray(enc) enc.writeByte(']') - defer enc.addToPool() + defer enc.Release() return enc.buf, nil } @@ -98,73 +94,73 @@ 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('}') b = enc.buf - defer enc.addToPool() + defer enc.Release() return b, nil case MarshalerArray: - enc := NewEncoder() + enc := BorrowEncoder() enc.writeByte('[') vt.MarshalArray(enc) enc.writeByte(']') b = enc.buf - defer enc.addToPool() + defer enc.Release() return b, nil case string: - enc := NewEncoder() + enc := BorrowEncoder() b, err = enc.encodeString(vt) - defer enc.addToPool() + defer enc.Release() case bool: - enc := NewEncoder() + enc := BorrowEncoder() err = enc.AddBool(vt) b = enc.buf - defer enc.addToPool() + defer enc.Release() case int: - enc := NewEncoder() + enc := BorrowEncoder() b, err = enc.encodeInt(int64(vt)) - defer enc.addToPool() + defer enc.Release() case int64: - enc := NewEncoder() - defer enc.addToPool() + enc := BorrowEncoder() + defer enc.Release() return enc.encodeInt(vt) case int32: - enc := NewEncoder() - defer enc.addToPool() + enc := BorrowEncoder() + defer enc.Release() return enc.encodeInt(int64(vt)) case int16: - enc := NewEncoder() - defer enc.addToPool() + enc := BorrowEncoder() + defer enc.Release() return enc.encodeInt(int64(vt)) case int8: - enc := NewEncoder() - defer enc.addToPool() + enc := BorrowEncoder() + defer enc.Release() return enc.encodeInt(int64(vt)) case uint64: - enc := NewEncoder() - defer enc.addToPool() + enc := BorrowEncoder() + defer enc.Release() return enc.encodeInt(int64(vt)) case uint32: - enc := NewEncoder() - defer enc.addToPool() + enc := BorrowEncoder() + defer enc.Release() return enc.encodeInt(int64(vt)) case uint16: - enc := NewEncoder() - defer enc.addToPool() + enc := BorrowEncoder() + defer enc.Release() return enc.encodeInt(int64(vt)) case uint8: - enc := NewEncoder() + enc := BorrowEncoder() b, err = enc.encodeInt(int64(vt)) - defer enc.addToPool() + defer enc.Release() case float64: - enc := NewEncoder() - defer enc.addToPool() + enc := BorrowEncoder() + defer enc.Release() return enc.encodeFloat(vt) case float32: - enc := NewEncoder() - defer enc.addToPool() + enc := BorrowEncoder() + defer enc.Release() return enc.encodeFloat(float64(vt)) } return b, err @@ -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{} } } -func (enc *Encoder) addToPool() { +// 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 @@ -33,3 +43,19 @@ type NoReaderError string func (err NoReaderError) Error() string { return string(err) } + +// InvalidUsagePooledDecoderError is a type representing an error returned +// when decoding is called on a still pooled Decoder +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) +}