Skip to content

Commit

Permalink
reflect.Value.Interface() has some problems with some kind of fields,…
Browse files Browse the repository at this point in the history
… like unexported fields.

The deepequal package had the same problem, so I copied their fix, seems to really fix this time.

https://github.com/juju/testing/blob/master/checkers/deepequal.go#L269
  • Loading branch information
RangelReale committed Nov 5, 2017
1 parent df73e2a commit 9c8ab07
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 41 deletions.
156 changes: 115 additions & 41 deletions sheriff.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"reflect"
"strings"
"unsafe"

"github.com/hashicorp/go-version"
)
Expand Down Expand Up @@ -87,55 +88,53 @@ func Marshal(options *Options, data interface{}) (interface{}, error) {
val = val.Elem()
}

if val.IsValid() && val.CanInterface() {
// we can skip the group checkif if the field is a composition field
isEmbeddedField := field.Anonymous && val.Kind() == reflect.Struct
if !isEmbeddedField {
if checkGroups {
groups := strings.Split(field.Tag.Get("groups"), ",")

shouldShow := listContains(groups, options.Groups)
if !shouldShow || len(groups) == 0 {
continue
}
}
// we can skip the group checkif if the field is a composition field
isEmbeddedField := field.Anonymous && val.Kind() == reflect.Struct
if !isEmbeddedField {
if checkGroups {
groups := strings.Split(field.Tag.Get("groups"), ",")

if since := field.Tag.Get("since"); since != "" {
sinceVersion, err := version.NewVersion(since)
if err != nil {
return nil, err
}
if options.ApiVersion.LessThan(sinceVersion) {
continue
}
shouldShow := listContains(groups, options.Groups)
if !shouldShow || len(groups) == 0 {
continue
}
}

if until := field.Tag.Get("until"); until != "" {
untilVersion, err := version.NewVersion(until)
if err != nil {
return nil, err
}
if options.ApiVersion.GreaterThan(untilVersion) {
continue
}
if since := field.Tag.Get("since"); since != "" {
sinceVersion, err := version.NewVersion(since)
if err != nil {
return nil, err
}
if options.ApiVersion.LessThan(sinceVersion) {
continue
}
}

v, err := marshalValue(options, val)
if err != nil {
return nil, err
if until := field.Tag.Get("until"); until != "" {
untilVersion, err := version.NewVersion(until)
if err != nil {
return nil, err
}
if options.ApiVersion.GreaterThan(untilVersion) {
continue
}
}
}

// when a composition field we want to bring the child
// nodes to the top
nestedVal, ok := v.(map[string]interface{})
if isEmbeddedField && ok {
for key, value := range nestedVal {
dest[key] = value
}
} else {
dest[jsonTag] = v
v, err := marshalValue(options, val)
if err != nil {
return nil, err
}

// when a composition field we want to bring the child
// nodes to the top
nestedVal, ok := v.(map[string]interface{})
if isEmbeddedField && ok {
for key, value := range nestedVal {
dest[key] = value
}
} else {
dest[jsonTag] = v
}
}

Expand All @@ -146,7 +145,7 @@ func Marshal(options *Options, data interface{}) (interface{}, error) {
//
// There is support for types implementing the Marshaller interface, arbitrary structs, slices, maps and base types.
func marshalValue(options *Options, v reflect.Value) (interface{}, error) {
val := v.Interface()
val := interfaceOf(v)

if marshaller, ok := val.(Marshaller); ok {
return marshaller.Marshal(options)
Expand Down Expand Up @@ -220,3 +219,78 @@ func listContains(a []string, b []string) bool {
}
return false
}

// interfaceOf returns v.Interface() even if v.CanInterface() == false.
// This enables us to call fmt.Printf on a value even if it's derived
// from inside an unexported field.
// See https://code.google.com/p/go/issues/detail?id=8965
// for a possible future alternative to this hack.
// from https://github.com/juju/testing/blob/master/checkers/deepequal.go
func interfaceOf(v reflect.Value) interface{} {
if !v.IsValid() {
return nil
}
return bypassCanInterface(v).Interface()
}

type flag uintptr

var flagRO flag

// constants copied from reflect/value.go
const (
// The value of flagRO up to and including Go 1.3.
flagRO1p3 = 1 << 0

// The value of flagRO from Go 1.4.
flagRO1p4 = 1 << 5
)

var flagValOffset = func() uintptr {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
return field.Offset
}()

func flagField(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}

// bypassCanInterface returns a version of v that
// bypasses the CanInterface check.
func bypassCanInterface(v reflect.Value) reflect.Value {
if !v.IsValid() || v.CanInterface() {
return v
}
*flagField(&v) &^= flagRO
return v
}

// Sanity checks against future reflect package changes
// to the type or semantics of the Value.flag field.
func init() {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
panic("reflect.Value flag field has changed kind")
}
var t struct {
a int
A int
}
vA := reflect.ValueOf(t).FieldByName("A")
va := reflect.ValueOf(t).FieldByName("a")
flagA := *flagField(&vA)
flaga := *flagField(&va)

// Infer flagRO from the difference between the flags
// for the (otherwise identical) fields in t.
flagRO = flagA ^ flaga
if flagRO != flagRO1p3 && flagRO != flagRO1p4 {
panic("reflect.Value read-only flag has changed semantics")
}
}
29 changes: 29 additions & 0 deletions sheriff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,32 @@ func TestMarshal_EmbeddedFieldEmpty(t *testing.T) {

assert.Equal(t, string(expected), string(actual))
}

type TestInlineStruct struct {
tableName struct{} `json:"-"`

Field string `json:"field"`
Field2 *string `json:"field2"`
}

func TestMarshal_InlineStruct(t *testing.T) {
v := TestInlineStruct{
Field: "World",
Field2: nil,
}
o := &Options{}

actualMap, err := Marshal(o, v)
assert.NoError(t, err)

actual, err := json.Marshal(actualMap)
assert.NoError(t, err)

expected, err := json.Marshal(map[string]interface{}{
"field": "World",
"field2": nil,
})
assert.NoError(t, err)

assert.Equal(t, string(expected), string(actual))
}

0 comments on commit 9c8ab07

Please sign in to comment.