-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feature/json-support' into develop
- Loading branch information
Showing
5 changed files
with
240 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
language: go | ||
sudo: false | ||
|
||
go: | ||
- 1.x | ||
|
||
before_install: | ||
- go get github.com/mattn/goveralls | ||
|
||
script: | ||
- $GOPATH/bin/goveralls -service=travis-ci |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,4 +24,4 @@ | |
package dict | ||
|
||
// Version is the version of this package. | ||
const Version = "0.0.1" | ||
const Version = "0.0.2" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// Copyright (c) 2019 srfrog - https://srfrog.me | ||
// Use of this source code is governed by the license in the LICENSE file. | ||
|
||
package dict | ||
|
||
import ( | ||
"encoding/json" | ||
"reflect" | ||
"strings" | ||
) | ||
|
||
// MarshalJSON implements the json.MarshalJSON interface. | ||
// The JSON representation of dict is just a JSON object. | ||
func (d *Dict) MarshalJSON() ([]byte, error) { | ||
if d.IsEmpty() { | ||
return []byte("null"), nil | ||
} | ||
|
||
var ( | ||
err error | ||
sb strings.Builder | ||
cnt int | ||
) | ||
|
||
sb.WriteByte('{') | ||
for item := range d.Items() { | ||
var p []byte | ||
|
||
sb.WriteByte('"') | ||
sb.WriteString(item.Key.(string)) | ||
sb.WriteByte('"') | ||
sb.WriteByte(':') | ||
|
||
p, err = json.Marshal(item.Value) | ||
if err != nil { | ||
return nil, err | ||
} | ||
sb.Write(p) | ||
cnt++ | ||
if cnt < d.Len() { | ||
sb.WriteByte(',') | ||
} | ||
} | ||
sb.WriteByte('}') | ||
|
||
return []byte(sb.String()), nil | ||
} | ||
|
||
// UnmarshalJSON implements the json.UnmarshalJSON interface. | ||
// The JSON representation of dict is just a JSON object. | ||
func (d *Dict) UnmarshalJSON(p []byte) error { | ||
var m map[string]interface{} | ||
if err := json.Unmarshal(p, &m); err != nil { | ||
return err | ||
} | ||
|
||
// Unforunately json.Unmarshal will produce dynamic interface types for JSON arrays | ||
// and objects - https://golang.org/pkg/encoding/json/#Unmarshal | ||
// So here we try to convert []interface{} (JSON array) values into a slice if all the | ||
// value types are the same. e.g., []string, []float64, etc... | ||
// Also convert map[string]interface{} (JSON object) values into embedded dict objects. | ||
for k, v := range m { | ||
switch x := v.(type) { | ||
// JSON array -> slice | ||
case []interface{}: | ||
kind, ok := hasSameKind(x) | ||
if !ok { | ||
break | ||
} | ||
switch kind { | ||
case reflect.Bool: | ||
var bs []bool | ||
for i := range x { | ||
bv, _ := x[i].(bool) | ||
bs = append(bs, bv) | ||
} | ||
m[k] = bs | ||
case reflect.Float64: | ||
var fs []float64 | ||
for i := range x { | ||
fv, _ := x[i].(float64) | ||
fs = append(fs, fv) | ||
} | ||
m[k] = fs | ||
case reflect.String: | ||
var ss []string | ||
for i := range x { | ||
sv, _ := x[i].(string) | ||
ss = append(ss, sv) | ||
} | ||
m[k] = ss | ||
} | ||
|
||
// JSON object -> dict | ||
case map[string]interface{}: | ||
m[k] = New(x) | ||
} | ||
} | ||
d.Update(m) | ||
|
||
return nil | ||
} | ||
|
||
func hasSameKind(a []interface{}) (reflect.Kind, bool) { | ||
var k, kseen reflect.Kind | ||
for i := range a { | ||
switch a[i].(type) { | ||
case nil: | ||
// If at least one value isn't nil (JSON null) convert it to the zero value of | ||
// the type. | ||
case bool: | ||
k = reflect.Bool | ||
case float64: | ||
k = reflect.Float64 | ||
case string: | ||
k = reflect.String | ||
default: | ||
// TODO: Array of arrays and array of objects. | ||
return reflect.Invalid, false | ||
} | ||
if kseen == 0 { | ||
kseen = k | ||
continue | ||
} | ||
if k != kseen { | ||
return reflect.Invalid, false | ||
} | ||
} | ||
return kseen, kseen != reflect.Invalid | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// Copyright (c) 2019 srfrog - https://srfrog.me | ||
// Use of this source code is governed by the license in the LICENSE file. | ||
|
||
package dict | ||
|
||
import ( | ||
"encoding/json" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestDictMarshalJSON(t *testing.T) { | ||
tests := []struct { | ||
in interface{} | ||
out string | ||
}{ | ||
{in: int(1), out: `{"0":1}`}, | ||
{in: float64(2.2), out: `{"0":2.2}`}, | ||
{in: "2.2", out: `{"0":"2.2"}`}, | ||
{in: uint(300), out: `{"0":300}`}, | ||
{in: []int{1, 2, 3}, out: `{"0":1,"1":2,"2":3}`}, | ||
{in: [][]int{{1, 2, 3}}, out: `{"0":[1,2,3]}`}, | ||
{in: map[string]int{"one item": 1}, out: `{"one item":1}`}, | ||
} | ||
for _, tc := range tests { | ||
d := New(tc.in) | ||
b, err := json.Marshal(d) | ||
require.NoError(t, err) | ||
require.JSONEq(t, tc.out, string(b)) | ||
} | ||
} | ||
|
||
func TestDictMarshalJSON_Embed(t *testing.T) { | ||
d := New(1, 2, 3) | ||
d.Set(d.Len(), New(4, 5, 6)) | ||
|
||
b, err := json.Marshal(d) | ||
require.NoError(t, err) | ||
j := ` | ||
{ | ||
"0":1, | ||
"1":2, | ||
"2":3, | ||
"3":{ | ||
"0":4, | ||
"1":5, | ||
"2":6 | ||
} | ||
}` | ||
require.JSONEq(t, j, string(b)) | ||
} | ||
|
||
func TestDictUnmarshalJSON(t *testing.T) { | ||
j := `{ | ||
"1": 1, | ||
"2": "two", | ||
"3": 3.30003, | ||
"4a": ["horse","cow"], | ||
"4b": [1, 2, 3], | ||
"4c": [1.1, 2.2, 3.3], | ||
"4d": [3, "something", 4.4], | ||
"4e": [null, null, 0.0001, null], | ||
"5": {"horse": "neighs", "cow": "moos", "dog": "woofs"}, | ||
"6": null | ||
}` | ||
d := New() | ||
require.NoError(t, json.Unmarshal([]byte(j), d)) | ||
|
||
tests := []struct { | ||
in string | ||
out interface{} | ||
}{ | ||
{in: "1", out: float64(1)}, | ||
{in: "2", out: "two"}, | ||
{in: "3", out: float64(3.30003)}, | ||
{in: "4a", out: []string{"horse", "cow"}}, | ||
{in: "4b", out: []float64{1, 2, 3}}, | ||
{in: "4c", out: []float64{1.1, 2.2, 3.3}}, | ||
{in: "4d", out: []interface{}{float64(3), "something", float64(4.4)}}, | ||
{in: "4e", out: []float64{0, 0, 0.0001, 0}}, | ||
{in: "6", out: nil}, | ||
} | ||
for _, tc := range tests { | ||
require.EqualValues(t, tc.out, d.Get(tc.in)) | ||
} | ||
|
||
// Embedded dict | ||
ed, ok := d.Get("5").(*Dict) | ||
require.True(t, ok) | ||
require.True(t, ed.Len() == 3) | ||
require.EqualValues(t, "neighs", ed.Get("horse")) | ||
require.EqualValues(t, "moos", ed.Get("cow")) | ||
require.EqualValues(t, "woofs", ed.Get("dog")) | ||
} |