gojay

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

commit a2c156ad288ef79007855b4278be4954da00f5bf
parent f4c5a97b2ce16ce14e1d30875b7cecf1f1f4f7a8
Author: Francois Parquet <francois.parquet@gmail.com>
Date:   Sun, 12 Aug 2018 23:00:24 +0800

Merge pull request #59 from francoispqt/version/v1.2.0

Version/v1.2.0
Diffstat:
Mdecode.go | 16++++++++++++++++
Mdecode_array_test.go | 11+++++++++--
Mdecode_object_test.go | 23++++++++++++++---------
Adecode_sqlnull.go | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adecode_sqlnull_test.go | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdecode_string.go | 162+++++++++++++++++++++----------------------------------------------------------
Mdecode_string_test.go | 60+++++++++++++++++++++++++++++++++++++++---------------------
Adecode_time.go | 36++++++++++++++++++++++++++++++++++++
Adecode_time_test.go | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_sqlnull.go | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_sqlnull_test.go | 1121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_time.go | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_time_test.go | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 2182 insertions(+), 151 deletions(-)

diff --git a/decode.go b/decode.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "reflect" + "time" ) // UnmarshalJSONArray parses the JSON-encoded data and stores the result in the value pointed to by v. @@ -482,6 +483,21 @@ func (dec *Decoder) String(v *string) error { return nil } +// AddTime decodes the next key to a *time.Time with the given format +func (dec *Decoder) AddTime(v *time.Time, format string) error { + return dec.Time(v, format) +} + +// Time decodes the next key to a *time.Time with the given format +func (dec *Decoder) Time(v *time.Time, format string) error { + err := dec.decodeTime(v, format) + if err != nil { + return err + } + dec.called |= 1 + return nil +} + // Object decodes the next key to a UnmarshalerJSONObject. func (dec *Decoder) Object(value UnmarshalerJSONObject) error { initialKeysDone := dec.keysDone diff --git a/decode_array_test.go b/decode_array_test.go @@ -112,12 +112,12 @@ func TestSliceStrings(t *testing.T) { }, { name: "basic-test", - json: `["hello world", "hey" , "foo","bar \\n escape"]`, + json: `["hello world", "hey" , "foo","bar \n escape"]`, expectedResult: testSliceStrings{"hello world", "hey", "foo", "bar \n escape"}, }, { name: "basic-test", - json: `["hello world", "hey" , null,"bar \\n escape"]`, + json: `["hello world", "hey" , null,"bar \n escape"]`, expectedResult: testSliceStrings{"hello world", "hey", "", "bar \n escape"}, }, { @@ -534,6 +534,13 @@ func TestSkipArray(t *testing.T) { assert.Nil(t, err) }, }, + { + json: `"test \n"]`, + expectations: func(t *testing.T, i int, err error) { + assert.Equal(t, len(`"test \n"]`), i) + assert.Nil(t, err) + }, + }, } for _, test := range testCases { diff --git a/decode_object_test.go b/decode_object_test.go @@ -877,14 +877,14 @@ func TestDecodeObjectComplex(t *testing.T) { json: `{ "testSubObject": { "testStr": "some string", - "testInt":124465, - "testUint16":120, - "testUint8":15, - "testInt16":-135, + "testInt":124465, + "testUint16":120, + "testUint8":15, + "testInt16":-135, "testInt8":-23 }, "testSubSliceInts": [1,2,3,4,5], - "testStr": "some \\n string" + "testStr": "some \n string" }`, expectedResult: testObjectComplex{ testSubObject: &testObject{ @@ -902,7 +902,7 @@ func TestDecodeObjectComplex(t *testing.T) { }, { name: "complex-json-err", - json: `{"testSubObject":{"testStr":"some string,"testInt":124465,"testUint16":120, "testUint8":15,"testInt16":-135,"testInt8":-23},"testSubSliceInts":[1,2],"testStr":"some \\n string"}`, + json: `{"testSubObject":{"testStr":"some string,"testInt":124465,"testUint16":120, "testUint8":15,"testInt16":-135,"testInt8":-23},"testSubSliceInts":[1,2],"testStr":"some \n string"}`, expectedResult: testObjectComplex{ testSubObject: &testObject{}, }, @@ -1012,9 +1012,9 @@ func TestDecodeObjectNull(t *testing.T) { var jsonComplex = []byte(`{ "test": "{\"test\":\"1\",\"test1\":2}", - "test2\\n": "\\\\\\\\\\n", + "test2\n": "\\\\\\\\\n", "testArrSkip": ["testString with escaped \\\" quotes"], - "testSkipString": "skip \\ string with \\n escaped char \" ", + "testSkipString": "skip \\ string with \n escaped char \" ", "testSkipObject": { "testSkipSubObj": { "test": "test" @@ -1028,7 +1028,7 @@ var jsonComplex = []byte(`{ "testSkipBoolNull": null, "testSub": { "test": "{\"test\":\"1\",\"test1\":2}", - "test2\\n": "[1,2,3]", + "test2\n": "[1,2,3]", "test3": 1, "testObjSkip": { "test": "test string with escaped \" quotes" @@ -1382,6 +1382,11 @@ func TestSkipObject(t *testing.T) { json: `{"key":"value"`, err: true, }, + { + name: "basic-err2", + json: `"key":"value\n"}`, + err: false, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { diff --git a/decode_sqlnull.go b/decode_sqlnull.go @@ -0,0 +1,75 @@ +package gojay + +import "database/sql" + +// DecodeSQLNullString decodes a sql.NullString +func (dec *Decoder) DecodeSQLNullString(v *sql.NullString) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeSQLNullString(v) +} + +func (dec *Decoder) decodeSQLNullString(v *sql.NullString) error { + var str string + if err := dec.decodeString(&str); err != nil { + return err + } + v.String = str + v.Valid = true + return nil +} + +// DecodeSQLNullInt64 decodes a sql.NullInt64 +func (dec *Decoder) DecodeSQLNullInt64(v *sql.NullInt64) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeSQLNullInt64(v) +} + +func (dec *Decoder) decodeSQLNullInt64(v *sql.NullInt64) error { + var i int64 + if err := dec.decodeInt64(&i); err != nil { + return err + } + v.Int64 = i + v.Valid = true + return nil +} + +// DecodeSQLNullFloat64 decodes a sql.NullString with the given format +func (dec *Decoder) DecodeSQLNullFloat64(v *sql.NullFloat64) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeSQLNullFloat64(v) +} + +func (dec *Decoder) decodeSQLNullFloat64(v *sql.NullFloat64) error { + var i float64 + if err := dec.decodeFloat64(&i); err != nil { + return err + } + v.Float64 = i + v.Valid = true + return nil +} + +// DecodeSQLNullBool decodes a sql.NullString with the given format +func (dec *Decoder) DecodeSQLNullBool(v *sql.NullBool) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeSQLNullBool(v) +} + +func (dec *Decoder) decodeSQLNullBool(v *sql.NullBool) error { + var b bool + if err := dec.decodeBool(&b); err != nil { + return err + } + v.Bool = b + v.Valid = true + return nil +} diff --git a/decode_sqlnull_test.go b/decode_sqlnull_test.go @@ -0,0 +1,201 @@ +package gojay + +import ( + "database/sql" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeSQLNullString(t *testing.T) { + testCases := []struct { + name string + json string + expectedNullString sql.NullString + err bool + }{ + { + name: "basic", + json: `"test"`, + expectedNullString: sql.NullString{String: "test", Valid: true}, + }, + { + name: "basic", + json: `"test`, + expectedNullString: sql.NullString{String: "test", Valid: true}, + err: true, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + nullString := sql.NullString{} + dec := NewDecoder(strings.NewReader(testCase.json)) + err := dec.DecodeSQLNullString(&nullString) + if testCase.err { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedNullString, nullString) + } + }) + } + t.Run( + "should panic because decoder is pooled", + func(t *testing.T) { + dec := NewDecoder(nil) + dec.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnt be nil") + assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledDecoderError") + }() + _ = dec.DecodeSQLNullString(&sql.NullString{}) + assert.True(t, false, "should not be called as decoder should have panicked") + }, + ) +} + +func TestDecodeSQLNullInt64(t *testing.T) { + testCases := []struct { + name string + json string + expectedNullInt64 sql.NullInt64 + err bool + }{ + { + name: "basic", + json: `1`, + expectedNullInt64: sql.NullInt64{Int64: 1, Valid: true}, + }, + { + name: "basic", + json: `"test`, + expectedNullInt64: sql.NullInt64{Int64: 1, Valid: true}, + err: true, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + nullInt64 := sql.NullInt64{} + dec := NewDecoder(strings.NewReader(testCase.json)) + err := dec.DecodeSQLNullInt64(&nullInt64) + if testCase.err { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedNullInt64, nullInt64) + } + }) + } + t.Run( + "should panic because decoder is pooled", + func(t *testing.T) { + dec := NewDecoder(nil) + dec.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnt be nil") + assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledDecoderError") + }() + _ = dec.DecodeSQLNullInt64(&sql.NullInt64{}) + assert.True(t, false, "should not be called as decoder should have panicked") + }, + ) +} + +func TestDecodeSQLNullFloat64(t *testing.T) { + testCases := []struct { + name string + json string + expectedNullFloat64 sql.NullFloat64 + err bool + }{ + { + name: "basic", + json: `1`, + expectedNullFloat64: sql.NullFloat64{Float64: 1, Valid: true}, + }, + { + name: "basic", + json: `"test`, + expectedNullFloat64: sql.NullFloat64{Float64: 1, Valid: true}, + err: true, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + nullFloat64 := sql.NullFloat64{} + dec := NewDecoder(strings.NewReader(testCase.json)) + err := dec.DecodeSQLNullFloat64(&nullFloat64) + if testCase.err { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedNullFloat64, nullFloat64) + } + }) + } + t.Run( + "should panic because decoder is pooled", + func(t *testing.T) { + dec := NewDecoder(nil) + dec.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnt be nil") + assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledDecoderError") + }() + _ = dec.DecodeSQLNullFloat64(&sql.NullFloat64{}) + assert.True(t, false, "should not be called as decoder should have panicked") + }, + ) +} + +func TestDecodeSQLNullBool(t *testing.T) { + testCases := []struct { + name string + json string + expectedNullBool sql.NullBool + err bool + }{ + { + name: "basic", + json: `true`, + expectedNullBool: sql.NullBool{Bool: true, Valid: true}, + }, + { + name: "basic", + json: `"&`, + expectedNullBool: sql.NullBool{Bool: true, Valid: true}, + err: true, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + nullBool := sql.NullBool{} + dec := NewDecoder(strings.NewReader(testCase.json)) + err := dec.DecodeSQLNullBool(&nullBool) + if testCase.err { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedNullBool, nullBool) + } + }) + } + t.Run( + "should panic because decoder is pooled", + func(t *testing.T) { + dec := NewDecoder(nil) + dec.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnt be nil") + assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledDecoderError") + }() + _ = dec.DecodeSQLNullBool(&sql.NullBool{}) + assert.True(t, false, "should not be called as decoder should have panicked") + }, + ) +} diff --git a/decode_string.go b/decode_string.go @@ -52,127 +52,51 @@ func (dec *Decoder) decodeString(v *string) error { } func (dec *Decoder) parseEscapedString() error { - // know where to stop slash - start := dec.cursor - for ; dec.cursor < dec.length || dec.read(); dec.cursor++ { - if dec.data[dec.cursor] != '\\' { - d := dec.data[dec.cursor] - dec.cursor = dec.cursor + 1 - nSlash := dec.cursor - start - switch d { - case '"': - // nSlash must be odd - if nSlash&1 != 1 { - return dec.raiseInvalidJSONErr(dec.cursor) - } - diff := (nSlash - 1) >> 1 - dec.data = append(dec.data[:start+diff-1], dec.data[dec.cursor-1:]...) - dec.length = len(dec.data) - dec.cursor -= nSlash - diff - return nil - case 'u': - if nSlash&1 == 0 { - diff := nSlash >> 1 - dec.data = append(dec.data[:start+diff-1], dec.data[dec.cursor-1:]...) - dec.length = len(dec.data) - dec.cursor -= nSlash - diff - return nil - } - start := dec.cursor - 2 - ((nSlash - 1) >> 1) - str, err := dec.parseUnicode() - if err != nil { - dec.err = err - return err - } - diff := dec.cursor - start - dec.data = append(append(dec.data[:start], str...), dec.data[dec.cursor:]...) - dec.length = len(dec.data) - dec.cursor = dec.cursor - diff + len(str) - return nil - case 'b': - // number of slash must be even - // if is odd number of slashes - // divide nSlash - 1 by 2 and leave last one - // else divide nSlash by 2 and leave the letter - if nSlash&1 != 0 { - return dec.raiseInvalidJSONErr(dec.cursor) - } - var diff int - diff = nSlash >> 1 - dec.data = append(append(dec.data[:start+diff-2], '\b'), dec.data[dec.cursor:]...) - dec.length = len(dec.data) - dec.cursor -= nSlash - diff + 1 - return nil - case 'f': - // number of slash must be even - // if is odd number of slashes - // divide nSlash - 1 by 2 and leave last one - // else divide nSlash by 2 and leave the letter - if nSlash&1 != 0 { - return dec.raiseInvalidJSONErr(dec.cursor) - } - var diff int - diff = nSlash >> 1 - dec.data = append(append(dec.data[:start+diff-2], '\f'), dec.data[dec.cursor:]...) - dec.length = len(dec.data) - dec.cursor -= nSlash - diff + 1 - return nil - case 'n': - // number of slash must be even - // if is odd number of slashes - // divide nSlash - 1 by 2 and leave last one - // else divide nSlash by 2 and leave the letter - if nSlash&1 != 0 { - return dec.raiseInvalidJSONErr(dec.cursor) - } - var diff int - diff = nSlash >> 1 - dec.data = append(append(dec.data[:start+diff-2], '\n'), dec.data[dec.cursor:]...) - dec.length = len(dec.data) - dec.cursor -= nSlash - diff + 1 - return nil - case 'r': - // number of slash must be even - // if is odd number of slashes - // divide nSlash - 1 by 2 and leave last one - // else divide nSlash by 2 and leave the letter - if nSlash&1 != 0 { - return dec.raiseInvalidJSONErr(dec.cursor) - } - var diff int - diff = nSlash >> 1 - dec.data = append(append(dec.data[:start+diff-2], '\r'), dec.data[dec.cursor:]...) - dec.length = len(dec.data) - dec.cursor -= nSlash - diff + 1 - return nil - case 't': - // number of slash must be even - // if is odd number of slashes - // divide nSlash - 1 by 2 and leave last one - // else divide nSlash by 2 and leave the letter - if nSlash&1 != 0 { - return dec.raiseInvalidJSONErr(dec.cursor) - } - var diff int - diff = nSlash >> 1 - dec.data = append(append(dec.data[:start+diff-2], '\t'), dec.data[dec.cursor:]...) - dec.length = len(dec.data) - dec.cursor -= nSlash - diff + 1 - return nil - default: - // nSlash must be even - if nSlash&1 == 1 { - return dec.raiseInvalidJSONErr(dec.cursor) - } - diff := nSlash >> 1 - dec.data = append(dec.data[:start+diff-1], dec.data[dec.cursor-1:]...) - dec.length = len(dec.data) - dec.cursor -= (nSlash - diff) - return nil - } + if dec.cursor >= dec.length && !dec.read() { + return dec.raiseInvalidJSONErr(dec.cursor) + } + switch dec.data[dec.cursor] { + case '"': + dec.data[dec.cursor] = '"' + case '\\': + dec.data[dec.cursor] = '\\' + case '/': + dec.data[dec.cursor] = '/' + case 'b': + dec.data[dec.cursor] = '\b' + case 'f': + dec.data[dec.cursor] = '\f' + case 'n': + dec.data[dec.cursor] = '\n' + case 'r': + dec.data[dec.cursor] = '\r' + case 't': + dec.data[dec.cursor] = '\t' + case 'u': + start := dec.cursor + dec.cursor++ + str, err := dec.parseUnicode() + if err != nil { + return err } + diff := dec.cursor - start + dec.data = append(append(dec.data[:start-1], str...), dec.data[dec.cursor:]...) + dec.length = len(dec.data) + dec.cursor += len(str) - diff - 1 + + return nil + default: + return dec.raiseInvalidJSONErr(dec.cursor) } - return dec.raiseInvalidJSONErr(dec.cursor) + // Truncate the previous backslash character, and the + dec.data = append(dec.data[:dec.cursor-1], dec.data[dec.cursor:]...) + dec.length-- + + // Since we've lost a character, our dec.cursor offset is now + // 1 past the escaped character which is precisely where we + // want it. + + return nil } func (dec *Decoder) getString() (int, int, error) { diff --git a/decode_string_test.go b/decode_string_test.go @@ -44,56 +44,56 @@ func TestDecoderString(t *testing.T) { { name: "escape-control-char", json: `"\n"`, - expectedResult: "", - err: true, + expectedResult: "\n", + err: false, }, { name: "escape-control-char", json: `"\\n"`, - expectedResult: "\n", + expectedResult: `\n`, err: false, }, { name: "escape-control-char", json: `"\t"`, - expectedResult: "", - err: true, + expectedResult: "\t", + err: false, }, { name: "escape-control-char", json: `"\\t"`, - expectedResult: "\t", + expectedResult: `\t`, err: false, }, { name: "escape-control-char", json: `"\b"`, - expectedResult: "", - err: true, + expectedResult: "\b", + err: false, }, { name: "escape-control-char", json: `"\\b"`, - expectedResult: "\b", + expectedResult: `\b`, err: false, }, { name: "escape-control-char", json: `"\f"`, - expectedResult: "", - err: true, + expectedResult: "\f", + err: false, }, { name: "escape-control-char", json: `"\\f"`, - expectedResult: "\f", + expectedResult: `\f`, err: false, }, { name: "escape-control-char", json: `"\r"`, - expectedResult: "", - err: true, + expectedResult: "\r", + err: false, }, { name: "escape-control-char", @@ -102,9 +102,27 @@ func TestDecoderString(t *testing.T) { err: true, }, { + name: "escape-control-char-solidus", + json: `"\/"`, + expectedResult: "/", + err: false, + }, + { + name: "escape-control-char-solidus", + json: `"/"`, + expectedResult: "/", + err: false, + }, + { + name: "escape-control-char-solidus-escape-char", + json: `"\\/"`, + expectedResult: `\/`, + err: false, + }, + { name: "escape-control-char", json: `"\\r"`, - expectedResult: "\r", + expectedResult: `\r`, err: false, }, { @@ -228,31 +246,31 @@ func TestDecoderString(t *testing.T) { }, { name: "escape quote err2", - json: `"test string \\t escaped"`, + json: `"test string \t escaped"`, expectedResult: "test string \t escaped", err: false, }, { name: "escape quote err2", - json: `"test string \\r escaped"`, + json: `"test string \r escaped"`, expectedResult: "test string \r escaped", err: false, }, { name: "escape quote err2", - json: `"test string \\b escaped"`, + json: `"test string \b escaped"`, expectedResult: "test string \b escaped", err: false, }, { name: "escape quote err", - json: `"test string \\n escaped"`, + json: `"test string \n escaped"`, expectedResult: "test string \n escaped", err: false, }, { name: "escape quote err", - json: `"test string \\" escaped"`, + json: `"test string \\\" escaped`, expectedResult: ``, err: true, errType: InvalidJSONError(""), @@ -273,7 +291,7 @@ func TestDecoderString(t *testing.T) { }, { name: "string-complex", - json: ` "string with spaces and \"escape\"d \"quotes\" and escaped line returns \\n and escaped \\\\ escaped char"`, + json: ` "string with spaces and \"escape\"d \"quotes\" and escaped line returns \n and escaped \\\\ escaped char"`, expectedResult: "string with spaces and \"escape\"d \"quotes\" and escaped line returns \n and escaped \\\\ escaped char", }, } diff --git a/decode_time.go b/decode_time.go @@ -0,0 +1,36 @@ +package gojay + +import ( + "time" +) + +// DecodeTime decodes time with the given format +func (dec *Decoder) DecodeTime(v *time.Time, format string) error { + if dec.isPooled == 1 { + panic(InvalidUsagePooledDecoderError("Invalid usage of pooled decoder")) + } + return dec.decodeTime(v, format) +} + +func (dec *Decoder) decodeTime(v *time.Time, format string) error { + if format == time.RFC3339 { + var ej = make(EmbeddedJSON, 0, 20) + if err := dec.decodeEmbeddedJSON(&ej); err != nil { + return err + } + if err := v.UnmarshalJSON(ej); err != nil { + return err + } + return nil + } + var str string + if err := dec.decodeString(&str); err != nil { + return err + } + tt, err := time.Parse(format, str) + if err != nil { + return err + } + *v = tt + return nil +} diff --git a/decode_time_test.go b/decode_time_test.go @@ -0,0 +1,141 @@ +package gojay + +import ( + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeTime(t *testing.T) { + testCases := []struct { + name string + json string + format string + err bool + expectedTime string + }{ + { + name: "basic", + json: `"2018-02-18"`, + format: `2006-01-02`, + err: false, + expectedTime: "2018-02-18", + }, + { + name: "basic", + json: `"2017-01-02T15:04:05Z"`, + format: time.RFC3339, + err: false, + expectedTime: "2017-01-02T15:04:05Z", + }, + { + name: "basic", + json: `"2017-01-02T15:04:05ZINVALID"`, + format: time.RFC3339, + err: true, + expectedTime: "", + }, + { + name: "basic", + json: `"2017-01-02T15:04:05ZINVALID`, + format: time.RFC1123, + err: true, + expectedTime: "", + }, + { + name: "basic", + json: `"2017-01-02T15:04:05ZINVALID"`, + format: time.RFC1123, + err: true, + expectedTime: "", + }, + { + name: "basic", + json: `"2017-01-02T15:04:05ZINVALID`, + format: time.RFC3339, + err: true, + expectedTime: "", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + tm := time.Time{} + dec := NewDecoder(strings.NewReader(testCase.json)) + err := dec.DecodeTime(&tm, testCase.format) + if !testCase.err { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedTime, tm.Format(testCase.format)) + return + } + assert.NotNil(t, err) + }) + } +} + +func TestDecodeAddTime(t *testing.T) { + testCases := []struct { + name string + json string + format string + err bool + expectedTime string + }{ + { + name: "basic", + json: `"2018-02-18"`, + format: `2006-01-02`, + err: false, + expectedTime: "2018-02-18", + }, + { + name: "basic", + json: ` "2017-01-02T15:04:05Z"`, + format: time.RFC3339, + err: false, + expectedTime: "2017-01-02T15:04:05Z", + }, + { + name: "basic", + json: ` "2017-01-02T15:04:05ZINVALID"`, + format: time.RFC3339, + err: true, + expectedTime: "", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + tm := time.Time{} + dec := NewDecoder(strings.NewReader(testCase.json)) + err := dec.AddTime(&tm, testCase.format) + if !testCase.err { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedTime, tm.Format(testCase.format)) + return + } + assert.NotNil(t, err) + }) + } +} + +func TestDecoderTimePoolError(t *testing.T) { + // reset the pool to make sure it's not full + decPool = sync.Pool{ + New: func() interface{} { + return NewDecoder(nil) + }, + } + dec := NewDecoder(nil) + dec.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnt be nil") + assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledDecoderError") + }() + _ = dec.DecodeTime(&time.Time{}, time.RFC3339) + assert.True(t, false, "should not be called as decoder should have panicked") +} diff --git a/encode_sqlnull.go b/encode_sqlnull.go @@ -0,0 +1,269 @@ +package gojay + +import "database/sql" + +// EncodeSQLNullString encodes a string to +func (enc *Encoder) EncodeSQLNullString(v *sql.NullString) error { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + _, _ = enc.encodeString(v.String) + _, err := enc.Write() + if err != nil { + enc.err = err + return err + } + return nil +} + +// AddSQLNullString adds a string to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddSQLNullString(v *sql.NullString) { + enc.String(v.String) +} + +// AddSQLNullStringOmitEmpty 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) AddSQLNullStringOmitEmpty(v *sql.NullString) { + if v != nil && v.Valid && v.String != "" { + enc.StringOmitEmpty(v.String) + } +} + +// AddSQLNullStringKey adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddSQLNullStringKey(key string, v *sql.NullString) { + enc.StringKey(key, v.String) +} + +// AddSQLNullStringKeyOmitEmpty 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) AddSQLNullStringKeyOmitEmpty(key string, v *sql.NullString) { + if v != nil && v.Valid && v.String != "" { + enc.StringKeyOmitEmpty(key, v.String) + } +} + +// SQLNullString adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullString(v *sql.NullString) { + enc.String(v.String) +} + +// SQLNullStringOmitEmpty adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullStringOmitEmpty(v *sql.NullString) { + if v != nil && v.Valid && v.String != "" { + enc.String(v.String) + } +} + +// SQLNullStringKey adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullStringKey(key string, v *sql.NullString) { + enc.StringKey(key, v.String) +} + +// SQLNullStringKeyOmitEmpty 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) SQLNullStringKeyOmitEmpty(key string, v *sql.NullString) { + if v != nil && v.Valid && v.String != "" { + enc.StringKeyOmitEmpty(key, v.String) + } +} + +// NullInt64 + +// EncodeSQLNullInt64 encodes a string to +func (enc *Encoder) EncodeSQLNullInt64(v *sql.NullInt64) error { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + _, _ = enc.encodeInt64(v.Int64) + _, err := enc.Write() + if err != nil { + enc.err = err + return err + } + return nil +} + +// AddSQLNullInt64 adds a string to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddSQLNullInt64(v *sql.NullInt64) { + enc.Int64(v.Int64) +} + +// AddSQLNullInt64OmitEmpty 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) AddSQLNullInt64OmitEmpty(v *sql.NullInt64) { + if v != nil && v.Valid && v.Int64 != 0 { + enc.Int64OmitEmpty(v.Int64) + } +} + +// AddSQLNullInt64Key adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddSQLNullInt64Key(key string, v *sql.NullInt64) { + enc.Int64Key(key, v.Int64) +} + +// AddSQLNullInt64KeyOmitEmpty 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) AddSQLNullInt64KeyOmitEmpty(key string, v *sql.NullInt64) { + if v != nil && v.Valid && v.Int64 != 0 { + enc.Int64KeyOmitEmpty(key, v.Int64) + } +} + +// SQLNullInt64 adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullInt64(v *sql.NullInt64) { + enc.Int64(v.Int64) +} + +// SQLNullInt64OmitEmpty adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullInt64OmitEmpty(v *sql.NullInt64) { + if v != nil && v.Valid && v.Int64 != 0 { + enc.Int64(v.Int64) + } +} + +// SQLNullInt64Key adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullInt64Key(key string, v *sql.NullInt64) { + enc.Int64Key(key, v.Int64) +} + +// SQLNullInt64KeyOmitEmpty 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) SQLNullInt64KeyOmitEmpty(key string, v *sql.NullInt64) { + if v != nil && v.Valid && v.Int64 != 0 { + enc.Int64KeyOmitEmpty(key, v.Int64) + } +} + +// NullFloat64 + +// EncodeSQLNullFloat64 encodes a string to +func (enc *Encoder) EncodeSQLNullFloat64(v *sql.NullFloat64) error { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + _, _ = enc.encodeFloat(v.Float64) + _, err := enc.Write() + if err != nil { + enc.err = err + return err + } + return nil +} + +// AddSQLNullFloat64 adds a string to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddSQLNullFloat64(v *sql.NullFloat64) { + enc.Float64(v.Float64) +} + +// AddSQLNullFloat64OmitEmpty 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) AddSQLNullFloat64OmitEmpty(v *sql.NullFloat64) { + if v != nil && v.Valid && v.Float64 != 0 { + enc.Float64OmitEmpty(v.Float64) + } +} + +// AddSQLNullFloat64Key adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddSQLNullFloat64Key(key string, v *sql.NullFloat64) { + enc.Float64Key(key, v.Float64) +} + +// AddSQLNullFloat64KeyOmitEmpty 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) AddSQLNullFloat64KeyOmitEmpty(key string, v *sql.NullFloat64) { + if v != nil && v.Valid && v.Float64 != 0 { + enc.Float64KeyOmitEmpty(key, v.Float64) + } +} + +// SQLNullFloat64 adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullFloat64(v *sql.NullFloat64) { + enc.Float64(v.Float64) +} + +// SQLNullFloat64OmitEmpty adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullFloat64OmitEmpty(v *sql.NullFloat64) { + if v != nil && v.Valid && v.Float64 != 0 { + enc.Float64(v.Float64) + } +} + +// SQLNullFloat64Key adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullFloat64Key(key string, v *sql.NullFloat64) { + enc.Float64Key(key, v.Float64) +} + +// SQLNullFloat64KeyOmitEmpty 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) SQLNullFloat64KeyOmitEmpty(key string, v *sql.NullFloat64) { + if v != nil && v.Valid && v.Float64 != 0 { + enc.Float64KeyOmitEmpty(key, v.Float64) + } +} + +// NullBool + +// EncodeSQLNullBool encodes a string to +func (enc *Encoder) EncodeSQLNullBool(v *sql.NullBool) error { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + _, _ = enc.encodeBool(v.Bool) + _, err := enc.Write() + if err != nil { + enc.err = err + return err + } + return nil +} + +// AddSQLNullBool adds a string to be encoded, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddSQLNullBool(v *sql.NullBool) { + enc.Bool(v.Bool) +} + +// AddSQLNullBoolOmitEmpty 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) AddSQLNullBoolOmitEmpty(v *sql.NullBool) { + if v != nil && v.Valid && v.Bool != false { + enc.BoolOmitEmpty(v.Bool) + } +} + +// AddSQLNullBoolKey adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) AddSQLNullBoolKey(key string, v *sql.NullBool) { + enc.BoolKey(key, v.Bool) +} + +// AddSQLNullBoolKeyOmitEmpty 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) AddSQLNullBoolKeyOmitEmpty(key string, v *sql.NullBool) { + if v != nil && v.Valid && v.Bool != false { + enc.BoolKeyOmitEmpty(key, v.Bool) + } +} + +// SQLNullBool adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullBool(v *sql.NullBool) { + enc.Bool(v.Bool) +} + +// SQLNullBoolOmitEmpty adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullBoolOmitEmpty(v *sql.NullBool) { + if v != nil && v.Valid && v.Bool != false { + enc.Bool(v.Bool) + } +} + +// SQLNullBoolKey adds a string to be encoded, must be used inside an object as it will encode a key +func (enc *Encoder) SQLNullBoolKey(key string, v *sql.NullBool) { + enc.BoolKey(key, v.Bool) +} + +// SQLNullBoolKeyOmitEmpty 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) SQLNullBoolKeyOmitEmpty(key string, v *sql.NullBool) { + if v != nil && v.Valid && v.Bool != false { + enc.BoolKeyOmitEmpty(key, v.Bool) + } +} diff --git a/encode_sqlnull_test.go b/encode_sqlnull_test.go @@ -0,0 +1,1121 @@ +package gojay + +import ( + "database/sql" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Null String +func TestEncoceSQLNullString(t *testing.T) { + testCases := []struct { + name string + sqlNullString sql.NullString + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullString: sql.NullString{ + String: "foo bar", + }, + expectedResult: `"foo bar"`, + }, + { + name: "it should return an err as the string is invalid", + sqlNullString: sql.NullString{ + String: "foo \t bar", + }, + expectedResult: `"foo \t bar"`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + err := enc.EncodeSQLNullString(&testCase.sqlNullString) + if testCase.err { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedResult, b.String()) + } + }) + } + + t.Run( + "should panic as the encoder is pooled", + 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.EncodeSQLNullString(&sql.NullString{}) + assert.True(t, false, "should not be called as encoder should have panicked") + }, + ) + + t.Run( + "should return an error as the writer encounters an error", + func(t *testing.T) { + builder := TestWriterError("") + enc := NewEncoder(builder) + err := enc.EncodeSQLNullString(&sql.NullString{}) + assert.NotNil(t, err) + }, + ) +} + +func TestAddSQLNullStringKey(t *testing.T) { + t.Run( + "AddSQLNullStringKey", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullString sql.NullString + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullString: sql.NullString{ + String: "foo bar", + }, + baseJSON: "{", + expectedResult: `{"foo":"foo bar"`, + }, + { + name: "it should encode a null string", + sqlNullString: sql.NullString{ + String: "foo \t bar", + }, + baseJSON: "{", + expectedResult: `{"foo":"foo \t bar"`, + }, + { + name: "it should encode a null string", + sqlNullString: sql.NullString{ + String: "foo \t bar", + }, + baseJSON: "{", + expectedResult: `{"foo":"foo \t bar"`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullStringKey("foo", &testCase.sqlNullString) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullStringKey("foo", &testCase.sqlNullString) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) + t.Run( + "AddSQLNullStringKeyOmitEmpty, is should encode a sql.NullString", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullString sql.NullString + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullString: sql.NullString{ + String: "foo bar", + Valid: true, + }, + baseJSON: "{", + expectedResult: `{"foo":"foo bar"`, + }, + { + name: "it should not encode anything as null string is invalid", + sqlNullString: sql.NullString{ + String: "foo \t bar", + Valid: false, + }, + baseJSON: "{", + expectedResult: `{`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullStringKeyOmitEmpty("foo", &testCase.sqlNullString) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullStringKeyOmitEmpty("foo", &testCase.sqlNullString) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) +} + +func TestAddSQLNullString(t *testing.T) { + t.Run( + "AddSQLNullString", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullString sql.NullString + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullString: sql.NullString{ + String: "foo bar", + }, + baseJSON: "[", + expectedResult: `["foo bar"`, + }, + { + name: "it should encode a null string", + sqlNullString: sql.NullString{ + String: "foo \t bar", + }, + baseJSON: "[", + expectedResult: `["foo \t bar"`, + }, + { + name: "it should encode a null string", + sqlNullString: sql.NullString{ + String: "foo \t bar", + }, + baseJSON: "[", + expectedResult: `["foo \t bar"`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullString(&testCase.sqlNullString) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullString(&testCase.sqlNullString) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) + t.Run( + "AddSQLNullStringKeyOmitEmpty, is should encode a sql.NullString", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullString sql.NullString + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullString: sql.NullString{ + String: "foo bar", + Valid: true, + }, + baseJSON: "[", + expectedResult: `["foo bar"`, + }, + { + name: "it should not encode anything as null string is invalid", + sqlNullString: sql.NullString{ + String: "foo \t bar", + Valid: false, + }, + baseJSON: "[", + expectedResult: `[`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullStringOmitEmpty(&testCase.sqlNullString) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullStringOmitEmpty(&testCase.sqlNullString) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) +} + +// NullInt64 +func TestEncoceSQLNullInt64(t *testing.T) { + testCases := []struct { + name string + sqlNullInt64 sql.NullInt64 + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullInt64: sql.NullInt64{ + Int64: int64(1), + }, + expectedResult: `1`, + }, + { + name: "it should return an err as the string is invalid", + sqlNullInt64: sql.NullInt64{ + Int64: int64(2), + }, + expectedResult: `2`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + err := enc.EncodeSQLNullInt64(&testCase.sqlNullInt64) + if testCase.err { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedResult, b.String()) + } + }) + } + t.Run( + "should panic as the encoder is pooled", + 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.EncodeSQLNullInt64(&sql.NullInt64{}) + assert.True(t, false, "should not be called as encoder should have panicked") + }, + ) + t.Run( + "should return an error as the writer encounters an error", + func(t *testing.T) { + builder := TestWriterError("") + enc := NewEncoder(builder) + err := enc.EncodeSQLNullInt64(&sql.NullInt64{}) + assert.NotNil(t, err) + }, + ) +} + +func TestAddSQLNullInt64Key(t *testing.T) { + t.Run( + "AddSQLNullInt64Key", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullInt64 sql.NullInt64 + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullInt64: sql.NullInt64{ + Int64: 1, + }, + baseJSON: "{", + expectedResult: `{"foo":1`, + }, + { + name: "it should encode a null string", + sqlNullInt64: sql.NullInt64{ + Int64: 2, + }, + baseJSON: "{", + expectedResult: `{"foo":2`, + }, + { + name: "it should encode a null string", + sqlNullInt64: sql.NullInt64{ + Int64: 2, + }, + baseJSON: "{", + expectedResult: `{"foo":2`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullInt64Key("foo", &testCase.sqlNullInt64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullInt64Key("foo", &testCase.sqlNullInt64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) + t.Run( + "AddSQLNullInt64KeyOmitEmpty, is should encode a sql.NullInt64", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullInt64 sql.NullInt64 + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullInt64: sql.NullInt64{ + Int64: 1, + Valid: true, + }, + baseJSON: "{", + expectedResult: `{"foo":1`, + }, + { + name: "it should not encode anything as null string is invalid", + sqlNullInt64: sql.NullInt64{ + Int64: 2, + Valid: false, + }, + baseJSON: "{", + expectedResult: `{`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullInt64KeyOmitEmpty("foo", &testCase.sqlNullInt64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullInt64KeyOmitEmpty("foo", &testCase.sqlNullInt64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) +} + +func TestAddSQLNullInt64(t *testing.T) { + t.Run( + "AddSQLNullInt64", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullInt64 sql.NullInt64 + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullInt64: sql.NullInt64{ + Int64: 1, + }, + baseJSON: "[", + expectedResult: `[1`, + }, + { + name: "it should encode a null string", + sqlNullInt64: sql.NullInt64{ + Int64: 2, + }, + baseJSON: "[", + expectedResult: `[2`, + }, + { + name: "it should encode a null string", + sqlNullInt64: sql.NullInt64{ + Int64: 2, + }, + baseJSON: "[", + expectedResult: `[2`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullInt64(&testCase.sqlNullInt64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullInt64(&testCase.sqlNullInt64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) + t.Run( + "AddSQLNullInt64KeyOmitEmpty, is should encode a sql.NullInt64", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullInt64 sql.NullInt64 + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullInt64: sql.NullInt64{ + Int64: 2, + Valid: true, + }, + baseJSON: "[", + expectedResult: `[2`, + }, + { + name: "it should not encode anything as null string is invalid", + sqlNullInt64: sql.NullInt64{ + Int64: 2, + Valid: false, + }, + baseJSON: "[", + expectedResult: `[`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullInt64OmitEmpty(&testCase.sqlNullInt64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullInt64OmitEmpty(&testCase.sqlNullInt64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) +} + +// NullFloat64 +func TestEncoceSQLNullFloat64(t *testing.T) { + testCases := []struct { + name string + sqlNullFloat64 sql.NullFloat64 + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullFloat64: sql.NullFloat64{ + Float64: float64(1), + }, + expectedResult: `1`, + }, + { + name: "it should return an err as the string is invalid", + sqlNullFloat64: sql.NullFloat64{ + Float64: float64(2), + }, + expectedResult: `2`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + err := enc.EncodeSQLNullFloat64(&testCase.sqlNullFloat64) + if testCase.err { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedResult, b.String()) + } + }) + } + t.Run( + "should panic as the encoder is pooled", + 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.EncodeSQLNullFloat64(&sql.NullFloat64{}) + assert.True(t, false, "should not be called as encoder should have panicked") + }, + ) + + t.Run( + "should return an error as the writer encounters an error", + func(t *testing.T) { + builder := TestWriterError("") + enc := NewEncoder(builder) + err := enc.EncodeSQLNullFloat64(&sql.NullFloat64{}) + assert.NotNil(t, err) + }, + ) +} + +func TestAddSQLNullFloat64Key(t *testing.T) { + t.Run( + "AddSQLNullFloat64Key", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullFloat64 sql.NullFloat64 + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullFloat64: sql.NullFloat64{ + Float64: 1, + }, + baseJSON: "{", + expectedResult: `{"foo":1`, + }, + { + name: "it should encode a null string", + sqlNullFloat64: sql.NullFloat64{ + Float64: 2, + }, + baseJSON: "{", + expectedResult: `{"foo":2`, + }, + { + name: "it should encode a null string", + sqlNullFloat64: sql.NullFloat64{ + Float64: 2, + }, + baseJSON: "{", + expectedResult: `{"foo":2`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullFloat64Key("foo", &testCase.sqlNullFloat64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullFloat64Key("foo", &testCase.sqlNullFloat64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) + t.Run( + "AddSQLNullFloat64KeyOmitEmpty, is should encode a sql.NullFloat64", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullFloat64 sql.NullFloat64 + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullFloat64: sql.NullFloat64{ + Float64: 1, + Valid: true, + }, + baseJSON: "{", + expectedResult: `{"foo":1`, + }, + { + name: "it should not encode anything as null string is invalid", + sqlNullFloat64: sql.NullFloat64{ + Float64: 2, + Valid: false, + }, + baseJSON: "{", + expectedResult: `{`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullFloat64KeyOmitEmpty("foo", &testCase.sqlNullFloat64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullFloat64KeyOmitEmpty("foo", &testCase.sqlNullFloat64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) +} + +func TestAddSQLNullFloat64(t *testing.T) { + t.Run( + "AddSQLNullFloat64", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullFloat64 sql.NullFloat64 + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullFloat64: sql.NullFloat64{ + Float64: 1, + }, + baseJSON: "[", + expectedResult: `[1`, + }, + { + name: "it should encode a null string", + sqlNullFloat64: sql.NullFloat64{ + Float64: 2, + }, + baseJSON: "[", + expectedResult: `[2`, + }, + { + name: "it should encode a null string", + sqlNullFloat64: sql.NullFloat64{ + Float64: 2, + }, + baseJSON: "[", + expectedResult: `[2`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullFloat64(&testCase.sqlNullFloat64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullFloat64(&testCase.sqlNullFloat64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) + t.Run( + "AddSQLNullFloat64KeyOmitEmpty, is should encode a sql.NullFloat64", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullFloat64 sql.NullFloat64 + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullFloat64: sql.NullFloat64{ + Float64: 2, + Valid: true, + }, + baseJSON: "[", + expectedResult: `[2`, + }, + { + name: "it should not encode anything as null string is invalid", + sqlNullFloat64: sql.NullFloat64{ + Float64: 2, + Valid: false, + }, + baseJSON: "[", + expectedResult: `[`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullFloat64OmitEmpty(&testCase.sqlNullFloat64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullFloat64OmitEmpty(&testCase.sqlNullFloat64) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) +} + +// NullBool +func TestEncoceSQLNullBool(t *testing.T) { + testCases := []struct { + name string + sqlNullBool sql.NullBool + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullBool: sql.NullBool{ + Bool: true, + }, + expectedResult: `true`, + }, + { + name: "it should return an err as the string is invalid", + sqlNullBool: sql.NullBool{ + Bool: false, + }, + expectedResult: `false`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + err := enc.EncodeSQLNullBool(&testCase.sqlNullBool) + if testCase.err { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedResult, b.String()) + } + }) + } + t.Run( + "should panic as the encoder is pooled", + 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.EncodeSQLNullBool(&sql.NullBool{}) + assert.True(t, false, "should not be called as encoder should have panicked") + }, + ) + + t.Run( + "should return an error as the writer encounters an error", + func(t *testing.T) { + builder := TestWriterError("") + enc := NewEncoder(builder) + err := enc.EncodeSQLNullBool(&sql.NullBool{}) + assert.NotNil(t, err) + }, + ) +} + +func TestAddSQLNullBoolKey(t *testing.T) { + t.Run( + "AddSQLNullBoolKey", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullBool sql.NullBool + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullBool: sql.NullBool{ + Bool: true, + }, + baseJSON: "{", + expectedResult: `{"foo":true`, + }, + { + name: "it should encode a null string", + sqlNullBool: sql.NullBool{ + Bool: false, + }, + baseJSON: "{", + expectedResult: `{"foo":false`, + }, + { + name: "it should encode a null string", + sqlNullBool: sql.NullBool{ + Bool: true, + }, + baseJSON: "{", + expectedResult: `{"foo":true`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullBoolKey("foo", &testCase.sqlNullBool) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullBoolKey("foo", &testCase.sqlNullBool) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) + t.Run( + "AddSQLNullBoolKeyOmitEmpty, is should encode a sql.NullBool", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullBool sql.NullBool + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullBool: sql.NullBool{ + Bool: true, + Valid: true, + }, + baseJSON: "{", + expectedResult: `{"foo":true`, + }, + { + name: "it should not encode anything as null string is invalid", + sqlNullBool: sql.NullBool{ + Bool: true, + Valid: false, + }, + baseJSON: "{", + expectedResult: `{`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullBoolKeyOmitEmpty("foo", &testCase.sqlNullBool) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullBoolKeyOmitEmpty("foo", &testCase.sqlNullBool) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) +} + +func TestAddSQLNullBool(t *testing.T) { + t.Run( + "AddSQLNullBool", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullBool sql.NullBool + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullBool: sql.NullBool{ + Bool: true, + }, + baseJSON: "[", + expectedResult: `[true`, + }, + { + name: "it should encode a null string", + sqlNullBool: sql.NullBool{ + Bool: true, + }, + baseJSON: "[", + expectedResult: `[true`, + }, + { + name: "it should encode a null string", + sqlNullBool: sql.NullBool{ + Bool: false, + }, + baseJSON: "[", + expectedResult: `[false`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullBool(&testCase.sqlNullBool) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullBool(&testCase.sqlNullBool) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) + t.Run( + "AddSQLNullBoolKeyOmitEmpty, is should encode a sql.NullBool", + func(t *testing.T) { + testCases := []struct { + name string + sqlNullBool sql.NullBool + baseJSON string + expectedResult string + err bool + }{ + { + name: "it should encode a null string", + sqlNullBool: sql.NullBool{ + Bool: true, + Valid: true, + }, + baseJSON: "[", + expectedResult: `[true`, + }, + { + name: "it should not encode anything as null string is invalid", + sqlNullBool: sql.NullBool{ + Bool: true, + Valid: false, + }, + baseJSON: "[", + expectedResult: `[`, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var b strings.Builder + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddSQLNullBoolOmitEmpty(&testCase.sqlNullBool) + enc.Write() + assert.Equal(t, testCase.expectedResult, b.String()) + + var b2 strings.Builder + enc = NewEncoder(&b2) + enc.writeString(testCase.baseJSON) + enc.SQLNullBoolOmitEmpty(&testCase.sqlNullBool) + enc.Write() + assert.Equal(t, testCase.expectedResult, b2.String()) + }) + } + }, + ) +} diff --git a/encode_time.go b/encode_time.go @@ -0,0 +1,62 @@ +package gojay + +import ( + "time" +) + +func (enc *Encoder) EncodeTime(t *time.Time, format string) error { + if enc.isPooled == 1 { + panic(InvalidUsagePooledEncoderError("Invalid usage of pooled encoder")) + } + _, _ = enc.encodeTime(t, format) + _, err := enc.Write() + if err != nil { + return err + } + return nil +} + +// encodeInt encodes an int to JSON +func (enc *Encoder) encodeTime(t *time.Time, format string) ([]byte, error) { + enc.writeByte('"') + enc.buf = t.AppendFormat(enc.buf, format) + enc.writeByte('"') + return enc.buf, nil +} + +// AddTimeKey adds an *time.Time to be encoded with the given format, must be used inside an object as it will encode a key +func (enc *Encoder) AddTimeKey(key string, t *time.Time, format string) { + enc.TimeKey(key, t, format) +} + +// TimeKey adds an *time.Time to be encoded with the given format, must be used inside an object as it will encode a key +func (enc *Encoder) TimeKey(key string, t *time.Time, format string) { + enc.grow(10 + len(key)) + r := enc.getPreviousRune() + if r != '{' { + enc.writeTwoBytes(',', '"') + } else { + enc.writeByte('"') + } + enc.writeStringEscape(key) + enc.writeBytes(objKeyStr) + enc.buf = t.AppendFormat(enc.buf, format) + enc.writeByte('"') +} + +// AddTime adds an *time.Time to be encoded with the given format, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) AddTime(t *time.Time, format string) { + enc.Time(t, format) +} + +// Time adds an *time.Time to be encoded with the given format, must be used inside a slice or array encoding (does not encode a key) +func (enc *Encoder) Time(t *time.Time, format string) { + enc.grow(10) + r := enc.getPreviousRune() + if r != '[' { + enc.writeByte(',') + } + enc.writeByte('"') + enc.buf = t.AppendFormat(enc.buf, format) + enc.writeByte('"') +} diff --git a/encode_time_test.go b/encode_time_test.go @@ -0,0 +1,156 @@ +package gojay + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestEncodeTime(t *testing.T) { + testCases := []struct { + name string + tt string + format string + expectedJSON string + err bool + }{ + { + name: "basic", + tt: "2018-02-01", + format: "2006-01-02", + expectedJSON: `"2018-02-01"`, + err: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + b := strings.Builder{} + tt, err := time.Parse(testCase.format, testCase.tt) + assert.Nil(t, err) + enc := NewEncoder(&b) + err = enc.EncodeTime(&tt, testCase.format) + if !testCase.err { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedJSON, b.String()) + } + }) + } + t.Run("encode-time-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.EncodeTime(&time.Time{}, "") + assert.True(t, false, "should not be called as encoder should have panicked") + }) + t.Run("write-error", func(t *testing.T) { + w := TestWriterError("") + enc := BorrowEncoder(w) + defer enc.Release() + err := enc.EncodeTime(&time.Time{}, "") + assert.NotNil(t, err, "err should not be nil") + }) +} + +func TestAddTimeKey(t *testing.T) { + testCases := []struct { + name string + tt string + format string + expectedJSON string + baseJSON string + err bool + }{ + { + name: "basic", + tt: "2018-02-01", + format: "2006-01-02", + baseJSON: "{", + expectedJSON: `{"test":"2018-02-01"`, + err: false, + }, + { + name: "basic", + tt: "2018-02-01", + format: "2006-01-02", + baseJSON: `{""`, + expectedJSON: `{"","test":"2018-02-01"`, + err: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + b := strings.Builder{} + tt, err := time.Parse(testCase.format, testCase.tt) + assert.Nil(t, err) + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddTimeKey("test", &tt, testCase.format) + enc.Write() + if !testCase.err { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedJSON, b.String()) + } + }) + } +} + +func TestAddTime(t *testing.T) { + testCases := []struct { + name string + tt string + format string + expectedJSON string + baseJSON string + err bool + }{ + { + name: "basic", + tt: "2018-02-01", + format: "2006-01-02", + baseJSON: "[", + expectedJSON: `["2018-02-01"`, + err: false, + }, + { + name: "basic", + tt: "2018-02-01", + format: "2006-01-02", + baseJSON: "[", + expectedJSON: `["2018-02-01"`, + err: false, + }, + { + name: "basic", + tt: "2018-02-01", + format: "2006-01-02", + baseJSON: `[""`, + expectedJSON: `["","2018-02-01"`, + err: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + b := strings.Builder{} + tt, err := time.Parse(testCase.format, testCase.tt) + assert.Nil(t, err) + enc := NewEncoder(&b) + enc.writeString(testCase.baseJSON) + enc.AddTime(&tt, testCase.format) + enc.Write() + if !testCase.err { + assert.Nil(t, err) + assert.Equal(t, testCase.expectedJSON, b.String()) + } + }) + } +}