From 9c8ab075c2485cacbe1400a9b932ea88bf4dcf10 Mon Sep 17 00:00:00 2001 From: Rangel Reale Date: Sun, 5 Nov 2017 18:42:05 -0200 Subject: [PATCH] reflect.Value.Interface() has some problems with some kind of fields, 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 --- sheriff.go | 156 +++++++++++++++++++++++++++++++++++------------- sheriff_test.go | 29 +++++++++ 2 files changed, 144 insertions(+), 41 deletions(-) diff --git a/sheriff.go b/sheriff.go index bc73799..1dd957a 100644 --- a/sheriff.go +++ b/sheriff.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "strings" + "unsafe" "github.com/hashicorp/go-version" ) @@ -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 } } @@ -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) @@ -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") + } +} diff --git a/sheriff_test.go b/sheriff_test.go index 3cd3d5c..6408b4a 100644 --- a/sheriff_test.go +++ b/sheriff_test.go @@ -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)) +}