Skip to content

Commit

Permalink
Unmarshalling: basic functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
blgm committed May 10, 2020
1 parent b5db5ce commit 19446c0
Show file tree
Hide file tree
Showing 8 changed files with 491 additions and 48 deletions.
52 changes: 52 additions & 0 deletions inspections.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package jsonry

import "reflect"

func public(field reflect.StructField) bool {
return field.PkgPath == ""
}

func basicType(k reflect.Kind) bool {
switch k {
case reflect.String, reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return true
default:
return false
}
}

type valueDetails struct {
realValue reflect.Value
realType reflect.Type
realKind reflect.Kind
pointer bool
}

func inspectValue(v reflect.Value) valueDetails {
k := v.Kind()
switch k {
case reflect.Ptr:
r := inspectValue(v.Elem())
r.pointer = true
return r
case reflect.Interface:
return inspectValue(v.Elem())
case reflect.Invalid:
return valueDetails{
realValue: v,
realType: nil,
realKind: k,
pointer: false,
}
default:
return valueDetails{
realValue: v,
realType: v.Type(),
realKind: k,
pointer: false,
}
}
}
50 changes: 48 additions & 2 deletions internal/tree/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ func (t Tree) Attach(p path.Path, v interface{}) Tree {
case 0:
panic("empty path")
case 1:
branch, _ := p.Pull()
t[branch.Name] = v
leaf, _ := p.Pull()
t[leaf.Name] = v
default:
branch, stem := p.Pull()
if branch.List {
Expand All @@ -30,6 +30,32 @@ func (t Tree) Attach(p path.Path, v interface{}) Tree {
return t
}

func (t Tree) Fetch(p path.Path) (interface{}, bool) {
switch p.Len() {
case 0:
panic("empty path")
case 1:
leaf, _ := p.Pull()
v, ok := t[leaf.Name]
return v, ok
default:
branch, stem := p.Pull()
v, ok := t[branch.Name]
if !ok {
return nil, false
}

switch vt := v.(type) {
case map[string]interface{}:
return Tree(vt).Fetch(stem)
case []interface{}:
return unspread(vt, stem), true
default:
return nil, false
}
}
}

func spread(p path.Path, v interface{}) []interface{} {
vv := reflect.ValueOf(v)
if vv.Kind() != reflect.Array && vv.Kind() != reflect.Slice {
Expand All @@ -43,3 +69,23 @@ func spread(p path.Path, v interface{}) []interface{} {
}
return s
}

func unspread(v []interface{}, stem path.Path) []interface{} {
l := make([]interface{}, 0, len(v))
for i := range v {
switch vt := v[i].(type) {
case map[string]interface{}:
if r, ok := Tree(vt).Fetch(stem); ok {
l = append(l, r)
} else {
l = append(l, nil)
}
case []interface{}:
l = append(l, unspread(vt, stem)...)
default:
l = append(l, v[i])
}
}

return l
}
82 changes: 82 additions & 0 deletions internal/tree/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,86 @@ var _ = Describe("Tree", func() {
})
})
})

Describe("Fetch", func() {
It("can fetch a basic value", func() {
var t tree.Tree
Expect(json.Unmarshal([]byte(`{"a":{"b":{"c":{"d":{"e":"hello"}}}}}`), &t)).NotTo(HaveOccurred())
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.c.d.e"`})

v, ok := t.Fetch(p)
Expect(ok).To(BeTrue())
Expect(v).To(Equal("hello"))
})

It("says not ok when not there", func() {
var t tree.Tree
Expect(json.Unmarshal([]byte(`{"a":{"b":{"c":{"d":{"e":"hello"}}}}}`), &t)).NotTo(HaveOccurred())
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.not_there.d.e"`})

v, ok := t.Fetch(p)
Expect(ok).To(BeFalse())
Expect(v).To(BeNil())
})

It("can fetch a nil", func() {
var t tree.Tree
Expect(json.Unmarshal([]byte(`{"a":{"b":{"c":{"d":{"e":null}}}}}`), &t)).NotTo(HaveOccurred())
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.c.d.e"`})

v, ok := t.Fetch(p)
Expect(ok).To(BeTrue())
Expect(v).To(BeNil())
})

It("can fetch an object", func() {
var t tree.Tree
Expect(json.Unmarshal([]byte(`{"a":{"b":{"c":{"d":{"e":"hello"}}}}}`), &t)).NotTo(HaveOccurred())
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.c"`})

v, ok := t.Fetch(p)
Expect(ok).To(BeTrue())
Expect(v).To(Equal(map[string]interface{}{"d": map[string]interface{}{"e": "hello"}}))
})

It("can fetch a list at the leaf", func() {
var t tree.Tree
Expect(json.Unmarshal([]byte(`{"a":{"b":{"c":{"d":{"e":["h","e","l","l","o"]}}}}}`), &t)).NotTo(HaveOccurred())
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.c.d.e"`})

v, ok := t.Fetch(p)
Expect(ok).To(BeTrue())
Expect(v).To(Equal([]interface{}{"h", "e", "l", "l", "o"}))
})

It("can fetch a list at a branch", func() {
var t tree.Tree
Expect(json.Unmarshal([]byte(`{"a":{"b":{"c":[{"d":{"e":"h"}},{"d":{"e":"i"}},{"d":{"e":"!"}}]}}}`), &t)).NotTo(HaveOccurred())
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.c.d.e"`})

v, ok := t.Fetch(p)
Expect(ok).To(BeTrue())
Expect(v).To(Equal([]interface{}{"h", "i", "!"}))
})

It("inserts nils when a list has missing elements", func() {
var t tree.Tree
Expect(json.Unmarshal([]byte(`{"a":{"b":{"c":[{"d":{"e":"h"}},{},{"d":{"e":"i"}},{"e":4},{"d":{"e":"!"}}]}}}`), &t)).NotTo(HaveOccurred())
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.c.d.e"`})

v, ok := t.Fetch(p)
Expect(ok).To(BeTrue())
Expect(v).To(Equal([]interface{}{"h", nil, "i", nil, "!"}))
})

It("flattens lists of lists", func() {
var t tree.Tree
Expect(json.Unmarshal([]byte(`{"a":{"b":{"c":[[{"d":{"e":"h"}}],[{}],{"d":{"e":"i"}},[],{"d":{"e":"!"}}]}}}`), &t)).NotTo(HaveOccurred())
p := path.ComputePath(reflect.StructField{Tag: `jsonry:"a.b.c.d.e"`})

v, ok := t.Fetch(p)
Expect(ok).To(BeTrue())
Expect(v).To(Equal([]interface{}{"h", nil, "i", "!"}))
})
})
})
66 changes: 20 additions & 46 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ type Marshaler interface {
}

func Marshal(input interface{}) ([]byte, error) {
i, _, k := underlying(reflect.ValueOf(input))
iv := inspectValue(reflect.ValueOf(input))

if k != reflect.Struct {
return nil, fmt.Errorf(`the input must be a struct, not "%s"`, i.Kind())
if iv.realKind != reflect.Struct {
return nil, fmt.Errorf(`the input must be a struct, not "%s"`, iv.realKind)
}

m, err := marshal(context.Context{}, i)
m, err := marshalStruct(context.Context{}, iv.realValue, iv.realType)
if err != nil {
return nil, err
}
Expand All @@ -33,39 +33,37 @@ func Marshal(input interface{}) ([]byte, error) {
}

func marshal(ctx context.Context, in reflect.Value) (r interface{}, err error) {
uv, ut, k := underlying(in)
input := inspectValue(in)

switch {
case implements(ut, (*json.Marshaler)(nil)):
r, err = marshalJSONMarshaler(ctx, uv)
case implements(ut, (*Marshaler)(nil)):
r, err = marshalJSONryMarshaler(ctx, uv)
case isBasicType(k):
case implements(input.realType, (*json.Marshaler)(nil)):
r, err = marshalJSONMarshaler(ctx, input.realValue)
case implements(input.realType, (*Marshaler)(nil)):
r, err = marshalJSONryMarshaler(ctx, input.realValue)
case basicType(input.realKind):
r = in.Interface()
case k == reflect.Invalid:
case input.realKind == reflect.Invalid:
r = nil
case k == reflect.Struct:
r, err = marshalStruct(ctx, uv)
case k == reflect.Slice || k == reflect.Array:
r, err = marshalList(ctx, uv)
case k == reflect.Map:
r, err = marshalMap(ctx, uv)
case input.realKind == reflect.Struct:
r, err = marshalStruct(ctx, input.realValue, input.realType)
case input.realKind == reflect.Slice || input.realKind == reflect.Array:
r, err = marshalList(ctx, input.realValue)
case input.realKind == reflect.Map:
r, err = marshalMap(ctx, input.realValue)
default:
err = newUnsupportedTypeError(ctx, ut)
err = newUnsupportedTypeError(ctx, input.realType)
}

return
}

func marshalStruct(ctx context.Context, in reflect.Value) (map[string]interface{}, error) {
func marshalStruct(ctx context.Context, in reflect.Value, t reflect.Type) (map[string]interface{}, error) {
out := make(tree.Tree)

t := in.Type()
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
private := f.PkgPath != ""

if !private {
if public(f) {
p := path.ComputePath(f)
if !p.OmitEmpty || !in.Field(i).IsZero() {
r, err := marshal(ctx.WithField(f.Name, f.Type), in.Field(i))
Expand Down Expand Up @@ -144,30 +142,6 @@ func marshalJSONryMarshaler(ctx context.Context, in reflect.Value) (interface{},
return marshal(ctx, t[0])
}

func underlying(v reflect.Value) (reflect.Value, reflect.Type, reflect.Kind) {
k := v.Kind()
switch k {
case reflect.Interface, reflect.Ptr:
return underlying(v.Elem())
case reflect.Invalid:
return v, nil, k
default:
return v, v.Type(), k
}
}

func isBasicType(k reflect.Kind) bool {
switch k {
case reflect.String, reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return true
default:
return false
}
}

func toError(v reflect.Value) error {
if v.CanInterface() {
if err, ok := v.Interface().(error); ok {
Expand Down
2 changes: 2 additions & 0 deletions marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ var _ = Describe("Marshal", func() {
Entry("uint64", c{V: uint64(42)}, `{"V":42}`),
Entry("float32", c{V: float32(4.2)}, `{"V":4.2}`),
Entry("float64", c{V: 4.2}, `{"V":4.2}`),
Entry("nil", c{V: nil}, `{"V":null}`),
Entry("interface{}", c{}, `{"V":null}`),
Entry("struct", c{V: c{V: "hierarchical"}}, `{"V":{"V":"hierarchical"}}`),
Entry("struct with private field", c{V: pri{private: true, Public: true}}, `{"V":{"Public":true}}`),
Entry("pointer", c{V: &i}, `{"V":0}`),
Expand Down
2 changes: 2 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#!/usr/bin/env sh

set +e

go run honnef.co/go/tools/cmd/staticcheck ./...
go run github.com/onsi/ginkgo/ginkgo -r

0 comments on commit 19446c0

Please sign in to comment.