commit 589f634deb0ce9ec0694c863f938284dccce24db
parent edeb6990b66b753afa4dfbf2bcdc639e36abf8f8
Author: Francois Parquet <francois.parquet@gmail.com>
Date: Thu, 3 May 2018 22:58:49 +0800
Merge pull request #16 from francoispqt/feature/add-zero-nkeys
Add support for decoding all keys and examples for map decoding
Diffstat:
5 files changed, 309 insertions(+), 34 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
@@ -399,7 +420,7 @@ func (enc *Encoder) EncodeBool(v bool) error
func (enc *Encoder) EncodeString(s string) error
```
-### Structs
+### Structs and Maps
To encode a structure, the structure must implement the MarshalerObject interface:
```go
@@ -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_number_test.go b/decode_number_test.go
@@ -89,7 +89,7 @@ func TestDecoderIntPoolError(t *testing.T) {
_ = dec.DecodeInt(&result)
assert.True(t, false, "should not be called as decoder should have panicked")
}
-func TestDecoderInttDecoderAPI(t *testing.T) {
+func TestDecoderIntDecoderAPI(t *testing.T) {
var v int
dec := NewDecoder(strings.NewReader(`33`))
defer dec.Release()
@@ -98,6 +98,15 @@ func TestDecoderInttDecoderAPI(t *testing.T) {
assert.Equal(t, int(33), v, "v must be equal to 33")
}
+func TestDecoderIntInvalidJSONError(t *testing.T) {
+ var v int
+ dec := NewDecoder(strings.NewReader(``))
+ defer dec.Release()
+ err := dec.DecodeInt(&v)
+ assert.NotNil(t, err, "Err must not be nil")
+ assert.IsType(t, InvalidJSONError(""), err, "err should be of type InvalidJSONError")
+}
+
func TestDecoderInt32Basic(t *testing.T) {
json := []byte(`124`)
var v int32
@@ -182,6 +191,15 @@ func TestDecoderInt32tDecoderAPI(t *testing.T) {
assert.Equal(t, int32(33), v, "v must be equal to 33")
}
+func TestDecoderInt32InvalidJSONError(t *testing.T) {
+ var v int32
+ dec := NewDecoder(strings.NewReader(``))
+ defer dec.Release()
+ err := dec.DecodeInt32(&v)
+ assert.NotNil(t, err, "Err must not be nil")
+ assert.IsType(t, InvalidJSONError(""), err, "err should be of type InvalidJSONError")
+}
+
func TestDecoderUint32Basic(t *testing.T) {
json := []byte(`124 `)
var v uint32
@@ -253,6 +271,15 @@ func TestDecoderUint32tDecoderAPI(t *testing.T) {
assert.Equal(t, uint32(33), v, "v must be equal to 33")
}
+func TestDecoderUint32InvalidJSONError(t *testing.T) {
+ var v uint32
+ dec := NewDecoder(strings.NewReader(``))
+ defer dec.Release()
+ err := dec.DecodeUint32(&v)
+ assert.NotNil(t, err, "Err must not be nil")
+ assert.IsType(t, InvalidJSONError(""), err, "err should be of type InvalidJSONError")
+}
+
func TestDecoderInt64Basic(t *testing.T) {
json := []byte(`124 `)
var v int64
@@ -321,7 +348,7 @@ func TestDecoderInt64PoolError(t *testing.T) {
_ = dec.DecodeInt64(&result)
assert.True(t, false, "should not be called as decoder should have panicked")
}
-func TestDecoderInt64tDecoderAPI(t *testing.T) {
+func TestDecoderInt64DecoderAPI(t *testing.T) {
var v int64
dec := NewDecoder(strings.NewReader(`33`))
defer dec.Release()
@@ -329,6 +356,15 @@ func TestDecoderInt64tDecoderAPI(t *testing.T) {
assert.Nil(t, err, "Err must be nil")
assert.Equal(t, int64(33), v, "v must be equal to 33")
}
+func TestDecoderInt64InvalidJSONError(t *testing.T) {
+ var v int64
+ dec := NewDecoder(strings.NewReader(``))
+ defer dec.Release()
+ err := dec.DecodeInt64(&v)
+ assert.NotNil(t, err, "Err must not be nil")
+ assert.IsType(t, InvalidJSONError(""), err, "err should be of type InvalidJSONError")
+}
+
func TestDecoderUint64Basic(t *testing.T) {
json := []byte(` 124 `)
var v uint64
@@ -398,6 +434,16 @@ func TestDecoderUint64tDecoderAPI(t *testing.T) {
assert.Nil(t, err, "Err must be nil")
assert.Equal(t, uint64(33), v, "v must be equal to 33")
}
+
+func TestDecoderUint64InvalidJSONError(t *testing.T) {
+ var v uint64
+ dec := NewDecoder(strings.NewReader(``))
+ defer dec.Release()
+ err := dec.DecodeUint64(&v)
+ assert.NotNil(t, err, "Err must not be nil")
+ assert.IsType(t, InvalidJSONError(""), err, "err should be of type InvalidJSONError")
+}
+
func TestDecoderFloatBasic(t *testing.T) {
json := []byte(`100.11 `)
var v float64
@@ -463,3 +509,12 @@ func TestDecoderFloatPoolError(t *testing.T) {
_ = dec.DecodeFloat64(&result)
assert.True(t, false, "should not be called as decoder should have panicked")
}
+
+func TestDecoderFloatInvalidJSONError(t *testing.T) {
+ var v float64
+ dec := NewDecoder(strings.NewReader(``))
+ defer dec.Release()
+ err := dec.DecodeFloat64(&v)
+ assert.NotNil(t, err, "Err must not be nil")
+ assert.IsType(t, InvalidJSONError(""), err, "err should be of type InvalidJSONError")
+}
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)
+ }
+}