gojay

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

commit 0f58d9e9e06dda76b9dddfb16d1a4ab409e422b8
parent a3885eabb90cc233617149549ee16c62b884144b
Author: Francois Parquet <francois.parquet@gmail.com>
Date:   Tue,  1 May 2018 23:18:04 +0800

Merge pull request #13 from francoispqt/version/v0.10.0

Version/v0.10.0 - Add io.Writer support to Encoder - Add Stream API for Encoding - Clean and add tests 
Diffstat:
MMakefile | 4++--
MREADME.md | 309++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mbenchmarks/benchmarks_large.go | 6++++++
Mbenchmarks/benchmarks_medium.go | 3+++
Mdecode.go | 10+++++-----
Mdecode_array.go | 2+-
Mdecode_array_test.go | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mdecode_bool_test.go | 2+-
Adecode_pool_test.go | 26++++++++++++++++++++++++++
Mdecode_stream_pool.go | 17+++++++++++++++--
Mdecode_stream_test.go | 2+-
Mdecode_string_test.go | 2++
Mdecode_test.go | 63++++++++++++++-------------------------------------------------
Mencode.go | 89+++++++++++++++++++++++++++++++++++++------------------------------------------
Mencode_array.go | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mencode_array_test.go | 405++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mencode_bool.go | 50+++++++++++++++++++++++++++++++++++++++++---------
Mencode_bool_test.go | 74+++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mencode_builder.go | 2+-
Mencode_builder_test.go | 6+++---
Mencode_interface.go | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mencode_interface_test.go | 113+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mencode_number.go | 182++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mencode_number_test.go | 330+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mencode_object.go | 84++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mencode_object_test.go | 470+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mencode_pool.go | 22++++++++++++++--------
Mencode_pool_test.go | 6+++---
Aencode_stream.go | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_stream_pool.go | 43+++++++++++++++++++++++++++++++++++++++++++
Aencode_stream_pool_test.go | 21+++++++++++++++++++++
Aencode_stream_test.go | 329+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mencode_string.go | 57+++++++++++++++++++++++++++++++++++++++++++++------------
Mencode_string_test.go | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mencode_test.go | 8++++++++
35 files changed, 2533 insertions(+), 772 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,10 +1,10 @@ .PHONY: test test: - go test -run=^Test -v + go test -race -run=^Test -v .PHONY: cover cover: - go test -coverprofile=coverage.out + go test -coverprofile=coverage.out -covermode=atomic .PHONY: coverhtml coverhtml: diff --git a/README.md b/README.md @@ -6,7 +6,7 @@ ![MIT License](https://img.shields.io/badge/license-mit-blue.svg?style=flat-square) # GoJay -**Package is currently at version 0.9.1 and still under development** +**Package is currently at version 0.10.0 and still under development** GoJay is a performant JSON encoder/decoder for Golang (currently the most performant, [see benchmarks](#benchmark-results)). @@ -28,6 +28,11 @@ This is how GoJay aims to be a very fast, JIT stream parser with 0 reflection, l go get github.com/francoispqt/gojay ``` +* [Encoder](#encoding) +* [Decoder](#decoding) +* [Stream API](#stream-api) + + ## Decoding Decoding is done through two different API similar to standard `encoding/json`: @@ -139,7 +144,7 @@ if err := dec.Decode(&str); err != nil { `*gojay.Decoder` has multiple methods to decode to specific types: * Decode ```go -func (dec *Decoder) DecodeInt(v *int) error +func (dec *Decoder) Decode(v interface{}) error ``` * DecodeObject ```go @@ -240,45 +245,6 @@ func (c ChannelArray) UnmarshalArray(dec *gojay.Decoder) error { } ``` -### Stream Decoding -GoJay ships with a powerful stream decoder. - -It allows to read continuously from an io.Reader stream and do JIT decoding writing unmarshalled JSON to a channel to allow async consuming. - -When using the Stream API, the Decoder implements context.Context to provide graceful cancellation. - -Example: -```go -type ChannelStream chan *TestObj -// implement UnmarshalerStream -func (c ChannelStream) UnmarshalStream(dec *gojay.StreamDecoder) error { - obj := &TestObj{} - if err := dec.AddObject(obj); err != nil { - return err - } - c <- obj - return nil -} - -func main() { - // create our channel which will receive our objects - streamChan := ChannelStream(make(chan *TestObj)) - // get a reader implementing io.Reader - reader := getAnIOReaderStream() - dec := gojay.Stream.NewDecoder(reader) - // start decoding (will block the goroutine until something is written to the ReadWriter) - go dec.DecodeStream(streamChan) - for { - select { - case v := <-streamChan: - // do something with my TestObj - case <-dec.Done(): - os.Exit("finished reading stream") - } - } -} -``` - ### Other types To decode other types (string, int, int32, int64, uint32, uint64, float, booleans), you don't need to implement any interface. @@ -297,7 +263,11 @@ func main() { ## Encoding -Example of basic structure encoding: +Encoding is done through two different API similar to standard `encoding/json`: +* [Marshal](#marshal-api) +* [Encode](#encode-api) + +Example of basic structure encoding with Marshal: ```go import "github.com/francoispqt/gojay" @@ -318,11 +288,115 @@ func (u *user) IsNil() bool { func main() { u := &user{1, "gojay", "gojay@email.com"} - b, _ := gojay.MarshalObject(u) + b, err := gojay.MarshalObject(u) + if err != nil { + log.Fatal(err) + } fmt.Println(string(b)) // {"id":1,"name":"gojay","email":"gojay@email.com"} } ``` +with Encode: +```go +func main() { + u := &user{1, "gojay", "gojay@email.com"} + b := strings.Builder{} + if err := gojay.NewEncoder(&b); err != nil { + log.Fatal(err) + } + fmt.Println(b.String()) // {"id":1,"name":"gojay","email":"gojay@email.com"} +} +``` + +### Marshal API + +Marshal API encodes a value to a JSON `[]byte` with a single function. + +Behind the doors, Marshal API borrows a `*gojay.Encoder` resets its settings and encodes the data to an internal byte buffer and releases the `*gojay.Encoder` to the pool when it finishes, whether it encounters an error or not. + +If it cannot find the right Encoding strategy for the type of the given value, it returns an `InvalidMarshalError`. You can test the error returned by doing `if ok := err.(InvalidMarshalError); ok {}`. + +Marshal API comes with three functions: +* Marshal +```go +func Marshal(v interface{}) ([]byte, error) +``` + +* MarshalObject +```go +func MarshalObject(v MarshalerObject) ([]byte, error) +``` + +* MarshalArray +```go +func MarshalArray(v MarshalerArray) ([]byte, error) +``` + +### Encode API + +Encode API decodes a value to JSON by creating or borrowing a `*gojay.Encoder` sending it to an `io.Writer` and calling `Encode` methods. + +__Getting a *gojay.Encoder or Borrowing__ + +You can either get a fresh `*gojay.Encoder` calling `enc := gojay.NewEncoder(io.Writer)` or borrow one from the pool by calling `enc := gojay.BorrowEncoder(io.Writer)`. + +After using an encoder, you can release it by calling `enc.Release()`. Beware, if you reuse the encoder after releasing it, it will panic with an error of type `InvalidUsagePooledEncoderError`. If you want to fully benefit from the pooling, you must release your encoders after using. + +Example getting a fresh encoder an releasing: +```go +str := "test" +b := strings.Builder{} +enc := gojay.NewEncoder(&b) +defer enc.Release() +if err := enc.Encode(str); err != nil { + log.Fatal(err) +} +``` +Example borrowing an encoder and releasing: +```go +str := "test" +b := strings.Builder{} +enc := gojay.BorrowEncoder(b) +defer enc.Release() +if err := enc.Encode(str); err != nil { + log.Fatal(err) +} +``` + +`*gojay.Encoder` has multiple methods to encoder specific types to JSON: +* Encode +```go +func (enc *Encoder) Encode(v interface{}) error +``` +* EncodeObject +```go +func (enc *Encoder) EncodeObject(v MarshalerObject) error +``` +* EncodeArray +```go +func (enc *Encoder) EncodeArray(v MarshalerArray) error +``` +* EncodeInt +```go +func (enc *Encoder) EncodeInt(n int) error +``` +* EncodeInt64 +```go +func (enc *Encoder) EncodeInt64(n int64) error +``` +* EncodeFloat +```go +func (enc *Encoder) EncodeFloat(n float64) error +``` +* EncodeBool +```go +func (enc *Encoder) EncodeBool(v bool) error +``` +* EncodeString +```go +func (enc *Encoder) EncodeString(s string) error +``` + ### Structs To encode a structure, the structure must implement the MarshalerObject interface: @@ -358,23 +432,25 @@ func (u *user) IsNil() bool { To encode an array or a slice, the slice/array must implement the MarshalerArray interface: ```go type MarshalerArray interface { - MarshalArray(enc *Encoder) + MarshalArray(enc *Encoder) + IsNil() bool } ``` MarshalArray method takes one argument, a pointer to the Encoder (*gojay.Encoder). The method must add all element in the JSON Array by calling Decoder's methods. +IsNil method returns a boolean indicating if the interface underlying value is nil(empty) or not. It is used to safely ensure that the underlying value is not nil without using Reflection and also to in `OmitEmpty` feature. + Example of implementation: ```go type users []*user // implement MarshalerArray -func (u *users) MarshalArray(dec *Decoder) error { +func (u *users) MarshalArray(dec *Decoder) { for _, e := range u { - err := enc.AddObject(e) - if err != nil { - return err - } + enc.AddObject(e) } - return nil +} +func (u *users) IsNil() bool { + return len(u) == 0 } ``` @@ -393,6 +469,139 @@ func main() { } ``` +# Stream API + +### Stream Decoding +GoJay ships with a powerful stream decoder. + +It allows to read continuously from an io.Reader stream and do JIT decoding writing unmarshalled JSON to a channel to allow async consuming. + +When using the Stream API, the Decoder implements context.Context to provide graceful cancellation. + +To decode a stream of JSON, you must call `gojay.Stream.DecodeStream` and pass it a `UnmarshalerStream` implementation. + +```go +type UnmarshalerStream interface { + UnmarshalStream(*StreamDecoder) error +} +``` + +Example of implementation of stream reading from a WebSocket connection: +```go +// implement UnmarshalerStream +type ChannelStream chan *user + +func (c ChannelStream) UnmarshalStream(dec *gojay.StreamDecoder) error { + u := &user{} + if err := dec.AddObject(u); err != nil { + return err + } + c <- u + return nil +} + +func main() { + // get our websocket connection + origin := "http://localhost/" + url := "ws://localhost:12345/ws" + ws, err := websocket.Dial(url, "", origin) + if err != nil { + log.Fatal(err) + } + // create our channel which will receive our objects + streamChan := ChannelStream(make(chan *user)) + // borrow a decoder + dec := gojay.Stream.BorrowDecoder(ws) + // start decoding, it will block until a JSON message is decoded from the WebSocket + // or until Done channel is closed + go dec.DecodeStream(streamChan) + for { + select { + case v := <-streamChan: + // Got something from my websocket! + case <-dec.Done(): + os.Exit("finished reading from WebSocket") + } + } +} +``` + +## Stream Encoding +GoJay ships with a powerful stream encoder part of the Stream API. + +It allows to write continuously to an io.Writer and do JIT encoding of data fed to a channel to allow async consuming. You can set multiple consumers on the channel to be as performant as possible. Consumers are non blocking and are scheduled individually in their own go routine. + +When using the Stream API, the Encoder implements context.Context to provide graceful cancellation. + +To encode a stream of data, you must call `EncodeStream` and pass it a `MarshalerStream` implementation. + +```go +type MarshalerStream interface { + MarshalStream(enc *StreamEncoder) +} +``` + +Example of implementation of stream writing to a WebSocket: +```go +// Our structure which will be pushed to our stream +type user struct { + id int + name string + email string +} + +func (u *user) MarshalObject(enc *gojay.Encoder) { + enc.AddIntKey("id", u.id) + enc.AddStringKey("name", u.name) + enc.AddStringKey("id", u.email) +} +func (u *user) IsNil() bool { + return u == nil +} + +// Our MarshalerStream implementation +type StreamChan chan *user + +func (s StreamChan) MarshalStream(enc *gojay.StreamEncoder) { + select { + case <-enc.Done(): + return + case o := <-s: + enc.AddObject(o) + } +} + +// Our main function +func main() { + // get our websocket connection + origin := "http://localhost/" + url := "ws://localhost:12345/ws" + ws, err := websocket.Dial(url, "", origin) + if err != nil { + log.Fatal(err) + } + // we borrow an encoder set stdout as the writer, + // set the number of consumer to 10 + // and tell the encoder to separate each encoded element + // added to the channel by a new line character + enc := gojay.Stream.BorrowEncoder(ws).NConsumer(10).LineDelimited() + // instantiate our MarshalerStream + s := StreamChan(make(chan *user)) + // start the stream encoder + // will block its goroutine until enc.Cancel(error) is called + // or until something is written to then channel + go enc.EncodeStream(s) + // write to our MarshalerStream + for i := 0; i < 1000; i++ { + s<-&user{i,"username","user@email.com"} + } + // Wait + select { + case <-enc.Done(): + } +} +``` + # 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. diff --git a/benchmarks/benchmarks_large.go b/benchmarks/benchmarks_large.go @@ -65,6 +65,9 @@ func (m *DSTopics) MarshalArray(enc *gojay.Encoder) { enc.AddObject(e) } } +func (m *DSTopics) IsNil() bool { + return m == nil +} type DSTopicsList struct { Topics DSTopics @@ -107,6 +110,9 @@ func (m *DSUsers) MarshalArray(enc *gojay.Encoder) { enc.AddObject(e) } } +func (m *DSUsers) IsNil() bool { + return m == nil +} type LargePayload struct { Users DSUsers diff --git a/benchmarks/benchmarks_medium.go b/benchmarks/benchmarks_medium.go @@ -133,6 +133,9 @@ func (m *Avatars) MarshalArray(enc *gojay.Encoder) { enc.AddObject(e) } } +func (m *Avatars) IsNil() bool { + return m == nil +} type CBGravatar struct { Avatars Avatars diff --git a/decode.go b/decode.go @@ -149,15 +149,15 @@ type UnmarshalerArray interface { // A Decoder reads and decodes JSON values from an input stream. type Decoder struct { + r io.Reader data []byte + err error + isPooled byte + called byte + child byte cursor int length int keysDone int - called byte - 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. diff --git a/decode_array.go b/decode_array.go @@ -109,5 +109,5 @@ func (dec *Decoder) skipArray() (int, error) { continue } } - return 0, nil + return 0, InvalidJSONError("Invalid JSON") } diff --git a/decode_array_test.go b/decode_array_test.go @@ -190,6 +190,57 @@ func TestDecoderSliceDecoderAPIError(t *testing.T) { assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") } +func TestUnmarshalArrays(t *testing.T) { + testCases := []struct { + name string + v UnmarshalerArray + d []byte + expectations func(err error, v interface{}, t *testing.T) + }{ + { + v: new(testDecodeSlice), + d: []byte(`[{"test":"test"}]`), + name: "test decode slice", + expectations: func(err error, v interface{}, t *testing.T) { + vtPtr := v.(*testDecodeSlice) + vt := *vtPtr + assert.Nil(t, err, "err must be nil") + assert.Len(t, vt, 1, "len of vt must be 1") + assert.Equal(t, "test", vt[0].test, "vt[0].test must be equal to 'test'") + }, + }, + { + v: new(testDecodeSlice), + d: []byte(`[{"test":"test"},{"test":"test2"}]`), + name: "test decode slice", + expectations: func(err error, v interface{}, t *testing.T) { + vtPtr := v.(*testDecodeSlice) + vt := *vtPtr + assert.Nil(t, err, "err must be nil") + assert.Len(t, vt, 2, "len of vt must be 2") + assert.Equal(t, "test", vt[0].test, "vt[0].test must be equal to 'test'") + assert.Equal(t, "test2", vt[1].test, "vt[1].test must be equal to 'test2'") + }, + }, + { + v: new(testDecodeSlice), + d: []byte(`invalid json`), + name: "test decode object null", + expectations: func(err error, v interface{}, t *testing.T) { + assert.NotNil(t, err, "err must not be nil") + assert.IsType(t, InvalidJSONError(""), err, "err must be of type InvalidJSONError") + }, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(*testing.T) { + err := UnmarshalArray(testCase.d, testCase.v) + testCase.expectations(err, testCase.v, t) + }) + } +} + func TestSkipArray(t *testing.T) { testCases := []struct { json string diff --git a/decode_bool_test.go b/decode_bool_test.go @@ -48,7 +48,7 @@ func TestDecoderBoolInvalidJSON(t *testing.T) { } func TestDecoderBoolDecoderAPI(t *testing.T) { var v bool - dec := NewDecoder(strings.NewReader("true")) + dec := BorrowDecoder(strings.NewReader("true")) defer dec.Release() err := dec.DecodeBool(&v) assert.Nil(t, err, "Err must be nil") diff --git a/decode_pool_test.go b/decode_pool_test.go @@ -0,0 +1,26 @@ +package gojay + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecoderBorrowFromPool(t *testing.T) { + // reset pool + decPool = make(chan *Decoder, 16) + // borrow decoder + dec := BorrowDecoder(strings.NewReader("")) + // release + dec.Release() + // get from pool + nDec := BorrowDecoder(strings.NewReader("")) + // assert same + assert.Equal(t, dec, nDec, "both decoders should be the same") +} + +func TestDecoderBorrowFromPoolSetBuffSize(t *testing.T) { + dec := borrowDecoder(nil, 512) + assert.Len(t, dec.data, 512, "data buffer should be of len 512") +} diff --git a/decode_stream_pool.go b/decode_stream_pool.go @@ -4,7 +4,7 @@ import "io" var streamDecPool = make(chan *StreamDecoder, 16) -// NewDecoder returns a new decoder. +// NewDecoder returns a new StreamDecoder. // 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 { @@ -16,9 +16,11 @@ func (s stream) NewDecoder(r io.Reader) *StreamDecoder { return streamDec } -// BorrowDecoder borrows a StreamDecoder a decoder from the pool. +// BorrowDecoder borrows a StreamDecoder from the pool. // It takes an io.Reader implementation as data input. // It initiates the done channel returned by Done(). +// +// If no StreamEncoder is available in the pool, it returns a fresh one func (s stream) BorrowDecoder(r io.Reader) *StreamDecoder { return s.borrowDecoder(r, 512) } @@ -51,3 +53,14 @@ func (s stream) borrowDecoder(r io.Reader, bufSize int) *StreamDecoder { return streamDec } } + +// 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 *StreamDecoder) Release() { + dec.isPooled = 1 + select { + case streamDecPool <- dec: + default: + } +} diff --git a/decode_stream_test.go b/decode_stream_test.go @@ -338,7 +338,7 @@ func (r *StreamReader) Write() { for r.writeCounter < t { time.Sleep(time.Duration(r.writeCounter*100) * time.Millisecond) currentChunkStart := (chunkSize) * r.writeCounter - lastWrite := currentChunkStart + chunkSize + lastWrite = currentChunkStart + chunkSize r.readChan <- r.data[currentChunkStart:lastWrite] carry = l - lastWrite r.writeCounter++ diff --git a/decode_string_test.go b/decode_string_test.go @@ -57,6 +57,8 @@ func TestDecoderStringDecoderAPI(t *testing.T) { } func TestDecoderStringPoolError(t *testing.T) { + // reset the pool to make sure it's not full + decPool = make(chan *Decoder, 16) result := "" dec := NewDecoder(nil) dec.Release() diff --git a/decode_test.go b/decode_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "reflect" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -553,53 +554,17 @@ func TestUnmarshalObjects(t *testing.T) { } } -func TestUnmarshalArrays(t *testing.T) { - testCases := []struct { - name string - v UnmarshalerArray - d []byte - expectations func(err error, v interface{}, t *testing.T) - }{ - { - v: new(testDecodeSlice), - d: []byte(`[{"test":"test"}]`), - name: "test decode slice", - expectations: func(err error, v interface{}, t *testing.T) { - vtPtr := v.(*testDecodeSlice) - vt := *vtPtr - assert.Nil(t, err, "err must be nil") - assert.Len(t, vt, 1, "len of vt must be 1") - assert.Equal(t, "test", vt[0].test, "vt[0].test must be equal to 'test'") - }, - }, - { - v: new(testDecodeSlice), - d: []byte(`[{"test":"test"},{"test":"test2"}]`), - name: "test decode slice", - expectations: func(err error, v interface{}, t *testing.T) { - vtPtr := v.(*testDecodeSlice) - vt := *vtPtr - assert.Nil(t, err, "err must be nil") - assert.Len(t, vt, 2, "len of vt must be 2") - assert.Equal(t, "test", vt[0].test, "vt[0].test must be equal to 'test'") - assert.Equal(t, "test2", vt[1].test, "vt[1].test must be equal to 'test2'") - }, - }, - { - v: new(testDecodeSlice), - d: []byte(`invalid json`), - name: "test decode object null", - expectations: func(err error, v interface{}, t *testing.T) { - assert.NotNil(t, err, "err must not be nil") - assert.IsType(t, InvalidJSONError(""), err, "err must be of type InvalidJSONError") - }, - }, - } - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(*testing.T) { - err := UnmarshalArray(testCase.d, testCase.v) - testCase.expectations(err, testCase.v, t) - }) - } +func TestSkipData(t *testing.T) { + t.Run("error-invalid-json", func(t *testing.T) { + dec := NewDecoder(strings.NewReader("")) + err := dec.skipData() + assert.NotNil(t, err, "err should not be nil as data is empty") + assert.IsType(t, InvalidJSONError(""), err, "err should of type InvalidJSONError") + }) + t.Run("skip-array-error-invalid-json", func(t *testing.T) { + dec := NewDecoder(strings.NewReader("")) + _, err := dec.skipArray() + assert.NotNil(t, err, "err should not be nil as data is empty") + assert.IsType(t, InvalidJSONError(""), err, "err should of type InvalidJSONError") + }) } diff --git a/encode.go b/encode.go @@ -2,6 +2,7 @@ package gojay import ( "fmt" + "io" "reflect" ) @@ -28,7 +29,7 @@ import ( // fmt.Println(b) // {"id":123456} // } func MarshalObject(v MarshalerObject) ([]byte, error) { - enc := NewEncoder() + enc := newEncoder() defer enc.Release() return enc.encodeObject(v) } @@ -55,7 +56,7 @@ func MarshalObject(v MarshalerObject) ([]byte, error) { // fmt.Println(b) // [{"id":123456},{"id":7890}] // } func MarshalArray(v MarshalerArray) ([]byte, error) { - enc := NewEncoder() + enc := newEncoder() enc.grow(200) enc.writeByte('[') v.(MarshalerArray).MarshalArray(enc) @@ -95,82 +96,70 @@ func MarshalArray(v MarshalerArray) ([]byte, error) { // fmt.Println(b) // {"id":123456} // } func Marshal(v interface{}) ([]byte, error) { - var b []byte - var err error = InvalidTypeError("Unknown type to Marshal") switch vt := v.(type) { case MarshalerObject: - enc := BorrowEncoder() - enc.writeByte('{') - vt.MarshalObject(enc) - enc.writeByte('}') - b = enc.buf + enc := BorrowEncoder(nil) defer enc.Release() - return b, nil + return enc.encodeObject(vt) case MarshalerArray: - enc := BorrowEncoder() - enc.writeByte('[') - vt.MarshalArray(enc) - enc.writeByte(']') - b = enc.buf + enc := BorrowEncoder(nil) defer enc.Release() - return b, nil + return enc.encodeArray(vt) case string: - enc := BorrowEncoder() - b, err = enc.encodeString(vt) + enc := BorrowEncoder(nil) defer enc.Release() + return enc.encodeString(vt) case bool: - enc := BorrowEncoder() - err = enc.AddBool(vt) - b = enc.buf + enc := BorrowEncoder(nil) defer enc.Release() + return enc.encodeBool(vt) case int: - enc := BorrowEncoder() - b, err = enc.encodeInt(int64(vt)) + enc := BorrowEncoder(nil) defer enc.Release() + return enc.encodeInt(vt) case int64: - enc := BorrowEncoder() + enc := BorrowEncoder(nil) defer enc.Release() - return enc.encodeInt(vt) + return enc.encodeInt64(vt) case int32: - enc := BorrowEncoder() + enc := BorrowEncoder(nil) defer enc.Release() - return enc.encodeInt(int64(vt)) + return enc.encodeInt(int(vt)) case int16: - enc := BorrowEncoder() + enc := BorrowEncoder(nil) defer enc.Release() - return enc.encodeInt(int64(vt)) + return enc.encodeInt(int(vt)) case int8: - enc := BorrowEncoder() + enc := BorrowEncoder(nil) defer enc.Release() - return enc.encodeInt(int64(vt)) + return enc.encodeInt(int(vt)) case uint64: - enc := BorrowEncoder() + enc := BorrowEncoder(nil) defer enc.Release() - return enc.encodeInt(int64(vt)) + return enc.encodeInt(int(vt)) case uint32: - enc := BorrowEncoder() + enc := BorrowEncoder(nil) defer enc.Release() - return enc.encodeInt(int64(vt)) + return enc.encodeInt(int(vt)) case uint16: - enc := BorrowEncoder() + enc := BorrowEncoder(nil) defer enc.Release() - return enc.encodeInt(int64(vt)) + return enc.encodeInt(int(vt)) case uint8: - enc := BorrowEncoder() - b, err = enc.encodeInt(int64(vt)) + enc := BorrowEncoder(nil) defer enc.Release() + return enc.encodeInt(int(vt)) case float64: - enc := BorrowEncoder() + enc := BorrowEncoder(nil) defer enc.Release() return enc.encodeFloat(vt) case float32: - enc := BorrowEncoder() + enc := BorrowEncoder(nil) defer enc.Release() - return enc.encodeFloat(float64(vt)) + return enc.encodeFloat32(vt) default: - err = InvalidMarshalError(fmt.Sprintf(invalidMarshalErrorMsg, reflect.TypeOf(vt).String())) + return nil, InvalidMarshalError(fmt.Sprintf(invalidMarshalErrorMsg, reflect.TypeOf(vt).String())) } - return b, err } // MarshalerObject is the interface to implement for struct to be encoded @@ -183,18 +172,24 @@ type MarshalerObject interface { // for a slice or an array to be encoded type MarshalerArray interface { MarshalArray(enc *Encoder) + IsNil() bool } // An Encoder writes JSON values to an output stream. type Encoder struct { buf []byte isPooled byte + w io.Writer + err error } func (enc *Encoder) getPreviousRune() (byte, bool) { last := len(enc.buf) - 1 - if last < 0 { - return 0, false - } return enc.buf[last], true } + +func (enc *Encoder) write() (int, error) { + i, err := enc.w.Write(enc.buf) + enc.buf = make([]byte, 0, 512) + return i, err +} diff --git a/encode_array.go b/encode_array.go @@ -1,45 +1,100 @@ package gojay // EncodeArray encodes an implementation of MarshalerArray to JSON -func (enc *Encoder) EncodeArray(v MarshalerArray) ([]byte, error) { +func (enc *Encoder) EncodeArray(v MarshalerArray) error { if enc.isPooled == 1 { panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) } - return enc.encodeArray(v) + _, _ = enc.encodeArray(v) + _, err := enc.write() + if err != nil { + enc.err = err + return err + } + return nil } func (enc *Encoder) encodeArray(v MarshalerArray) ([]byte, error) { enc.grow(200) enc.writeByte('[') v.MarshalArray(enc) enc.writeByte(']') - return enc.buf, nil + return enc.buf, enc.err } -// AddArray adds an array or slice to be encoded, must be used inside a slice or array encoding (does not encode a key) +// AddArray adds an implementation of MarshalerArray 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 { +func (enc *Encoder) AddArray(v MarshalerArray) { + if v.IsNil() { + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeByte('[') + enc.writeByte(']') + return + } r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') } enc.writeByte('[') - value.MarshalArray(enc) + v.MarshalArray(enc) + enc.writeByte(']') +} + +// AddArrayOmitEmpty 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) AddArrayOmitEmpty(v MarshalerArray) { + if v.IsNil() { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeByte('[') + v.MarshalArray(enc) enc.writeByte(']') - return nil } // AddArrayKey adds an array or slice to be encoded, must be used inside an object as it will encode a key // value must implement Marshaler -func (enc *Encoder) AddArrayKey(key string, value MarshalerArray) error { - // grow to avoid allocs (length of key/value + quotes) +func (enc *Encoder) AddArrayKey(key string, v MarshalerArray) { + if v.IsNil() { + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.writeBytes(objKeyArr) + enc.writeByte(']') + return + } r, ok := enc.getPreviousRune() if ok && r != '[' && r != '{' { enc.writeByte(',') } enc.writeByte('"') enc.writeString(key) - enc.write(objKeyArr) - value.MarshalArray(enc) + enc.writeBytes(objKeyArr) + v.MarshalArray(enc) + enc.writeByte(']') +} + +// AddArrayKeyOmitEmpty adds an array or slice to be encoded and skips it if it is nil. +// Must be called inside an object as it will encode a key. +func (enc *Encoder) AddArrayKeyOmitEmpty(key string, v MarshalerArray) { + if v.IsNil() { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '[' && r != '{' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.writeBytes(objKeyArr) + v.MarshalArray(enc) enc.writeByte(']') - return nil } diff --git a/encode_array_test.go b/encode_array_test.go @@ -1,6 +1,7 @@ package gojay import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -13,6 +14,9 @@ func (t TestEncodingArrStrings) MarshalArray(enc *Encoder) { enc.AddString(e) } } +func (t TestEncodingArrStrings) IsNil() bool { + return len(t) == 0 +} type TestEncodingArr []*TestEncoding @@ -21,142 +25,309 @@ func (t TestEncodingArr) MarshalArray(enc *Encoder) { enc.AddObject(e) } } -func TestEncoderArrayObjects(t *testing.T) { - v := &TestEncodingArr{ - &TestEncoding{ - test: "hello world", - test2: "漢字", - testInt: 1, - testBool: true, - testInterface: 1, - sub: &SubObject{ - test1: 10, - test2: "hello world", - test3: 1.23543, - testBool: true, +func (t TestEncodingArr) IsNil() bool { + return t == nil +} + +type testEncodingArrInterfaces []interface{} + +func (t testEncodingArrInterfaces) MarshalArray(enc *Encoder) { + for _, e := range t { + enc.AddInterface(e) + } +} +func (t testEncodingArrInterfaces) IsNil() bool { + return t == nil +} + +func TestEncoderArrayMarshalAPI(t *testing.T) { + t.Run("array-objects", func(t *testing.T) { + v := &TestEncodingArr{ + &TestEncoding{ + test: "hello world", + test2: "漢字", + testInt: 1, + testBool: true, + testInterface: 1, sub: &SubObject{ test1: 10, - testBool: false, test2: "hello world", + test3: 1.23543, + testBool: true, + sub: &SubObject{ + test1: 10, + testBool: false, + test2: "hello world", + }, }, }, - }, - &TestEncoding{ - test: "hello world", - test2: "漢字", - testInt: 1, - testBool: true, - sub: &SubObject{ - test1: 10, - test2: "hello world", - test3: 1.23543, + &TestEncoding{ + test: "hello world", + test2: "漢字", + testInt: 1, testBool: true, sub: &SubObject{ test1: 10, - testBool: false, test2: "hello world", + test3: 1.23543, + testBool: true, + sub: &SubObject{ + test1: 10, + testBool: false, + test2: "hello world", + }, }, }, - }, + nil, + } + r, err := Marshal(v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `[{"test":"hello world","test2":"漢字","testInt":1,"testBool":true,`+ + `"testArr":[],"testF64":0,"testF32":0,"testInterface":1,"sub":{"test1":10,"test2":"hello world",`+ + `"test3":1.23543,"testBool":true,"sub":{"test1":10,"test2":"hello world",`+ + `"test3":0,"testBool":false,"sub":{}}}},{"test":"hello world","test2":"漢字","testInt":1,`+ + `"testBool":true,"testArr":[],"testF64":0,"testF32":0,"sub":{"test1":10,"test2":"hello world","test3":1.23543,`+ + `"testBool":true,"sub":{"test1":10,"test2":"hello world","test3":0,"testBool":false,"sub":{}}}},{}]`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("array-interfaces", func(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{}, + &TestEncodingArrStrings{}, + true, + false, + "test", + &TestEncoding{ + test: "hello world", + test2: "foobar", + testInt: 1, + testBool: true, + }, + } + r, err := MarshalArray(v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `[1,1,1,1,1,1,1,1,1.31,1.31,[],[],true,false,"test",{"test":"hello world","test2":"foobar","testInt":1,"testBool":true,"testArr":[],"testF64":0,"testF32":0,"sub":{}}]`, + string(r), + "Result of marshalling is different as the one expected") + }) +} + +func TestEncoderArrayEncodeAPI(t *testing.T) { + t.Run("array-interfaces", func(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, + }, + } + builder := &strings.Builder{} + enc := BorrowEncoder(builder) + defer enc.Release() + 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,"sub":{}}]`, + builder.String(), + "Result of marshalling is different as the one expected") + }) + + t.Run("array-interfaces-write-error", func(t *testing.T) { + v := &testEncodingArrInterfaces{} + w := TestWriterError("") + enc := BorrowEncoder(w) + defer enc.Release() + err := enc.EncodeArray(v) + assert.NotNil(t, err, "err should not be nil") + }) +} + +// Array add with omit key tests + +type TestEncodingIntOmitEmpty []int + +func (t TestEncodingIntOmitEmpty) MarshalArray(enc *Encoder) { + for _, e := range t { + enc.AddIntOmitEmpty(e) } - r, err := Marshal(v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `[{"test":"hello world","test2":"漢字","testInt":1,"testBool":true,`+ - `"testArr":[],"testF64":0,"testF32":0,"testInterface":1,"sub":{"test1":10,"test2":"hello world",`+ - `"test3":1.23543,"testBool":true,"sub":{"test1":10,"test2":"hello world",`+ - `"test3":0,"testBool":false}}},{"test":"hello world","test2":"漢字","testInt":1,`+ - `"testBool":true,"testArr":[],"testF64":0,"testF32":0,"sub":{"test1":10,"test2":"hello world","test3":1.23543,`+ - `"testBool":true,"sub":{"test1":10,"test2":"hello world","test3":0,"testBool":false}}}]`, - string(r), - "Result of marshalling is different as the one expected") +} +func (t TestEncodingIntOmitEmpty) IsNil() bool { + return t == nil } -type testEncodingArrInterfaces []interface{} +type TestEncodingStringOmitEmpty []string -func (t testEncodingArrInterfaces) MarshalArray(enc *Encoder) { +func (t TestEncodingStringOmitEmpty) MarshalArray(enc *Encoder) { for _, e := range t { - enc.AddInterface(e) + enc.AddStringOmitEmpty(e) + } +} +func (t TestEncodingStringOmitEmpty) IsNil() bool { + return t == nil +} + +type TestEncodingFloatOmitEmpty []float64 + +func (t TestEncodingFloatOmitEmpty) MarshalArray(enc *Encoder) { + for _, e := range t { + enc.AddFloatOmitEmpty(e) } } +func (t TestEncodingFloatOmitEmpty) IsNil() bool { + return t == nil +} -func TestEncoderArrayInterfaces(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, - }, +type TestEncodingFloat32OmitEmpty []float32 + +func (t TestEncodingFloat32OmitEmpty) MarshalArray(enc *Encoder) { + for _, e := range t { + enc.AddFloat32OmitEmpty(e) } - r, err := MarshalArray(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 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, - }, +} +func (t TestEncodingFloat32OmitEmpty) IsNil() bool { + return t == nil +} + +type TestEncodingBoolOmitEmpty []bool + +func (t TestEncodingBoolOmitEmpty) MarshalArray(enc *Encoder) { + for _, e := range t { + enc.AddBoolOmitEmpty(e) + } +} +func (t TestEncodingBoolOmitEmpty) IsNil() bool { + return len(t) == 0 +} + +type TestEncodingArrOmitEmpty []TestEncodingBoolOmitEmpty + +func (t TestEncodingArrOmitEmpty) MarshalArray(enc *Encoder) { + for _, e := range t { + enc.AddArrayOmitEmpty(e) } - 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") +} +func (t TestEncodingArrOmitEmpty) IsNil() bool { + return len(t) == 0 +} + +type TestObjEmpty struct { + empty bool +} + +func (t *TestObjEmpty) MarshalObject(enc *Encoder) { +} + +func (t *TestObjEmpty) IsNil() bool { + return !t.empty +} + +type TestEncodingObjOmitEmpty []*TestObjEmpty + +func (t TestEncodingObjOmitEmpty) MarshalArray(enc *Encoder) { + for _, e := range t { + enc.AddObjectOmitEmpty(e) + } +} +func (t TestEncodingObjOmitEmpty) IsNil() bool { + return t == nil +} + +func TestEncoderArrayOmitEmpty(t *testing.T) { + t.Run("omit-int", func(t *testing.T) { + intArr := TestEncodingIntOmitEmpty{0, 1, 0, 1} + b, err := Marshal(intArr) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, `[1,1]`, string(b), "string(b) must be equal to `[1,1]`") + }) + t.Run("omit-float", func(t *testing.T) { + floatArr := TestEncodingFloatOmitEmpty{0, 1, 0, 1} + b, err := Marshal(floatArr) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, `[1,1]`, string(b), "string(b) must be equal to `[1,1]`") + }) + t.Run("omit-float32", func(t *testing.T) { + float32Arr := TestEncodingFloat32OmitEmpty{0, 1, 0, 1} + b, err := Marshal(float32Arr) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, `[1,1]`, string(b), "string(b) must be equal to `[1,1]`") + }) + t.Run("omit-string", func(t *testing.T) { + stringArr := TestEncodingStringOmitEmpty{"", "hello", "", "world"} + b, err := Marshal(stringArr) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, `["hello","world"]`, string(b), "string(b) must be equal to `[\"hello\",\"world\"]`") + }) + t.Run("omit-bool", func(t *testing.T) { + boolArr := TestEncodingBoolOmitEmpty{false, true, false, true} + b, err := Marshal(boolArr) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, `[true,true]`, string(b), "string(b) must be equal to `[true,true]`") + }) + t.Run("omit-arr", func(t *testing.T) { + arrArr := TestEncodingArrOmitEmpty{TestEncodingBoolOmitEmpty{true}, nil, TestEncodingBoolOmitEmpty{true}, nil} + b, err := Marshal(arrArr) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, `[[true],[true]]`, string(b), "string(b) must be equal to `[[true],[true]]`") + }) + t.Run("omit-obj", func(t *testing.T) { + objArr := TestEncodingObjOmitEmpty{&TestObjEmpty{true}, &TestObjEmpty{false}, &TestObjEmpty{true}, &TestObjEmpty{false}} + b, err := Marshal(objArr) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, `[{},{}]`, string(b), "string(b) must be equal to `[{},{}]`") + }) +} + +func TestEncoderArrErrors(t *testing.T) { + t.Run("add-interface-error", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + enc.AddInterface(nil) + assert.Nil(t, enc.err, "enc.Err() should not be nil") + assert.Equal(t, "", builder.String(), "builder.String() should not be ''") + }) + t.Run("array-pooled-error", func(t *testing.T) { + v := &testEncodingArrInterfaces{} + enc := BorrowEncoder(nil) + 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 @@ -3,11 +3,17 @@ package gojay import "strconv" // EncodeBool encodes a bool to JSON -func (enc *Encoder) EncodeBool(v bool) ([]byte, error) { +func (enc *Encoder) EncodeBool(v bool) error { if enc.isPooled == 1 { panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) } - return enc.encodeBool(v) + _, _ = enc.encodeBool(v) + _, err := enc.write() + if err != nil { + enc.err = err + return err + } + return nil } // encodeBool encodes a bool to JSON @@ -17,32 +23,58 @@ func (enc *Encoder) encodeBool(v bool) ([]byte, error) { } else { enc.writeString("false") } - return enc.buf, nil + return enc.buf, enc.err } // 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 { +func (enc *Encoder) AddBool(v bool) { r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') } - if value { + if v { enc.writeString("true") } else { enc.writeString("false") } - return nil +} + +// AddBoolOmitEmpty adds a bool to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddBoolOmitEmpty(v bool) { + if v == false { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeString("true") } // 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 { +func (enc *Encoder) AddBoolKey(key string, value bool) { r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') enc.writeString(key) - enc.write(objKey) + enc.writeBytes(objKey) enc.buf = strconv.AppendBool(enc.buf, value) - return nil +} + +// AddBoolKeyOmitEmpty adds a bool to be encoded and skips it if it is zero value. +// Must be used inside an object as it will encode a key. +func (enc *Encoder) AddBoolKeyOmitEmpty(key string, v bool) { + if v == false { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.writeBytes(objKey) + enc.buf = strconv.AppendBool(enc.buf, v) } diff --git a/encode_bool_test.go b/encode_bool_test.go @@ -1,36 +1,64 @@ package gojay import ( + "strings" "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 TestEncoderBoolMarshalAPI(t *testing.T) { + t.Run("true", func(t *testing.T) { + b, err := Marshal(true) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "true", string(b), "string(b) must be equal to 'true'") + }) + t.Run("false", func(t *testing.T) { + b, err := Marshal(false) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "false", string(b), "string(b) must be equal to 'false'") + }) } -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 TestEncoderBoolEncodeAPI(t *testing.T) { + t.Run("true", func(t *testing.T) { + builder := &strings.Builder{} + enc := BorrowEncoder(builder) + defer enc.Release() + err := enc.EncodeBool(true) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "true", builder.String(), "string(b) must be equal to 'true'") + }) + t.Run("false", func(t *testing.T) { + builder := &strings.Builder{} + enc := BorrowEncoder(builder) + defer enc.Release() + err := enc.EncodeBool(false) + assert.Nil(t, err, "err must be nil") + assert.Equal(t, "false", builder.String(), "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") +func TestEncoderBoolErrors(t *testing.T) { + t.Run("pool-error", func(t *testing.T) { + builder := &strings.Builder{} + enc := BorrowEncoder(builder) + 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") + }) + t.Run("encode-api-write-error", func(t *testing.T) { + v := true + w := TestWriterError("") + enc := BorrowEncoder(w) + defer enc.Release() + err := enc.EncodeBool(v) + assert.NotNil(t, err, "err should not be nil") + }) } diff --git a/encode_builder.go b/encode_builder.go @@ -20,7 +20,7 @@ func (enc *Encoder) grow(n int) { // Write appends the contents of p to b's Buffer. // Write always returns len(p), nil. -func (enc *Encoder) write(p []byte) { +func (enc *Encoder) writeBytes(p []byte) { enc.buf = append(enc.buf, p...) } diff --git a/encode_builder_test.go b/encode_builder_test.go @@ -2,15 +2,16 @@ package gojay import ( "testing" + "github.com/stretchr/testify/assert" ) func TestEncoderBuilderError(t *testing.T) { - enc := NewEncoder() + enc := NewEncoder(nil) defer func() { err := recover() assert.NotNil(t, err, "err is not nil as we pass an invalid number to grow") }() enc.grow(-1) assert.True(t, false, "should not be called") -} -\ No newline at end of file +} diff --git a/encode_interface.go b/encode_interface.go @@ -9,114 +9,167 @@ import ( // // 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) { +func (enc *Encoder) Encode(v interface{}) error { if enc.isPooled == 1 { panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) } switch vt := v.(type) { case string: - return enc.encodeString(vt) + return enc.EncodeString(vt) case bool: - return enc.encodeBool(vt) + return enc.EncodeBool(vt) case MarshalerArray: - return enc.encodeArray(vt) + return enc.EncodeArray(vt) case MarshalerObject: - return enc.encodeObject(vt) + return enc.EncodeObject(vt) case int: - return enc.encodeInt(int64(vt)) + return enc.EncodeInt(vt) case int64: - return enc.encodeInt(vt) + return enc.EncodeInt64(vt) case int32: - return enc.encodeInt(int64(vt)) + return enc.EncodeInt(int(vt)) case int8: - return enc.encodeInt(int64(vt)) + return enc.EncodeInt(int(vt)) case uint64: - return enc.encodeInt(int64(vt)) + return enc.EncodeInt(int(vt)) case uint32: - return enc.encodeInt(int64(vt)) + return enc.EncodeInt(int(vt)) case uint16: - return enc.encodeInt(int64(vt)) + return enc.EncodeInt(int(vt)) case uint8: - return enc.encodeInt(int64(vt)) + return enc.EncodeInt(int(vt)) case float64: - return enc.encodeFloat(vt) + return enc.EncodeFloat(vt) case float32: - return enc.encodeFloat(float64(vt)) + return enc.EncodeFloat32(vt) default: - return nil, InvalidMarshalError(fmt.Sprintf(invalidMarshalErrorMsg, reflect.TypeOf(vt).String())) + return 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) { +func (enc *Encoder) AddInterface(value interface{}) { + switch vt := value.(type) { case string: - return enc.AddString(value.(string)) + enc.AddString(vt) case bool: - return enc.AddBool(value.(bool)) + enc.AddBool(vt) case MarshalerArray: - return enc.AddArray(value.(MarshalerArray)) + enc.AddArray(vt) case MarshalerObject: - return enc.AddObject(value.(MarshalerObject)) + enc.AddObject(vt) case int: - return enc.AddInt(value.(int)) + enc.AddInt(vt) case int64: - return enc.AddInt(int(value.(int64))) + enc.AddInt(int(vt)) case int32: - return enc.AddInt(int(value.(int32))) + enc.AddInt(int(vt)) case int8: - return enc.AddInt(int(value.(int8))) + enc.AddInt(int(vt)) case uint64: - return enc.AddInt(int(value.(uint64))) + enc.AddInt(int(vt)) case uint32: - return enc.AddInt(int(value.(uint32))) + enc.AddInt(int(vt)) case uint16: - return enc.AddInt(int(value.(uint16))) + enc.AddInt(int(vt)) case uint8: - return enc.AddInt(int(value.(uint8))) + enc.AddInt(int(vt)) case float64: - return enc.AddFloat(value.(float64)) + enc.AddFloat(vt) case float32: - return enc.AddFloat(float64(value.(float32))) + enc.AddFloat32(vt) + default: + t := reflect.TypeOf(vt) + if t != nil { + enc.err = InvalidMarshalError(fmt.Sprintf(invalidMarshalErrorMsg, t.String())) + return + } + return } - - return nil } // AddInterfaceKey adds an interface{} to be encoded, must be used inside an object as it will encode a key -func (enc *Encoder) AddInterfaceKey(key string, value interface{}) error { +func (enc *Encoder) AddInterfaceKey(key string, value interface{}) { switch vt := value.(type) { case string: - return enc.AddStringKey(key, vt) + enc.AddStringKey(key, vt) case bool: - return enc.AddBoolKey(key, vt) + enc.AddBoolKey(key, vt) case MarshalerArray: - return enc.AddArrayKey(key, value.(MarshalerArray)) + enc.AddArrayKey(key, vt) case MarshalerObject: - return enc.AddObjectKey(key, value.(MarshalerObject)) + enc.AddObjectKey(key, vt) case int: - return enc.AddIntKey(key, vt) + enc.AddIntKey(key, vt) case int64: - return enc.AddIntKey(key, int(vt)) + enc.AddIntKey(key, int(vt)) case int32: - return enc.AddIntKey(key, int(vt)) + enc.AddIntKey(key, int(vt)) case int16: - return enc.AddIntKey(key, int(vt)) + enc.AddIntKey(key, int(vt)) case int8: - return enc.AddIntKey(key, int(vt)) + enc.AddIntKey(key, int(vt)) case uint64: - return enc.AddIntKey(key, int(vt)) + enc.AddIntKey(key, int(vt)) case uint32: - return enc.AddIntKey(key, int(vt)) + enc.AddIntKey(key, int(vt)) case uint16: - return enc.AddIntKey(key, int(vt)) + enc.AddIntKey(key, int(vt)) case uint8: - return enc.AddIntKey(key, int(vt)) + enc.AddIntKey(key, int(vt)) case float64: - return enc.AddFloatKey(key, vt) + enc.AddFloatKey(key, vt) case float32: - return enc.AddFloat32Key(key, vt) + enc.AddFloat32Key(key, vt) + default: + t := reflect.TypeOf(vt) + if t != nil { + enc.err = InvalidMarshalError(fmt.Sprintf(invalidMarshalErrorMsg, t.String())) + return + } + return } +} - return nil +// AddInterfaceKeyOmitEmpty adds an interface{} to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddInterfaceKeyOmitEmpty(key string, v interface{}) { + switch vt := v.(type) { + case string: + enc.AddStringKeyOmitEmpty(key, vt) + case bool: + enc.AddBoolKeyOmitEmpty(key, vt) + case MarshalerArray: + enc.AddArrayKeyOmitEmpty(key, vt) + case MarshalerObject: + enc.AddObjectKeyOmitEmpty(key, vt) + case int: + enc.AddIntKeyOmitEmpty(key, vt) + case int64: + enc.AddIntKeyOmitEmpty(key, int(vt)) + case int32: + enc.AddIntKeyOmitEmpty(key, int(vt)) + case int16: + enc.AddIntKeyOmitEmpty(key, int(vt)) + case int8: + enc.AddIntKeyOmitEmpty(key, int(vt)) + case uint64: + enc.AddIntKeyOmitEmpty(key, int(vt)) + case uint32: + enc.AddIntKeyOmitEmpty(key, int(vt)) + case uint16: + enc.AddIntKeyOmitEmpty(key, int(vt)) + case uint8: + enc.AddIntKeyOmitEmpty(key, int(vt)) + case float64: + enc.AddFloatKeyOmitEmpty(key, vt) + case float32: + enc.AddFloat32KeyOmitEmpty(key, vt) + default: + t := reflect.TypeOf(vt) + if t != nil { + enc.err = InvalidMarshalError(fmt.Sprintf(invalidMarshalErrorMsg, t.String())) + return + } + return + } } diff --git a/encode_interface_test.go b/encode_interface_test.go @@ -3,6 +3,7 @@ package gojay import ( "fmt" "reflect" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -10,109 +11,116 @@ import ( var encoderTestCases = []struct { v interface{} - expectations func(t *testing.T, b []byte, err error) + expectations func(t *testing.T, b string, err error) }{ { v: 100, - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, "100", string(b), "string(b) should equal 100") + assert.Equal(t, "100", b, "b should equal 100") }, }, { v: int64(100), - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, "100", string(b), "string(b) should equal 100") + assert.Equal(t, "100", b, "b should equal 100") }, }, { v: int32(100), - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, "100", string(b), "string(b) should equal 100") + assert.Equal(t, "100", b, "b should equal 100") }, }, { v: int8(100), - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, "100", string(b), "string(b) should equal 100") + assert.Equal(t, "100", b, "b should equal 100") }, }, { v: uint64(100), - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, "100", string(b), "string(b) should equal 100") + assert.Equal(t, "100", b, "b should equal 100") }, }, { v: uint32(100), - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, "100", string(b), "string(b) should equal 100") + assert.Equal(t, "100", b, "b should equal 100") }, }, { v: uint16(100), - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, "100", string(b), "string(b) should equal 100") + assert.Equal(t, "100", b, "b should equal 100") }, }, { v: uint8(100), - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, "100", string(b), "string(b) should equal 100") + assert.Equal(t, "100", b, "b should equal 100") }, }, { v: float64(100.12), - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, "100.12", string(b), "string(b) should equal 100.12") + assert.Equal(t, "100.12", b, "b should equal 100.12") + }, + }, + { + v: float32(100.12), + expectations: func(t *testing.T, b string, err error) { + assert.Nil(t, err, "err should be nil") + assert.Equal(t, "100.12", b, "b should equal 100.12") }, }, { v: true, - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, "true", string(b), "string(b) should equal true") + assert.Equal(t, "true", b, "b should equal true") }, }, { v: "hello world", - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, `"hello world"`, string(b), `string(b) should equal "hello world"`) + assert.Equal(t, `"hello world"`, b, `b should equal "hello world"`) }, }, { v: "hello world", - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") - assert.Equal(t, `"hello world"`, string(b), `string(b) should equal "hello world"`) + assert.Equal(t, `"hello world"`, b, `b should equal "hello world"`) }, }, { v: &TestEncodingArrStrings{"hello world", "foo bar"}, - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, 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"]`) + assert.Equal(t, `["hello world","foo bar"]`, b, `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) { + expectations: func(t *testing.T, b string, err error) { assert.Nil(t, err, "err should be nil") assert.Equal(t, `{"testStr":"漢字","testInt":1,"testInt64":1,"testInt32":1,"testInt16":1,"testInt8":1,"testUint64":1,"testUint32":1,"testUint16":1,"testUint8":1,"testFloat64":1.1,"testFloat32":1.1,"testBool":true}`, string(b), `string(b) should equal {"testStr":"漢字","testInt":1,"testInt64":1,"testInt32":1,"testInt16":1,"testInt8":1,"testUint64":1,"testUint32":1,"testUint16":1,"testUint8":1,"testFloat64":1.1,"testFloat32":1.1,"testBool":true}`) }, }, { v: &struct{}{}, - expectations: func(t *testing.T, b []byte, err error) { + expectations: func(t *testing.T, b string, err error) { assert.NotNil(t, err, "err should be nil") assert.IsType(t, InvalidMarshalError(""), err, "err should be of type InvalidMarshalError") assert.Equal(t, fmt.Sprintf(invalidMarshalErrorMsg, reflect.TypeOf(&struct{}{}).String()), err.Error(), "err message should be equal to invalidMarshalErrorMsg") @@ -120,18 +128,43 @@ var encoderTestCases = []struct { }, } -func TestEncoderInterfaceAllTypesDecoderAPI(t *testing.T) { - for _, test := range encoderTestCases { - enc := BorrowEncoder() - b, err := enc.Encode(test.v) +func TestEncoderInterfaceEncodeAPI(t *testing.T) { + t.Run("encode-all-types", func(t *testing.T) { + for _, test := range encoderTestCases { + builder := &strings.Builder{} + enc := BorrowEncoder(builder) + err := enc.Encode(test.v) + enc.Release() + test.expectations(t, builder.String(), err) + } + }) + t.Run("encode-all-types-write-error", func(t *testing.T) { + v := "" + w := TestWriterError("") + enc := BorrowEncoder(w) + err := enc.Encode(v) + assert.NotNil(t, err, "err should not be nil") + }) + t.Run("encode-all-types-pool-error", func(t *testing.T) { + v := "" + w := TestWriterError("") + enc := BorrowEncoder(w) enc.Release() - test.expectations(t, b, err) - } + defer func() { + err := recover() + assert.NotNil(t, err, "err should not be nil") + assert.IsType(t, InvalidUsagePooledEncoderError(""), err, "err should be of type InvalidUsagePooledEncoderError") + }() + _ = enc.Encode(v) + assert.True(t, false, "should not be called as decoder should have panicked") + }) } -func TestEncoderInterfaceAllTypesMarshalAPI(t *testing.T) { - for _, test := range encoderTestCases { - b, err := Marshal(test.v) - test.expectations(t, b, err) - } +func TestEncoderInterfaceMarshalAPI(t *testing.T) { + t.Run("marshal-all-types", func(t *testing.T) { + for _, test := range encoderTestCases { + b, err := Marshal(test.v) + test.expectations(t, string(b), err) + } + }) } diff --git a/encode_number.go b/encode_number.go @@ -3,86 +3,204 @@ package gojay import "strconv" // EncodeInt encodes an int to JSON -func (enc *Encoder) EncodeInt(n int64) ([]byte, error) { +func (enc *Encoder) EncodeInt(n int) error { if enc.isPooled == 1 { panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) } - return enc.encodeInt(n) + _, _ = enc.encodeInt(n) + _, err := enc.write() + if err != nil { + return err + } + return nil } // encodeInt encodes an int to JSON -func (enc *Encoder) encodeInt(n int64) ([]byte, error) { - s := strconv.Itoa(int(n)) - enc.writeString(s) +func (enc *Encoder) encodeInt(n int) ([]byte, error) { + enc.buf = strconv.AppendInt(enc.buf, int64(n), 10) + return enc.buf, nil +} + +// EncodeInt64 encodes an int64 to JSON +func (enc *Encoder) EncodeInt64(n int64) error { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + _, _ = enc.encodeInt64(n) + _, err := enc.write() + if err != nil { + return err + } + return nil +} + +// encodeInt64 encodes an int to JSON +func (enc *Encoder) encodeInt64(n int64) ([]byte, error) { + enc.buf = strconv.AppendInt(enc.buf, n, 10) return enc.buf, nil } // EncodeFloat encodes a float64 to JSON -func (enc *Encoder) EncodeFloat(n float64) ([]byte, error) { +func (enc *Encoder) EncodeFloat(n float64) error { if enc.isPooled == 1 { panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) } - return enc.encodeFloat(n) + _, _ = enc.encodeFloat(n) + _, err := enc.write() + if err != nil { + return err + } + return nil } // encodeFloat encodes a float64 to JSON func (enc *Encoder) encodeFloat(n float64) ([]byte, error) { - s := strconv.FormatFloat(n, 'f', -1, 64) - enc.writeString(s) + enc.buf = strconv.AppendFloat(enc.buf, n, 'f', -1, 64) + return enc.buf, nil +} + +// EncodeFloat32 encodes a float32 to JSON +func (enc *Encoder) EncodeFloat32(n float32) error { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + _, _ = enc.encodeFloat32(n) + _, err := enc.write() + if err != nil { + return err + } + return nil +} + +func (enc *Encoder) encodeFloat32(n float32) ([]byte, error) { + enc.buf = strconv.AppendFloat(enc.buf, float64(n), 'f', -1, 32) return enc.buf, nil } // AddInt adds an int to be encoded, must be used inside a slice or array encoding (does not encode a key) -func (enc *Encoder) AddInt(value int) error { +func (enc *Encoder) AddInt(v int) { r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') } - enc.buf = strconv.AppendInt(enc.buf, int64(value), 10) - return nil + enc.buf = strconv.AppendInt(enc.buf, int64(v), 10) +} + +// AddIntOmitEmpty adds an int to be encoded and skips it if its value is 0, +// must be used inside a slice or array encoding (does not encode a key). +func (enc *Encoder) AddIntOmitEmpty(v int) { + if v == 0 { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.buf = strconv.AppendInt(enc.buf, int64(v), 10) } // AddFloat adds a float64 to be encoded, must be used inside a slice or array encoding (does not encode a key) -func (enc *Encoder) AddFloat(value float64) error { +func (enc *Encoder) AddFloat(v float64) { r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') } - enc.buf = strconv.AppendFloat(enc.buf, value, 'f', -1, 64) + enc.buf = strconv.AppendFloat(enc.buf, v, 'f', -1, 64) +} - return nil +// AddFloatOmitEmpty adds a float64 to be encoded and skips it if its value is 0, +// must be used inside a slice or array encoding (does not encode a key). +func (enc *Encoder) AddFloatOmitEmpty(v float64) { + if v == 0 { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.buf = strconv.AppendFloat(enc.buf, v, 'f', -1, 64) +} + +// AddFloat32 adds a float32 to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddFloat32(v float32) { + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.buf = strconv.AppendFloat(enc.buf, float64(v), 'f', -1, 32) +} + +// AddFloat32OmitEmpty adds an int to be encoded and skips it if its value is 0, +// must be used inside a slice or array encoding (does not encode a key). +func (enc *Encoder) AddFloat32OmitEmpty(v float32) { + if v == 0 { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.buf = strconv.AppendFloat(enc.buf, float64(v), 'f', -1, 32) } // AddIntKey adds an int to be encoded, must be used inside an object as it will encode a key -func (enc *Encoder) AddIntKey(key string, value int) error { +func (enc *Encoder) AddIntKey(key string, v int) { r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') enc.writeString(key) - enc.write(objKey) - enc.buf = strconv.AppendInt(enc.buf, int64(value), 10) + enc.writeBytes(objKey) + enc.buf = strconv.AppendInt(enc.buf, int64(v), 10) +} - return nil +// AddIntKeyOmitEmpty adds an int to be encoded and skips it if its value is 0. +// Must be used inside an object as it will encode a key. +func (enc *Encoder) AddIntKeyOmitEmpty(key string, v int) { + if v == 0 { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.writeBytes(objKey) + enc.buf = strconv.AppendInt(enc.buf, int64(v), 10) } // AddFloatKey adds a float64 to be encoded, must be used inside an object as it will encode a key -func (enc *Encoder) AddFloatKey(key string, value float64) error { +func (enc *Encoder) AddFloatKey(key string, value float64) { r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') enc.writeString(key) - enc.write(objKey) + enc.writeBytes(objKey) enc.buf = strconv.AppendFloat(enc.buf, value, 'f', -1, 64) +} - return nil +// AddFloatKeyOmitEmpty adds a float64 to be encoded and skips it if its value is 0. +// Must be used inside an object as it will encode a key +func (enc *Encoder) AddFloatKeyOmitEmpty(key string, v float64) { + if v == 0 { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.writeBytes(objKey) + enc.buf = strconv.AppendFloat(enc.buf, v, 'f', -1, 64) } // AddFloat32Key adds a float32 to be encoded, must be used inside an object as it will encode a key -func (enc *Encoder) AddFloat32Key(key string, value float32) error { +func (enc *Encoder) AddFloat32Key(key string, v float32) { r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') @@ -91,7 +209,21 @@ func (enc *Encoder) AddFloat32Key(key string, value float32) error { enc.writeString(key) enc.writeByte('"') enc.writeByte(':') - enc.buf = strconv.AppendFloat(enc.buf, float64(value), 'f', -1, 32) + enc.buf = strconv.AppendFloat(enc.buf, float64(v), 'f', -1, 32) +} - return nil +// AddFloat32KeyOmitEmpty adds a float64 to be encoded and skips it if its value is 0. +// Must be used inside an object as it will encode a key +func (enc *Encoder) AddFloat32KeyOmitEmpty(key string, v float32) { + if v == 0 { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.writeBytes(objKey) + enc.buf = strconv.AppendFloat(enc.buf, float64(v), 'f', -1, 32) } diff --git a/encode_number_test.go b/encode_number_test.go @@ -1,131 +1,229 @@ package gojay import ( + "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestEncoderInt(t *testing.T) { - r, err := Marshal(1) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `1`, - string(r), - "Result of marshalling is different as the one expected") -} - -func TestEncoderInt64(t *testing.T) { - r, err := Marshal(int64(1)) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `1`, - string(r), - "Result of marshalling is different as the one expected") -} - -func TestEncoderInt32(t *testing.T) { - r, err := Marshal(int32(1)) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `1`, - string(r), - "Result of marshalling is different as the one expected") -} - -func TestEncoderInt16(t *testing.T) { - r, err := Marshal(int16(1)) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `1`, - string(r), - "Result of marshalling is different as the one expected") -} +func TestEncoderNumberEncodeAPI(t *testing.T) { + t.Run("encoder-int", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + err := enc.EncodeInt(1) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + builder.String(), + "Result of marshalling is different as the one expected") + }) + t.Run("encode-int64", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + err := enc.EncodeInt64(int64(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + builder.String(), + "Result of marshalling is different as the one expected") + }) + t.Run("encode-float64", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + err := enc.EncodeFloat(float64(1.1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1.1`, + builder.String(), + "Result of marshalling is different as the one expected") + }) + t.Run("encode-float32", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + err := enc.EncodeFloat32(float32(1.12)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1.12`, + builder.String(), + "Result of marshalling is different as the one expected") -func TestEncoderInt8(t *testing.T) { - r, err := Marshal(int8(1)) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `1`, - string(r), - "Result of marshalling is different as the one expected") + }) } -func TestEncoderUint64(t *testing.T) { - r, err := Marshal(uint64(1)) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `1`, - string(r), - "Result of marshalling is different as the one expected") -} -func TestEncoderUint32(t *testing.T) { - r, err := Marshal(uint32(1)) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `1`, - string(r), - "Result of marshalling is different as the one expected") -} -func TestEncoderUint16(t *testing.T) { - r, err := Marshal(uint16(1)) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `1`, - string(r), - "Result of marshalling is different as the one expected") -} -func TestEncoderUint8(t *testing.T) { - r, err := Marshal(uint8(1)) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `1`, - string(r), - "Result of marshalling is different as the one expected") -} -func TestEncoderFloat(t *testing.T) { - r, err := Marshal(1.1) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `1.1`, - string(r), - "Result of marshalling is different as the one expected") -} +func TestEncoderNumberEncodeAPIErrors(t *testing.T) { + t.Run("encode-int-pool-error", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + enc.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err should not be nil") + assert.IsType(t, InvalidUsagePooledEncoderError(""), err, "err should be of type InvalidUsagePooledEncoderError") + }() + _ = enc.EncodeInt(1) + assert.True(t, false, "should not be called as encoder should have panicked") + }) + t.Run("encode-int-write-error", func(t *testing.T) { + w := TestWriterError("") + enc := NewEncoder(w) + err := enc.EncodeInt(1) + assert.NotNil(t, err, "err should not be nil") + assert.Equal(t, "Test Error", err.Error(), "err should be of type InvalidUsagePooledEncoderError") + }) + t.Run("encode-int64-pool-error", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + enc.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err should not be nil") + assert.IsType(t, InvalidUsagePooledEncoderError(""), err, "err should be of type InvalidUsagePooledEncoderError") + }() + _ = enc.EncodeInt64(1) + assert.True(t, false, "should not be called as encoder should have panicked") + }) + t.Run("encode-int64-write-error", func(t *testing.T) { + w := TestWriterError("") + enc := NewEncoder(w) + err := enc.EncodeInt64(1) + assert.NotNil(t, err, "err should not be nil") + assert.Equal(t, "Test Error", err.Error(), "err should be of type InvalidUsagePooledEncoderError") -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") + }) + t.Run("encode-float64-pool-error", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + enc.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err should not be nil") + assert.IsType(t, InvalidUsagePooledEncoderError(""), err, "err should be of type InvalidUsagePooledEncoderError") + }() + _ = enc.EncodeFloat(1.1) + assert.True(t, false, "should not be called as encoder should have panicked") + }) + t.Run("encode-float64-write-error", func(t *testing.T) { + w := TestWriterError("") + enc := NewEncoder(w) + err := enc.EncodeFloat(1.1) + assert.NotNil(t, err, "err should not be nil") + assert.Equal(t, "Test Error", err.Error(), "err should be of type InvalidUsagePooledEncoderError") + }) + t.Run("encode-float32-pool-error", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + enc.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err should not be nil") + assert.IsType(t, InvalidUsagePooledEncoderError(""), err, "err should be of type InvalidUsagePooledEncoderError") + }() + _ = enc.EncodeFloat32(float32(1.1)) + assert.True(t, false, "should not be called as encoder should have panicked") + }) + t.Run("encode-float32-write-error", func(t *testing.T) { + w := TestWriterError("") + enc := NewEncoder(w) + err := enc.EncodeFloat32(float32(1.1)) + assert.NotNil(t, err, "err should not be nil") + assert.Equal(t, "Test Error", err.Error(), "err should be of type InvalidUsagePooledEncoderError") + }) } -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") +func TestEncoderNumberMarshalAPI(t *testing.T) { + t.Run("int", func(t *testing.T) { + r, err := Marshal(1) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("int64", func(t *testing.T) { + r, err := Marshal(int64(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("int32", func(t *testing.T) { + r, err := Marshal(int32(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("int16", func(t *testing.T) { + r, err := Marshal(int16(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("int8", func(t *testing.T) { + r, err := Marshal(int8(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("uint64", func(t *testing.T) { + r, err := Marshal(uint64(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("uint32", func(t *testing.T) { + r, err := Marshal(uint32(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("uint16", func(t *testing.T) { + r, err := Marshal(uint16(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("uint8", func(t *testing.T) { + r, err := Marshal(uint8(1)) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("float64", func(t *testing.T) { + r, err := Marshal(1.1) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `1.1`, + string(r), + "Result of marshalling is different as the one expected") + }) } diff --git a/encode_object.go b/encode_object.go @@ -6,41 +6,80 @@ var objKeyArr = []byte(`":[`) var objKey = []byte(`":`) // EncodeObject encodes an object to JSON -func (enc *Encoder) EncodeObject(v MarshalerObject) ([]byte, error) { +func (enc *Encoder) EncodeObject(v MarshalerObject) error { if enc.isPooled == 1 { panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) } - return enc.encodeObject(v) + _, err := enc.encodeObject(v) + if err != nil { + enc.err = err + return err + } + _, err = enc.write() + if err != nil { + enc.err = err + return err + } + return nil } func (enc *Encoder) encodeObject(v MarshalerObject) ([]byte, error) { enc.grow(200) enc.writeByte('{') v.MarshalObject(enc) enc.writeByte('}') - return enc.buf, nil + return enc.buf, enc.err } // 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 { - if value.IsNil() { - return nil +// value must implement MarshalerObject +func (enc *Encoder) AddObject(v MarshalerObject) { + if v.IsNil() { + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('{') + enc.writeByte('}') + return } r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') } enc.writeByte('{') - value.MarshalObject(enc) + v.MarshalObject(enc) + enc.writeByte('}') +} + +// AddObjectOmitEmpty adds an object to be encoded or skips it if IsNil returns true. +// Must be used inside a slice or array encoding (does not encode a key) +// value must implement MarshalerObject +func (enc *Encoder) AddObjectOmitEmpty(v MarshalerObject) { + if v.IsNil() { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeByte('{') + v.MarshalObject(enc) enc.writeByte('}') - return nil } // AddObjectKey adds a struct to be encoded, must be used inside an object as it will encode a key -// value must implement Marshaler -func (enc *Encoder) AddObjectKey(key string, value MarshalerObject) error { +// value must implement MarshalerObject +func (enc *Encoder) AddObjectKey(key string, value MarshalerObject) { if value.IsNil() { - return nil + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.writeBytes(objKeyObj) + enc.writeByte('}') + return } r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { @@ -48,8 +87,25 @@ func (enc *Encoder) AddObjectKey(key string, value MarshalerObject) error { } enc.writeByte('"') enc.writeString(key) - enc.write(objKeyObj) + enc.writeBytes(objKeyObj) + value.MarshalObject(enc) + enc.writeByte('}') +} + +// AddObjectKeyOmitEmpty adds an object to be encoded or skips it if IsNil returns true. +// Must be used inside a slice or array encoding (does not encode a key) +// value must implement MarshalerObject +func (enc *Encoder) AddObjectKeyOmitEmpty(key string, value MarshalerObject) { + if value.IsNil() { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.writeBytes(objKeyObj) value.MarshalObject(enc) enc.writeByte('}') - return nil } diff --git a/encode_object_test.go b/encode_object_test.go @@ -1,6 +1,7 @@ package gojay import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -42,15 +43,16 @@ func (t *testObject) MarshalObject(enc *Encoder) { enc.AddBoolKey("testBool", t.testBool) } -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( - 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(r), - "Result of marshalling is different as the one expected", - ) +type testObjectWithUnknownType struct { + unknownType struct{} +} + +func (t *testObjectWithUnknownType) IsNil() bool { + return t == nil +} + +func (t *testObjectWithUnknownType) MarshalObject(enc *Encoder) { + enc.AddInterfaceKey("unknownType", t.unknownType) } type TestEncoding struct { @@ -101,163 +103,327 @@ func (t *SubObject) MarshalObject(enc *Encoder) { enc.AddObjectKey("sub", t.sub) } -func TestEncoderObjectComplex(t *testing.T) { - v := &TestEncoding{ - test: "hello world", - test2: "foobar", - testInt: 1, - testBool: true, - testF32: 120.53, - testF64: 120.15, - testInterface: true, - testArr: TestEncodingArr{ - &TestEncoding{ - test: "1", +type testEncodingObjInterfaces struct { + interfaceVal interface{} +} + +func (t *testEncodingObjInterfaces) IsNil() bool { + return t == nil +} + +func (t *testEncodingObjInterfaces) MarshalObject(enc *Encoder) { + enc.AddInterfaceKey("interfaceVal", t.interfaceVal) +} + +func TestEncoderObjectEncodeAPI(t *testing.T) { + t.Run("encode-basic", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + err := enc.EncodeObject(&testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true}) + assert.Nil(t, err, "Error 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}`, + builder.String(), + "Result of marshalling is different as the one expected", + ) + }) +} + +func TestEncoderObjectMarshalAPI(t *testing.T) { + t.Run("marshal-basic", func(t *testing.T) { + r, err := Marshal(&testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true}) + assert.Nil(t, err, "Error 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(r), + "Result of marshalling is different as the one expected", + ) + }) + + t.Run("marshal-complex", func(t *testing.T) { + v := &TestEncoding{ + test: "hello world", + test2: "foobar", + testInt: 1, + testBool: true, + testF32: 120.53, + testF64: 120.15, + testInterface: true, + testArr: TestEncodingArr{ + &TestEncoding{ + test: "1", + }, }, - }, - sub: &SubObject{ - test1: 10, - test2: "hello world", - test3: 1.23543, - testBool: true, sub: &SubObject{ test1: 10, - testBool: false, test2: "hello world", + test3: 1.23543, + testBool: true, + sub: &SubObject{ + test1: 10, + testBool: false, + test2: "hello world", + }, }, - }, - } - r, err := MarshalObject(v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"test":"hello world","test2":"foobar","testInt":1,"testBool":true,"testArr":[{"test":"1","test2":"","testInt":0,"testBool":false,"testArr":[],"testF64":0,"testF32":0}],"testF64":120.15,"testF32":120.53,"testInterface":true,"sub":{"test1":10,"test2":"hello world","test3":1.23543,"testBool":true,"sub":{"test1":10,"test2":"hello world","test3":0,"testBool":false}}}`, - string(r), - "Result of marshalling is different as the one expected", - ) + } + r, err := MarshalObject(v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"test":"hello world","test2":"foobar","testInt":1,"testBool":true,"testArr":[{"test":"1","test2":"","testInt":0,"testBool":false,"testArr":[],"testF64":0,"testF32":0,"sub":{}}],"testF64":120.15,"testF32":120.53,"testInterface":true,"sub":{"test1":10,"test2":"hello world","test3":1.23543,"testBool":true,"sub":{"test1":10,"test2":"hello world","test3":0,"testBool":false,"sub":{}}}}`, + string(r), + "Result of marshalling is different as the one expected", + ) + }) + + t.Run("marshal-interface-string", func(t *testing.T) { + v := testEncodingObjInterfaces{"string"} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":"string"}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-int", func(t *testing.T) { + v := testEncodingObjInterfaces{1} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-int64", func(t *testing.T) { + v := testEncodingObjInterfaces{int64(1)} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-int32", func(t *testing.T) { + v := testEncodingObjInterfaces{int32(1)} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-int16", func(t *testing.T) { + v := testEncodingObjInterfaces{int16(1)} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-int8", func(t *testing.T) { + v := testEncodingObjInterfaces{int8(1)} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-uint64", func(t *testing.T) { + v := testEncodingObjInterfaces{uint64(1)} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-uint32", func(t *testing.T) { + v := testEncodingObjInterfaces{uint32(1)} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-uint16", func(t *testing.T) { + v := testEncodingObjInterfaces{uint16(1)} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-uint8", func(t *testing.T) { + v := testEncodingObjInterfaces{uint8(1)} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-float64", func(t *testing.T) { + v := testEncodingObjInterfaces{float64(1.1)} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1.1}`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("marshal-interface-float32", func(t *testing.T) { + v := testEncodingObjInterfaces{float32(1.1)} + r, err := Marshal(&v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"interfaceVal":1.1}`, + string(r), + "Result of marshalling is different as the one expected") + }) } -type testEncodingObjInterfaces struct { - interfaceVal interface{} +type TestObectOmitEmpty struct { + nonNiler int + testInt int + testFloat float64 + testFloat32 float32 + testString string + testBool bool + testObectOmitEmpty *TestObectOmitEmpty + testObect *TestObectOmitEmpty } -func (t *testEncodingObjInterfaces) IsNil() bool { +func (t *TestObectOmitEmpty) IsNil() bool { return t == nil } -func (t *testEncodingObjInterfaces) MarshalObject(enc *Encoder) { - enc.AddInterfaceKey("interfaceVal", t.interfaceVal) +func (t *TestObectOmitEmpty) MarshalObject(enc *Encoder) { + enc.AddIntKeyOmitEmpty("testInt", t.testInt) + enc.AddIntKeyOmitEmpty("testIntNotEmpty", 1) + enc.AddFloatKeyOmitEmpty("testFloat", t.testFloat) + enc.AddFloatKeyOmitEmpty("testFloatNotEmpty", 1.1) + enc.AddFloat32KeyOmitEmpty("testFloat32", t.testFloat32) + enc.AddFloat32KeyOmitEmpty("testFloat32NotEmpty", 1.1) + enc.AddStringKeyOmitEmpty("testString", t.testString) + enc.AddStringKeyOmitEmpty("testStringNotEmpty", "foo") + enc.AddBoolKeyOmitEmpty("testBool", t.testBool) + enc.AddBoolKeyOmitEmpty("testBoolNotEmpty", true) + enc.AddObjectKeyOmitEmpty("testObect", t.testObect) + enc.AddObjectKeyOmitEmpty("testObectOmitEmpty", t.testObectOmitEmpty) + enc.AddArrayKeyOmitEmpty("testArrayOmitEmpty", TestEncodingArrStrings{}) + enc.AddArrayKeyOmitEmpty("testArray", TestEncodingArrStrings{"foo"}) +} + +type TestObectOmitEmptyInterface struct{} + +func (t *TestObectOmitEmptyInterface) IsNil() bool { + return t == nil +} + +func (t *TestObectOmitEmptyInterface) MarshalObject(enc *Encoder) { + enc.AddInterfaceKeyOmitEmpty("testInt", 0) + enc.AddInterfaceKeyOmitEmpty("testInt64", int64(0)) + enc.AddInterfaceKeyOmitEmpty("testInt32", int32(0)) + enc.AddInterfaceKeyOmitEmpty("testInt16", int16(0)) + enc.AddInterfaceKeyOmitEmpty("testInt8", int8(0)) + enc.AddInterfaceKeyOmitEmpty("testUint8", uint8(0)) + enc.AddInterfaceKeyOmitEmpty("testUint16", uint16(0)) + enc.AddInterfaceKeyOmitEmpty("testUint32", uint32(0)) + enc.AddInterfaceKeyOmitEmpty("testUint64", uint64(0)) + enc.AddInterfaceKeyOmitEmpty("testIntNotEmpty", 1) + enc.AddInterfaceKeyOmitEmpty("testFloat", 0) + enc.AddInterfaceKeyOmitEmpty("testFloatNotEmpty", 1.1) + enc.AddInterfaceKeyOmitEmpty("testFloat32", float32(0)) + enc.AddInterfaceKeyOmitEmpty("testFloat32NotEmpty", float32(1.1)) + enc.AddInterfaceKeyOmitEmpty("testString", "") + enc.AddInterfaceKeyOmitEmpty("testStringNotEmpty", "foo") + enc.AddInterfaceKeyOmitEmpty("testBool", false) + enc.AddInterfaceKeyOmitEmpty("testBoolNotEmpty", true) + enc.AddInterfaceKeyOmitEmpty("testObectOmitEmpty", nil) + enc.AddInterfaceKeyOmitEmpty("testObect", &TestEncoding{}) + enc.AddInterfaceKeyOmitEmpty("testArr", &TestEncodingArrStrings{}) } -func TestEncoderObjectInterfaces(t *testing.T) { - v := testEncodingObjInterfaces{"string"} - r, err := Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":"string"}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{1} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{int64(1)} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{int32(1)} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{int16(1)} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{int8(1)} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{uint64(1)} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{uint32(1)} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{uint16(1)} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{uint8(1)} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{float64(1.1)} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1.1}`, - string(r), - "Result of marshalling is different as the one expected") - v = testEncodingObjInterfaces{float32(1.1)} - r, err = Marshal(&v) - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `{"interfaceVal":1.1}`, - string(r), - "Result of marshalling is different as the one expected") +func TestEncoderObjectOmitEmpty(t *testing.T) { + t.Run("encoder-omit-empty-all-types", func(t *testing.T) { + v := &TestObectOmitEmpty{ + nonNiler: 1, + testInt: 0, + testObect: &TestObectOmitEmpty{testInt: 1}, + } + r, err := MarshalObject(v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"testIntNotEmpty":1,"testFloatNotEmpty":1.1,"testFloat32NotEmpty":1.1,"testStringNotEmpty":"foo","testBoolNotEmpty":true,"testObect":{"testInt":1,"testIntNotEmpty":1,"testFloatNotEmpty":1.1,"testFloat32NotEmpty":1.1,"testStringNotEmpty":"foo","testBoolNotEmpty":true,"testArray":["foo"]},"testArray":["foo"]}`, + string(r), + "Result of marshalling is different as the one expected", + ) + }) + + t.Run("encoder-omit-empty-interface", func(t *testing.T) { + v := &TestObectOmitEmptyInterface{} + r, err := MarshalObject(v) + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `{"testIntNotEmpty":1,"testFloatNotEmpty":1.1,"testFloat32NotEmpty":1.1,"testStringNotEmpty":"foo","testBoolNotEmpty":true,"testObect":{"test":"","test2":"","testInt":0,"testBool":false,"testArr":[],"testF64":0,"testF32":0,"sub":{}}}`, + 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") +func TestEncoderObjectEncodeAPIError(t *testing.T) { + t.Run("interface-key-error", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + err := enc.EncodeObject(&testObjectWithUnknownType{struct{}{}}) + assert.NotNil(t, err, "Error should not be nil") + assert.Equal(t, "Invalid type struct {} provided to Marshal", err.Error(), "err.Error() should be 'Invalid type struct {} provided to Marshal'") + }) + t.Run("write-error", func(t *testing.T) { + w := TestWriterError("") + enc := NewEncoder(w) + err := enc.EncodeObject(&testObject{"漢字", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.1, 1.1, true}) + assert.NotNil(t, err, "Error should not be nil") + assert.Equal(t, "Test Error", err.Error(), "err.Error() should be 'Test Error'") + }) + t.Run("interface-error", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + enc.AddInterfaceKeyOmitEmpty("test", struct{}{}) + assert.NotNil(t, enc.err, "enc.Err() should not be nil") + }) + t.Run("pool-error", func(t *testing.T) { + v := &TestEncoding{} + enc := BorrowEncoder(nil) + 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 @@ -1,32 +1,38 @@ package gojay -var encObjPool = make(chan *Encoder, 16) +import "io" + +var encPool = make(chan *Encoder, 16) +var streamEncPool = make(chan *StreamEncoder, 16) // NewEncoder returns a new encoder or borrows one from the pool -func NewEncoder() *Encoder { - return &Encoder{} +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{w: w} } func newEncoder() *Encoder { return &Encoder{} } // BorrowEncoder borrows an Encoder from the pool. -func BorrowEncoder() *Encoder { +func BorrowEncoder(w io.Writer) *Encoder { select { - case enc := <-encObjPool: + case enc := <-encPool: enc.isPooled = 0 + enc.w = w + enc.err = nil + enc.buf = make([]byte, 0) return enc default: - return &Encoder{} + return &Encoder{w: w} } } // Release sends back a Encoder to the pool. func (enc *Encoder) Release() { enc.buf = nil + enc.isPooled = 1 select { - case encObjPool <- enc: - enc.isPooled = 1 + case encPool <- enc: default: } } diff --git a/encode_pool_test.go b/encode_pool_test.go @@ -8,13 +8,13 @@ import ( func TestEncoderNewFromPool(t *testing.T) { // reset pool - encObjPool = make(chan *Encoder, 16) + encPool = make(chan *Encoder, 16) // get new Encoder - enc := NewEncoder() + enc := NewEncoder(nil) // add to pool enc.Release() // borrow encoder - nEnc := BorrowEncoder() + nEnc := BorrowEncoder(nil) // make sure it's the same assert.Equal(t, enc, nEnc, "enc and nEnc from pool should be the same") } diff --git a/encode_stream.go b/encode_stream.go @@ -0,0 +1,190 @@ +package gojay + +import ( + "strconv" + "time" +) + +// MarshalerStream is the interface to implement +// to continuously encode of stream of data. +type MarshalerStream interface { + MarshalStream(enc *StreamEncoder) +} + +// A StreamEncoder reads and encodes values to JSON from an input stream. +// +// It implements conext.Context and provide a channel to notify interruption. +type StreamEncoder struct { + *Encoder + nConsumer int + delimiter byte + deadline *time.Time + done chan struct{} +} + +// EncodeStream spins up a defined number of non blocking consumers of the MarshalerStream m. +// +// m must implement MarshalerStream. Ideally m is a channel. See example for implementation. +// +// See the documentation for Marshal for details about the conversion of Go value to JSON. +func (s *StreamEncoder) EncodeStream(m MarshalerStream) { + // if a single consumer, just use this encoder + if s.nConsumer == 1 { + go consume(s, s, m) + return + } + // else use this Encoder only for first consumer + // and use new encoders for other consumers + // this is to avoid concurrent writing to same buffer + // resulting in a weird JSON + go consume(s, s, m) + for i := 1; i < s.nConsumer; i++ { + ss := Stream.borrowEncoder(s.w) + ss.done = s.done + ss.buf = make([]byte, 0, 512) + ss.delimiter = s.delimiter + go consume(s, ss, m) + } + return +} + +// LineDelimited sets the delimiter to a new line character. +// +// It will add a new line after each JSON marshaled by the MarshalerStream +func (s *StreamEncoder) LineDelimited() *StreamEncoder { + s.delimiter = '\n' + return s +} + +// CommaDelimited sets the delimiter to a comma. +// +// It will add a new line after each JSON marshaled by the MarshalerStream +func (s *StreamEncoder) CommaDelimited() *StreamEncoder { + s.delimiter = ',' + return s +} + +// NConsumer sets the number of non blocking go routine to consume the stream. +func (s *StreamEncoder) NConsumer(n int) *StreamEncoder { + s.nConsumer = n + return s +} + +// 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 (s *StreamEncoder) Release() { + s.Encoder.isPooled = 1 + select { + case streamEncPool <- s: + default: + } +} + +// Done returns a channel that's closed when work is done. +// It implements context.Context +func (s *StreamEncoder) Done() <-chan struct{} { + return s.done +} + +// Err returns nil if Done is not yet closed. +// If Done is closed, Err returns a non-nil error explaining why. +// It implements context.Context +func (s *StreamEncoder) Err() error { + return s.err +} + +// Deadline returns the time when work done on behalf of this context +// should be canceled. Deadline returns ok==false when no deadline is +// set. Successive calls to Deadline return the same results. +func (s *StreamEncoder) Deadline() (time.Time, bool) { + if s.deadline != nil { + return *s.deadline, true + } + return time.Time{}, false +} + +// SetDeadline sets the deadline +func (s *StreamEncoder) SetDeadline(t time.Time) { + s.deadline = &t +} + +// Value implements context.Context +func (s *StreamEncoder) Value(key interface{}) interface{} { + return nil +} + +// Cancel cancels the consumers of the stream, interrupting the stream encoding. +// +// After calling cancel, Done() will return a closed channel. +func (s *StreamEncoder) Cancel(err error) { + select { + case <-s.done: + default: + s.err = err + close(s.done) + } +} + +// AddObject adds an object to be encoded. +// value must implement MarshalerObject. +func (s *StreamEncoder) AddObject(v MarshalerObject) { + if v.IsNil() { + return + } + s.Encoder.writeByte('{') + v.MarshalObject(s.Encoder) + s.Encoder.writeByte('}') + s.Encoder.writeByte(s.delimiter) +} + +// AddString adds a string to be encoded. +func (s *StreamEncoder) AddString(v string) { + s.Encoder.writeByte('"') + s.Encoder.writeString(v) + s.Encoder.writeByte('"') + s.Encoder.writeByte(s.delimiter) +} + +// AddArray adds an implementation of MarshalerArray to be encoded. +func (s *StreamEncoder) AddArray(v MarshalerArray) { + s.Encoder.writeByte('[') + v.MarshalArray(s.Encoder) + s.Encoder.writeByte(']') + s.Encoder.writeByte(s.delimiter) +} + +// AddInt adds an int to be encoded. +func (s *StreamEncoder) AddInt(value int) { + s.buf = strconv.AppendInt(s.buf, int64(value), 10) + s.Encoder.writeByte(s.delimiter) +} + +// AddFloat adds a float64 to be encoded. +func (s *StreamEncoder) AddFloat(value float64) { + s.buf = strconv.AppendFloat(s.buf, value, 'f', -1, 64) + s.Encoder.writeByte(s.delimiter) +} + +// Non exposed + +func consume(init *StreamEncoder, s *StreamEncoder, m MarshalerStream) { + defer s.Release() + for { + select { + case <-init.Done(): + return + default: + m.MarshalStream(s) + if s.Encoder.err != nil { + init.Cancel(s.Encoder.err) + return + } + i, err := s.Encoder.write() + if err != nil || i == 0 { + init.Cancel(err) + return + } + } + } +} diff --git a/encode_stream_pool.go b/encode_stream_pool.go @@ -0,0 +1,43 @@ +package gojay + +import "io" + +// NewEncoder returns a new StreamEncoder. +// It takes an io.Writer implementation to output data. +// It initiates the done channel returned by Done(). +func (s stream) NewEncoder(w io.Writer) *StreamEncoder { + enc := BorrowEncoder(w) + return &StreamEncoder{Encoder: enc, nConsumer: 1, done: make(chan struct{}, 1)} +} + +// BorrowEncoder borrows a StreamEncoder from the pool. +// It takes an io.Writer implementation to output data. +// It initiates the done channel returned by Done(). +// +// If no StreamEncoder is available in the pool, it returns a fresh one +func (s stream) BorrowEncoder(w io.Writer) *StreamEncoder { + select { + case streamEnc := <-streamEncPool: + streamEnc.isPooled = 0 + streamEnc.w = w + streamEnc.Encoder.err = nil + streamEnc.done = make(chan struct{}, 1) + streamEnc.Encoder.buf = make([]byte, 0, 512) + streamEnc.nConsumer = 1 + return streamEnc + default: + return s.NewEncoder(w) + } +} + +func (s stream) borrowEncoder(w io.Writer) *StreamEncoder { + select { + case streamEnc := <-streamEncPool: + streamEnc.isPooled = 0 + streamEnc.w = w + streamEnc.Encoder.err = nil + return streamEnc + default: + return s.NewEncoder(w) + } +} diff --git a/encode_stream_pool_test.go b/encode_stream_pool_test.go @@ -0,0 +1,21 @@ +package gojay + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncodeStreamBorrow1(t *testing.T) { + // we override the pool chan + streamEncPool = make(chan *StreamEncoder, 1) + // add one decoder to the channel + enc := Stream.NewEncoder(nil) + streamEncPool <- enc + // reset streamEncPool + streamEncPool = make(chan *StreamEncoder, 1) + // borrow one decoder to the channel + nEnc := Stream.BorrowEncoder(nil) + // make sure they are the same + assert.NotEqual(t, enc, nEnc, "encoder added to the pool and new decoder should be the same") +} diff --git a/encode_stream_test.go b/encode_stream_test.go @@ -0,0 +1,329 @@ +package gojay + +import ( + "os" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type StreamChanObject chan *testObject + +func (s StreamChanObject) MarshalStream(enc *StreamEncoder) { + select { + case <-enc.Done(): + return + case o := <-s: + enc.AddObject(o) + } +} + +type StreamChanSlice chan *TestEncodingArrStrings + +func (s StreamChanSlice) MarshalStream(enc *StreamEncoder) { + select { + case <-enc.Done(): + return + case o := <-s: + enc.AddArray(o) + } +} + +type StreamChanString chan string + +func (s StreamChanString) MarshalStream(enc *StreamEncoder) { + select { + case <-enc.Done(): + return + case o := <-s: + enc.AddString(o) + } +} + +type StreamChanInt chan int + +func (s StreamChanInt) MarshalStream(enc *StreamEncoder) { + select { + case <-enc.Done(): + return + case o := <-s: + enc.AddInt(o) + } +} + +type StreamChanFloat chan float64 + +func (s StreamChanFloat) MarshalStream(enc *StreamEncoder) { + select { + case <-enc.Done(): + return + case o := <-s: + enc.AddFloat(o) + } +} + +type StreamChanError chan *testObject + +func (s StreamChanError) MarshalStream(enc *StreamEncoder) { + select { + case <-enc.Done(): + return + case <-s: + enc.AddInterface(struct{}{}) + } +} + +// TestWriter to assert result +type TestWriter struct { + nWrite *int + target int + enc *StreamEncoder + result [][]byte + mux *sync.RWMutex +} + +func (w *TestWriter) Write(b []byte) (int, error) { + if len(b) > 0 { + w.mux.Lock() + w.result = append(w.result, b) + if len(w.result) == w.target { + w.enc.Cancel(nil) + } + w.mux.Unlock() + } + return len(b), nil +} + +func feedStreamNil(s chan *testObject, target int) { + for i := 0; i < target; i++ { + s <- nil + } +} + +func feedStream(s chan *testObject, target int) { + for i := 0; i < target; i++ { + s <- &testObject{} + } +} + +func feedStreamSlices(s chan *TestEncodingArrStrings, target int) { + for i := 0; i < target; i++ { + s <- &TestEncodingArrStrings{"test", "test2"} + } +} + +func feedStreamStrings(s chan string, target int) { + for i := 0; i < target; i++ { + s <- "hello" + } +} + +func feedStreamInt(s chan int, target int) { + for i := 0; i < target; i++ { + s <- i + } +} + +func feedStreamFloat(s chan float64, target int) { + for i := 0; i < target; i++ { + s <- float64(i) + } +} + +func TestEncodeStream(t *testing.T) { + t.Run("single-consumer-object", func(t *testing.T) { + expectedStr := + `{"testStr":"","testInt":0,"testInt64":0,"testInt32":0,"testInt16":0,"testInt8":0,"testUint64":0,"testUint32":0,"testUint16":0,"testUint8":0,"testFloat64":0,"testFloat32":0,"testBool":false} +` + // create our writer + w := &TestWriter{target: 100, mux: &sync.RWMutex{}} + enc := Stream.NewEncoder(w).LineDelimited() + w.enc = enc + s := StreamChanObject(make(chan *testObject)) + go enc.EncodeStream(s) + go feedStream(s, 100) + select { + case <-enc.Done(): + assert.Nil(t, enc.Err(), "enc.Err() should be nil") + assert.Len(t, w.result, 100, "w.result should be 100") + for _, b := range w.result { + assert.Equal(t, expectedStr, string(b), "every byte buffer should be equal to expected string") + } + } + }) + + t.Run("single-consumer-slice", func(t *testing.T) { + expectedStr := + `["test","test2"] +` + // create our writer + w := &TestWriter{target: 100, mux: &sync.RWMutex{}} + enc := Stream.NewEncoder(w).LineDelimited() + w.enc = enc + s := StreamChanSlice(make(chan *TestEncodingArrStrings)) + go enc.EncodeStream(s) + go feedStreamSlices(s, 100) + select { + case <-enc.Done(): + assert.Nil(t, enc.Err(), "enc.Err() should be nil") + assert.Len(t, w.result, 100, "w.result should be 100") + for _, b := range w.result { + assert.Equal(t, expectedStr, string(b), "every byte buffer should be equal to expected string") + } + } + }) + + t.Run("single-consumer-string", func(t *testing.T) { + expectedStr := + `"hello" +` + // create our writer + w := &TestWriter{target: 100, mux: &sync.RWMutex{}} + enc := Stream.NewEncoder(w).LineDelimited() + w.enc = enc + s := StreamChanString(make(chan string)) + go enc.EncodeStream(s) + go feedStreamStrings(s, 100) + select { + case <-enc.Done(): + assert.Nil(t, enc.Err(), "enc.Err() should be nil") + assert.Len(t, w.result, 100, "w.result should be 100") + for _, b := range w.result { + assert.Equal(t, expectedStr, string(b), "every byte buffer should be equal to expected string") + } + } + }) + + t.Run("single-consumer-object-nil-value", func(t *testing.T) { + expectedStr := `` + // create our writer + w := &TestWriter{target: 100, mux: &sync.RWMutex{}} + enc := Stream.NewEncoder(w).LineDelimited() + w.enc = enc + s := StreamChanObject(make(chan *testObject)) + go enc.EncodeStream(s) + go feedStreamNil(s, 100) + select { + case <-enc.Done(): + assert.Nil(t, enc.Err(), "enc.Err() should be nil") + assert.Nil(t, enc.Err(), "enc.Err() should not be nil") + for _, b := range w.result { + assert.Equal(t, expectedStr, string(b), "every byte buffer should be equal to expected string") + } + } + }) + + t.Run("single-consumer-int", func(t *testing.T) { + // create our writer + w := &TestWriter{target: 100, mux: &sync.RWMutex{}} + enc := Stream.NewEncoder(w).LineDelimited() + w.enc = enc + s := StreamChanInt(make(chan int)) + go enc.EncodeStream(s) + go feedStreamInt(s, 100) + select { + case <-enc.Done(): + assert.Nil(t, enc.Err(), "enc.Err() should be nil") + assert.Len(t, w.result, 100, "w.result should be 100") + } + }) + + t.Run("single-consumer-float", func(t *testing.T) { + // create our writer + w := &TestWriter{target: 100, mux: &sync.RWMutex{}} + enc := Stream.NewEncoder(w).LineDelimited() + w.enc = enc + s := StreamChanFloat(make(chan float64)) + go enc.EncodeStream(s) + go feedStreamFloat(s, 100) + select { + case <-enc.Done(): + assert.Nil(t, enc.Err(), "enc.Err() should be nil") + assert.Len(t, w.result, 100, "w.result should be 100") + } + }) + + t.Run("single-consumer-marshal-error", func(t *testing.T) { + // create our writer + w := &TestWriter{target: 100, mux: &sync.RWMutex{}} + enc := Stream.NewEncoder(w).LineDelimited() + w.enc = enc + s := StreamChanError(make(chan *testObject)) + go enc.EncodeStream(s) + go feedStream(s, 100) + select { + case <-enc.Done(): + assert.NotNil(t, enc.Err(), "enc.Err() should not be nil") + } + }) + + t.Run("single-consumer-write-error", func(t *testing.T) { + // create our writer + w := TestWriterError("") + enc := Stream.NewEncoder(w).LineDelimited() + s := StreamChanObject(make(chan *testObject)) + go enc.EncodeStream(s) + go feedStream(s, 100) + select { + case <-enc.Done(): + assert.NotNil(t, enc.Err(), "enc.Err() should not be nil") + } + }) + + t.Run("multiple-consumer-object-comma-delimited", func(t *testing.T) { + expectedStr := + `{"testStr":"","testInt":0,"testInt64":0,"testInt32":0,"testInt16":0,"testInt8":0,"testUint64":0,"testUint32":0,"testUint16":0,"testUint8":0,"testFloat64":0,"testFloat32":0,"testBool":false},` + // create our writer + w := &TestWriter{target: 5000, mux: &sync.RWMutex{}} + enc := Stream.BorrowEncoder(w).NConsumer(50).CommaDelimited() + w.enc = enc + s := StreamChanObject(make(chan *testObject)) + go enc.EncodeStream(s) + go feedStream(s, 5000) + select { + case <-enc.Done(): + assert.Nil(t, enc.Err(), "enc.Err() should be nil") + assert.Len(t, w.result, 5000, "w.result should be 100") + for _, b := range w.result { + assert.Equal(t, expectedStr, string(b), "every byte buffer should be equal to expected string") + } + } + }) + + t.Run("multiple-consumer-object-line-delimited", func(t *testing.T) { + expectedStr := + `{"testStr":"","testInt":0,"testInt64":0,"testInt32":0,"testInt16":0,"testInt8":0,"testUint64":0,"testUint32":0,"testUint16":0,"testUint8":0,"testFloat64":0,"testFloat32":0,"testBool":false} +` + // create our writer + w := &TestWriter{target: 5000, mux: &sync.RWMutex{}} + enc := Stream.NewEncoder(w).NConsumer(50).LineDelimited() + w.enc = enc + s := StreamChanObject(make(chan *testObject)) + go enc.EncodeStream(s) + go feedStream(s, 5000) + select { + case <-enc.Done(): + assert.Nil(t, enc.Err(), "enc.Err() should be nil") + assert.Len(t, w.result, 5000, "w.result should be 100") + for _, b := range w.result { + assert.Equal(t, expectedStr, string(b), "every byte buffer should be equal to expected string") + } + } + }) + + t.Run("encoder-deadline", func(t *testing.T) { + enc := Stream.NewEncoder(os.Stdout) + now := time.Now() + enc.SetDeadline(now) + d, _ := enc.Deadline() + assert.Equal(t, now, d, "deadline should be the one just set") + }) + + // just for coverage + t.Run("encoder-context-value", func(t *testing.T) { + enc := Stream.NewEncoder(os.Stdout) + assert.Nil(t, enc.Value(""), "enc.Value should be nil") + }) +} diff --git a/encode_string.go b/encode_string.go @@ -1,46 +1,79 @@ package gojay // EncodeString encodes a string to -func (enc *Encoder) EncodeString(s string) ([]byte, error) { +func (enc *Encoder) EncodeString(s string) error { if enc.isPooled == 1 { panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) } - return enc.encodeString(s) + _, _ = enc.encodeString(s) + _, err := enc.write() + if err != nil { + enc.err = err + return err + } + return nil } // encodeString encodes a string to -func (enc *Encoder) encodeString(s string) ([]byte, error) { +func (enc *Encoder) encodeString(v string) ([]byte, error) { enc.writeByte('"') - enc.writeString(s) + enc.writeString(v) enc.writeByte('"') return enc.buf, nil } // AddString adds a string to be encoded, must be used inside a slice or array encoding (does not encode a key) -func (enc *Encoder) AddString(value string) error { +func (enc *Encoder) AddString(v string) { r, ok := enc.getPreviousRune() if ok && r != '[' { enc.writeByte(',') } enc.writeByte('"') - enc.writeString(value) + enc.writeString(v) enc.writeByte('"') +} - return nil +// AddStringOmitEmpty adds a string to be encoded or skips it if it is zero value. +// Must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddStringOmitEmpty(v string) { + if v == "" { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(v) + enc.writeByte('"') } // AddStringKey adds a string to be encoded, must be used inside an object as it will encode a key -func (enc *Encoder) AddStringKey(key, value string) error { - // grow to avoid allocs (length of key/value + quotes) +func (enc *Encoder) AddStringKey(key, v string) { r, ok := enc.getPreviousRune() if ok && r != '{' && r != '[' { enc.writeByte(',') } enc.writeByte('"') enc.writeString(key) - enc.write(objKeyStr) - enc.writeString(value) + enc.writeBytes(objKeyStr) + enc.writeString(v) enc.writeByte('"') +} - return nil +// AddStringKeyOmitEmpty adds a string to be encoded or skips it if it is zero value. +// Must be used inside an object as it will encode a key +func (enc *Encoder) AddStringKeyOmitEmpty(key, v string) { + if v == "" { + return + } + r, ok := enc.getPreviousRune() + if ok && r != '{' && r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.writeString(key) + enc.writeBytes(objKeyStr) + enc.writeString(v) + enc.writeByte('"') } diff --git a/encode_string_test.go b/encode_string_test.go @@ -1,41 +1,78 @@ package gojay import ( + "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestEncoderString(t *testing.T) { - r, err := Marshal("string") - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `"string"`, - string(r), - "Result of marshalling is different as the one expected") +func TestEncoderStringEncodeAPI(t *testing.T) { + t.Run("basic", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + err := enc.EncodeString("hello world") + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `"hello world"`, + builder.String(), + "Result of marshalling is different as the one expected") + }) + t.Run("utf8", func(t *testing.T) { + builder := &strings.Builder{} + enc := NewEncoder(builder) + err := enc.EncodeString("漢字") + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `"漢字"`, + builder.String(), + "Result of marshalling is different as the one expected") + }) } -func TestEncoderStringUTF8(t *testing.T) { - r, err := Marshal("漢字") - assert.Nil(t, err, "Error should be nil") - assert.Equal( - t, - `"漢字"`, - string(r), - "Result of marshalling is different as the one expected") +func TestEncoderStringEncodeAPIErrors(t *testing.T) { + t.Run("pool-error", func(t *testing.T) { + v := "" + enc := BorrowEncoder(nil) + 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") + }) + t.Run("write-error", func(t *testing.T) { + v := "test" + w := TestWriterError("") + enc := BorrowEncoder(w) + defer enc.Release() + err := enc.EncodeString(v) + assert.NotNil(t, err, "err should not be nil") + }) } -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") +func TestEncoderStringMarshalAPI(t *testing.T) { + t.Run("basic", func(t *testing.T) { + r, err := Marshal("string") + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `"string"`, + string(r), + "Result of marshalling is different as the one expected") + }) + t.Run("utf8", func(t *testing.T) { + r, err := Marshal("漢字") + assert.Nil(t, err, "Error should be nil") + assert.Equal( + t, + `"漢字"`, + string(r), + "Result of marshalling is different as the one expected") + }) } diff --git a/encode_test.go b/encode_test.go @@ -1 +1,9 @@ package gojay + +import "errors" + +type TestWriterError string + +func (t TestWriterError) Write(b []byte) (int, error) { + return 0, errors.New("Test Error") +}