Skip to content

Commit

Permalink
Marshal: can delegate to MarshalJSON()
Browse files Browse the repository at this point in the history
  • Loading branch information
blgm committed May 6, 2020
1 parent a97ee91 commit b86ac8a
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 65 deletions.
84 changes: 69 additions & 15 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package jsonry

import (
"encoding/json"
"errors"
"fmt"
"reflect"

"code.cloudfoundry.org/jsonry/internal/context"
Expand All @@ -15,14 +15,27 @@ const (
structSort
listSort
mapSort
jsonMarshalerSort
jsonryMarshalerSort
unsupportedSort
)

var (
jsonMarshalerInterface = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
jsonryMarshalerInterface = reflect.TypeOf((*Marshaler)(nil)).Elem()
)

// Marshaler is the interface implemented by types that
// can marshal themselves into a Go type that JSONry can handle.
type Marshaler interface {
MarshalJSONry() (interface{}, error)
}

func Marshal(input interface{}) ([]byte, error) {
i := underlyingValue(reflect.ValueOf(input))

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

m, err := marshal(context.Context{}, i)
Expand All @@ -38,11 +51,13 @@ func marshal(ctx context.Context, in reflect.Value) (r interface{}, err error) {
case basicSort:
r = in.Interface()
case structSort:
r, err = marshalStruct(ctx, underlyingValue(in))
r, err = marshalStruct(ctx, in)
case listSort:
r, err = marshalList(ctx, underlyingValue(in))
r, err = marshalList(ctx, in)
case mapSort:
r, err = marshalMap(ctx, underlyingValue(in))
r, err = marshalMap(ctx, in)
case jsonMarshalerSort:
r, err = marshalJSONMarshaler(ctx, in)
default:
err = NewUnsupportedTypeError(ctx, underlyingType(in))
}
Expand All @@ -52,16 +67,20 @@ func marshal(ctx context.Context, in reflect.Value) (r interface{}, err error) {
func marshalStruct(ctx context.Context, in reflect.Value) (map[string]interface{}, error) {
out := make(map[string]interface{})

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

r, err := marshal(ctx.WithField(f.Name, f.Type), in.Field(i))
if err != nil {
return nil, err
}
if !private {
r, err := marshal(ctx.WithField(f.Name, f.Type), s.Field(i))
if err != nil {
return nil, err
}

out[f.Name] = r
out[f.Name] = r
}
}

return out, nil
Expand All @@ -70,9 +89,10 @@ func marshalStruct(ctx context.Context, in reflect.Value) (map[string]interface{
func marshalList(ctx context.Context, in reflect.Value) ([]interface{}, error) {
var out []interface{}

for i := 0; i < in.Len(); i++ {
ctx := ctx.WithIndex(i, in.Type())
r, err := marshal(ctx, in.Index(i))
list := underlyingValue(in)
for i := 0; i < list.Len(); i++ {
ctx := ctx.WithIndex(i, list.Type())
r, err := marshal(ctx, list.Index(i))
if err != nil {
return nil, err
}
Expand All @@ -85,7 +105,7 @@ func marshalList(ctx context.Context, in reflect.Value) ([]interface{}, error) {
func marshalMap(ctx context.Context, in reflect.Value) (map[string]interface{}, error) {
out := make(map[string]interface{})

iter := in.MapRange()
iter := underlyingValue(in).MapRange()
for iter.Next() {
k := iter.Key()
if k.Kind() != reflect.String {
Expand All @@ -104,7 +124,30 @@ func marshalMap(ctx context.Context, in reflect.Value) (map[string]interface{},
return out, nil
}

func marshalJSONMarshaler(ctx context.Context, in reflect.Value) (interface{}, error) {
t := underlyingValue(in).MethodByName("MarshalJSON").Call(nil)

if !t[1].IsNil() {
return nil, fmt.Errorf("error from MarshaJSON() call %s: %w", ctx, toError(t[1]))
}

var r interface{}
err := json.Unmarshal(t[0].Bytes(), &r)
if err != nil {
return nil, fmt.Errorf(`error parsing MarshaJSON() output "%s" %s: %w`, t[0].Bytes(), ctx, err)
}

return r, nil
}

func sortOf(v reflect.Value) sort {
switch {
case underlyingType(v).Implements(jsonMarshalerInterface):
return jsonMarshalerSort
case v.Type().Implements(jsonryMarshalerInterface):
return jsonryMarshalerSort
}

switch underlyingValue(v).Kind() {
case reflect.Struct:
return structSort
Expand Down Expand Up @@ -141,3 +184,14 @@ func underlyingValue(v reflect.Value) reflect.Value {
return v
}
}

func toError(v reflect.Value) error {
if v.CanInterface() {
if err, ok := v.Interface().(error); ok {
return err
}
return fmt.Errorf("could not cast to error: %+v", v)
}
r := v.MethodByName("Error").Call(nil)
return fmt.Errorf("%s", r[0])
}
128 changes: 78 additions & 50 deletions marshal_test.go
Original file line number Diff line number Diff line change
@@ -1,63 +1,91 @@
package jsonry_test

import (
"encoding/json"
"errors"

"code.cloudfoundry.org/jsonry"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
)

type pri struct {
private bool
Public bool
}

type jsm struct{ value bool }

func (j jsm) MarshalJSON() ([]byte, error) {
if j.value {
return nil, errors.New("ouch")
}
return json.Marshal("hello")
}

type jrm bool

func (j jrm) MarshalJSONry() (interface{}, error) {
if j {
return nil, errors.New("ouch")
}
return "hello", nil
}

var _ = Describe("Marshal", func() {
Describe("supported types", func() {
type c struct{ V interface{} }
var i int
type c struct{ V interface{} }

DescribeTable(
"supported types",
func(input c, expected string) {
out, err := jsonry.Marshal(input)
Expect(err).NotTo(HaveOccurred())
Expect(out).To(MatchJSON(expected))
},
Entry("string", c{V: "hello"}, `{"V":"hello"}`),
Entry("boolean", c{V: true}, `{"V":true}`),
Entry("int", c{V: 42}, `{"V":42}`),
Entry("int8", c{V: int8(42)}, `{"V":42}`),
Entry("int16", c{V: int16(42)}, `{"V":42}`),
Entry("int32", c{V: int32(42)}, `{"V":42}`),
Entry("int64", c{V: int64(42)}, `{"V":42}`),
Entry("uint", c{V: uint(42)}, `{"V":42}`),
Entry("uint8", c{V: uint8(42)}, `{"V":42}`),
Entry("uint16", c{V: uint16(42)}, `{"V":42}`),
Entry("uint32", c{V: uint32(42)}, `{"V":42}`),
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("struct", c{V: c{V: "hierarchical"}}, `{"V":{"V":"hierarchical"}}`),
Entry("pointer", c{V: &i}, `{"V":0}`),
Entry("slice", c{V: []interface{}{"hello", true, 42}}, `{"V":["hello",true,42]}`),
Entry("array", c{V: [3]interface{}{"hello", true, 42}}, `{"V":["hello",true,42]}`),
Entry("map of interfaces", c{V: map[string]interface{}{"foo": "hello", "bar": true, "baz": 42}}, `{"V":{"foo":"hello","bar":true,"baz":42}}`),
Entry("map of strings", c{V: map[string]string{"foo": "hello", "bar": "true", "baz": "42"}}, `{"V":{"foo":"hello","bar":"true","baz":"42"}}`),
)
var i int

DescribeTable(
"unsupported types",
func(input c, message string) {
_, err := jsonry.Marshal(input)
Expect(err).To(MatchError(message), func() string {
if err != nil {
return err.Error()
}
return "there was no error"
})
},
Entry("complex", c{V: complex(1, 2)}, `unsupported type "complex128" at field "V" (type "interface {}")`),
Entry("channel", c{V: make(chan bool)}, `unsupported type "chan bool" at field "V" (type "interface {}")`),
Entry("func", c{V: func() {}}, `unsupported type "func()" at field "V" (type "interface {}")`),
Entry("map with non-string keys", c{V: map[int]interface{}{4: 3}}, `maps must only have strings keys for "map[int]interface {}" at field "V" (type "interface {}")`),
)
})
DescribeTable(
"supported conversions",
func(input c, expected string) {
out, err := jsonry.Marshal(input)
Expect(err).NotTo(HaveOccurred())
Expect(out).To(MatchJSON(expected))
},
Entry("string", c{V: "hello"}, `{"V":"hello"}`),
Entry("boolean", c{V: true}, `{"V":true}`),
Entry("int", c{V: 42}, `{"V":42}`),
Entry("int8", c{V: int8(42)}, `{"V":42}`),
Entry("int16", c{V: int16(42)}, `{"V":42}`),
Entry("int32", c{V: int32(42)}, `{"V":42}`),
Entry("int64", c{V: int64(42)}, `{"V":42}`),
Entry("uint", c{V: uint(42)}, `{"V":42}`),
Entry("uint8", c{V: uint8(42)}, `{"V":42}`),
Entry("uint16", c{V: uint16(42)}, `{"V":42}`),
Entry("uint32", c{V: uint32(42)}, `{"V":42}`),
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("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}`),
Entry("slice", c{V: []interface{}{"hello", true, 42}}, `{"V":["hello",true,42]}`),
Entry("array", c{V: [3]interface{}{"hello", true, 42}}, `{"V":["hello",true,42]}`),
Entry("map of interfaces", c{V: map[string]interface{}{"foo": "hello", "bar": true, "baz": 42}}, `{"V":{"foo":"hello","bar":true,"baz":42}}`),
Entry("map of strings", c{V: map[string]string{"foo": "hello", "bar": "true", "baz": "42"}}, `{"V":{"foo":"hello","bar":"true","baz":"42"}}`),
Entry("json.Marshaler", c{V: jsm{}}, `{"V": "hello"}`),
)

DescribeTable(
"failure cases",
func(input c, message string) {
_, err := jsonry.Marshal(input)
Expect(err).To(MatchError(message), func() string {
if err != nil {
return err.Error()
}
return "there was no error"
})
},
Entry("complex", c{V: complex(1, 2)}, `unsupported type "complex128" at field "V" (type "interface {}")`),
Entry("channel", c{V: make(chan bool)}, `unsupported type "chan bool" at field "V" (type "interface {}")`),
Entry("func", c{V: func() {}}, `unsupported type "func()" at field "V" (type "interface {}")`),
Entry("map with non-string keys", c{V: map[int]interface{}{4: 3}}, `maps must only have strings keys for "map[int]interface {}" at field "V" (type "interface {}")`),
Entry("json.Marshaler with failure", c{V: jsm{value: true}}, `error from MarshaJSON() call at field "V" (type "interface {}"): ouch`),
)

Describe("inputs", func() {
It("accept a struct", func() {
Expand All @@ -74,14 +102,14 @@ var _ = Describe("Marshal", func() {

It("rejects a non-struct value", func() {
_, err := jsonry.Marshal(42)
Expect(err).To(MatchError("the input must be a struct"))
Expect(err).To(MatchError(`the input must be a struct, not "int"`))
})

It("rejects a nil pointer", func() {
type s struct{}
var sp *s
_, err := jsonry.Marshal(sp)
Expect(err).To(MatchError("the input must be a struct"))
Expect(err).To(MatchError(`the input must be a struct, not "invalid"`))
})
})
})

0 comments on commit b86ac8a

Please sign in to comment.