Skip to content

Commit

Permalink
Merge branch 'feature/json-support' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
srfrog committed May 2, 2019
2 parents 13b7da1 + 92c8269 commit 4f13a9c
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 2 deletions.
11 changes: 11 additions & 0 deletions .travis.yml
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Dict
[![GoDoc](https://godoc.org/github.com/srfrog/dict?status.svg)](https://godoc.org/github.com/srfrog/dict)
[![Go Report Card](https://goreportcard.com/badge/github.com/srfrog/dict?svg=1)](https://goreportcard.com/report/github.com/srfrog/dict)
[![Coverage Status](https://coveralls.io/repos/github/srfrog/dict/badge.svg?branch=master)](https://coveralls.io/github/srfrog/dict?branch=master)
[![Build Status](https://travis-ci.com/srfrog/dict.svg?branch=master)](https://travis-ci.com/srfrog/dict)

*Python dictionary data type (dict) in Go*

Expand Down Expand Up @@ -28,7 +30,7 @@ View [example_test.go][2] for an extended example of basic usage and features.
- [x] Go map keys are used for dict keys if they are hashable.
- [x] Dict items are sorted in their insertion order, unlike Go maps.
- [ ] Go routine safe with minimal mutex locking (WIP)
- [ ] Builtin JSON support for marshalling and unmarshalling (WIP)
- [x] Builtin JSON support for marshalling and unmarshalling
- [ ] sql.Scanner support via optional sub-package (WIP)
- [ ] Plenty of tests and examples to get you started quickly (WIP)

Expand Down
2 changes: 1 addition & 1 deletion doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@
package dict

// Version is the version of this package.
const Version = "0.0.1"
const Version = "0.0.2"
130 changes: 130 additions & 0 deletions json.go
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
}
95 changes: 95 additions & 0 deletions json_test.go
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"))
}

0 comments on commit 4f13a9c

Please sign in to comment.