gojay

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

commit b18e4dd9c07535ad52434257da37c76b7858e996
parent 71c0966e1a7260957ced1f424f1bc2219207eda1
Author: francoispqt <francois@parquet.ninja>
Date:   Tue, 24 Jul 2018 23:21:52 +0800

add time type

Diffstat:
Mdecode.go | 16++++++++++++++++
Adecode_time.go | 36++++++++++++++++++++++++++++++++++++
Adecode_time_test.go | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_time.go | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencode_time_test.go | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 411 insertions(+), 0 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_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_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()) + } + }) + } +}