gojay

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

commit 9d59726d276a8fe33c254963e1a5fcc471c2addc
parent edeb6990b66b753afa4dfbf2bcdc639e36abf8f8
Author: francoispqt <francois@parquet.ninja>
Date:   Thu,  3 May 2018 22:43:41 +0800

add example for map decoding, add support for decoding all keys, add tests

Diffstat:
MREADME.md | 46++++++++++++++++++++++++++++++++++++++++++----
Mdecode_object.go | 53+++++++++++++++++++++++++++++++++++++++--------------
Mdecode_object_test.go | 83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Aexamples/encode-decode-map/main.go | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 251 insertions(+), 31 deletions(-)

diff --git a/README.md b/README.md @@ -168,7 +168,7 @@ func (dec *Decoder) DecodeString(v *string) error ``` -### Structs +### Structs and Maps #### UnmarshalerObject Interface To unmarshal a JSON object to a structure, the structure must implement the UnmarshalerObject interface: @@ -180,9 +180,9 @@ type UnmarshalerObject interface { ``` UnmarshalObject method takes two arguments, the first one is a pointer to the Decoder (*gojay.Decoder) and the second one is the string value of the current key being parsed. If the JSON data is not an object, the UnmarshalObject method will never be called. -NKeys method must return the number of keys to Unmarshal in the JSON object. +NKeys method must return the number of keys to Unmarshal in the JSON object or 0. If zero is returned, all keys will be parsed. -Example of implementation: +Example of implementation for a struct: ```go type user struct { id int @@ -206,6 +206,27 @@ func (u *user) NKeys() int { } ``` +Example of implementation for a `map[string]string`: +```go +// define our custom map type implementing UnmarshalerObject +type message map[string]string + +// Implementing Unmarshaler +func (m message) UnmarshalObject(dec *gojay.Decoder, k string) error { + str := "" + err := dec.AddString(&str) + if err != nil { + return err + } + m[k] = str + return nil +} + +// we return 0, it tells the Decoder to decode all keys +func (m myMap) NKeys() int { + return 0 +} +``` ### Arrays, Slices and Channels @@ -412,7 +433,7 @@ MarshalObject method takes one argument, a pointer to the Encoder (*gojay.Encode IsNil method returns a boolean indicating if the interface underlying value is nil or not. It is used to safely ensure that the underlying value is not nil without using Reflection. -Example of implementation: +Example of implementation for a struct: ```go type user struct { id int @@ -430,6 +451,23 @@ func (u *user) IsNil() bool { } ``` +Example of implementation for a `map[string]string`: +```go +// define our custom map type implementing MarshalerObject +type message map[string]string + +// Implementing Marshaler +func (m message) MarshalObject(enc *gojay.Encoder) { + for k, v := range m { + enc.AddStringKey(k, v) + } +} + +func (m message) IsNil() bool { + return m == nil +} +``` + ### Arrays and Slices To encode an array or a slice, the slice/array must implement the MarshalerArray interface: ```go diff --git a/decode_object.go b/decode_object.go @@ -24,25 +24,50 @@ func (dec *Decoder) decodeObject(j UnmarshalerObject) (int, error) { case ' ', '\n', '\t', '\r', ',': case '{': dec.cursor = dec.cursor + 1 - for (dec.cursor < dec.length || dec.read()) && dec.keysDone < keys { - k, done, err := dec.nextKey() - if err != nil { - return 0, err - } else if done { - return dec.cursor, nil + // if keys is zero we will parse all keys + // we run two loops for micro optimization + if keys == 0 { + for dec.cursor < dec.length || dec.read() { + k, done, err := dec.nextKey() + if err != nil { + return 0, err + } else if done { + return dec.cursor, nil + } + err = j.UnmarshalObject(dec, k) + if err != nil { + return 0, err + } else if dec.called&1 == 0 { + err := dec.skipData() + if err != nil { + return 0, err + } + } else { + dec.keysDone++ + } + dec.called &= 0 } - err = j.UnmarshalObject(dec, k) - if err != nil { - return 0, err - } else if dec.called&1 == 0 { - err := dec.skipData() + } else { + for (dec.cursor < dec.length || dec.read()) && dec.keysDone < keys { + k, done, err := dec.nextKey() + if err != nil { + return 0, err + } else if done { + return dec.cursor, nil + } + err = j.UnmarshalObject(dec, k) if err != nil { return 0, err + } else if dec.called&1 == 0 { + err := dec.skipData() + if err != nil { + return 0, err + } + } else { + dec.keysDone++ } - } else { - dec.keysDone++ + dec.called &= 0 } - dec.called &= 0 } // will get to that point when keysDone is not lower than keys anymore // in that case, we make sure cursor goes to the end of object, but we skip diff --git a/decode_object_test.go b/decode_object_test.go @@ -45,7 +45,7 @@ func (t *TestSubObj) UnmarshalObject(dec *Decoder, key string) error { } func (t *TestSubObj) NKeys() int { - return 1000 + return 0 } func (t *TestObj) UnmarshalObject(dec *Decoder, key string) error { @@ -274,17 +274,37 @@ func TestDecoderObjectInvalidJSON(t *testing.T) { assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") } -func TestDecoderObjectPoolError(t *testing.T) { - result := jsonDecodePartial{} - dec := NewDecoder(nil) - dec.Release() - defer func() { - err := recover() - assert.NotNil(t, err, "err shouldnot be nil") - assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledDecoderError") - }() - _ = dec.DecodeObject(&result) - assert.True(t, false, "should not be called as decoder should have panicked") +type myMap map[string]string + +func (m myMap) UnmarshalObject(dec *Decoder, k string) error { + str := "" + err := dec.AddString(&str) + if err != nil { + return err + } + m[k] = str + return nil +} + +// return 0 to parse all keys +func (m myMap) NKeys() int { + return 0 +} + +func TestDecoderObjectMap(t *testing.T) { + json := `{ + "test": "string", + "test2": "string", + "test3": "string", + "test4": "string", + "test5": "string", + }` + m := myMap(make(map[string]string)) + dec := BorrowDecoder(strings.NewReader(json)) + err := dec.Decode(m) + + assert.Nil(t, err, "err should be nil") + assert.Len(t, m, 5, "len of m should be 5") } func TestDecoderObjectDecoderAPI(t *testing.T) { @@ -334,10 +354,47 @@ func TestDecoderObjectDecoderAPI(t *testing.T) { assertResult(t, v, err) } -func TestDecoderObjectDecoderAPIError(t *testing.T) { +func TestDecoderObjectDecoderInvalidJSONError(t *testing.T) { + v := &TestObj{} + dec := NewDecoder(strings.NewReader(`{"err:}`)) + err := dec.DecodeObject(v) + assert.NotNil(t, err, "Err must not be nil as JSON is invalid") + assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") +} + +func TestDecoderObjectDecoderInvalidJSONError2(t *testing.T) { + v := &TestSubObj{} + dec := NewDecoder(strings.NewReader(`{"err:}`)) + err := dec.DecodeObject(v) + assert.NotNil(t, err, "Err must not be nil as JSON is invalid") + assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") +} + +func TestDecoderObjectDecoderInvalidJSONError3(t *testing.T) { + v := &TestSubObj{} + dec := NewDecoder(strings.NewReader(`{"err":"test}`)) + err := dec.DecodeObject(v) + assert.NotNil(t, err, "Err must not be nil as JSON is invalid") + assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") +} + +func TestDecoderObjectDecoderInvalidJSONError4(t *testing.T) { testArr := testSliceInts{} dec := NewDecoder(strings.NewReader(`hello`)) err := dec.DecodeArray(&testArr) assert.NotNil(t, err, "Err must not be nil as JSON is invalid") assert.IsType(t, InvalidJSONError(""), err, "err message must be 'Invalid JSON'") } + +func TestDecoderObjectPoolError(t *testing.T) { + result := jsonDecodePartial{} + dec := NewDecoder(nil) + dec.Release() + defer func() { + err := recover() + assert.NotNil(t, err, "err shouldnot be nil") + assert.IsType(t, InvalidUsagePooledDecoderError(""), err, "err should be of type InvalidUsagePooledDecoderError") + }() + _ = dec.DecodeObject(&result) + assert.True(t, false, "should not be called as decoder should have panicked") +} diff --git a/examples/encode-decode-map/main.go b/examples/encode-decode-map/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "log" + "strings" + + "github.com/francoispqt/gojay" +) + +// define our custom map type implementing MarshalerObject and UnmarshalerObject +type myMap map[string]string + +// Implementing Unmarshaler +func (m myMap) UnmarshalObject(dec *gojay.Decoder, k string) error { + str := "" + err := dec.AddString(&str) + if err != nil { + return err + } + m[k] = str + return nil +} + +// Her we put the number of keys +// If number of keys is unknown return 0, it will parse all keys +func (m myMap) NKeys() int { + return 0 +} + +// Implementing Marshaler +func (m myMap) MarshalObject(enc *gojay.Encoder) { + for k, v := range m { + enc.AddStringKey(k, v) + } +} + +func (m myMap) IsNil() bool { + return m == nil +} + +// Using Marshal / Unmarshal API +func marshalAPI(m myMap) error { + b, err := gojay.Marshal(m) + if err != nil { + return err + } + log.Print(string(b)) + + nM := myMap(make(map[string]string)) + err = gojay.Unmarshal(b, nM) + if err != nil { + return err + } + log.Print(nM) + return nil +} + +// Using Encode / Decode API +func encodeAPI(m myMap) error { + // we use strings.Builder as it implements io.Writer + builder := &strings.Builder{} + enc := gojay.BorrowEncoder(builder) + defer enc.Release() + // encode + err := enc.EncodeObject(m) + if err != nil { + return err + } + log.Print(builder.String()) + + // make our new map which will receive the decoded JSON + nM := myMap(make(map[string]string)) + // get our decoder with an io.Reader + dec := gojay.BorrowDecoder(strings.NewReader(builder.String())) + defer dec.Release() + // decode + err = dec.DecodeObject(nM) + if err != nil { + return err + } + log.Print(nM) + return nil +} + +func main() { + // make our map to be encoded + m := myMap(map[string]string{ + "test": "test", + "test2": "test2", + }) + + err := marshalAPI(m) + if err != nil { + log.Fatal(err) + } + err = encodeAPI(m) + if err != nil { + log.Fatal(err) + } +}