Skip to content
This repository has been archived by the owner on Jul 22, 2024. It is now read-only.

Commit

Permalink
Make use of binaryencoder interface too
Browse files Browse the repository at this point in the history
  • Loading branch information
jpalawaga committed Oct 3, 2022
1 parent 8412676 commit caa7370
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 41 deletions.
62 changes: 37 additions & 25 deletions hashstructure.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package hashstructure

import (
"encoding"
"encoding/binary"
"fmt"
"hash"
Expand Down Expand Up @@ -38,9 +39,10 @@ type HashOptions struct {
// panic)
UseStringer bool

// StringIgnoredStructs will attempt to .String() a struct if all of the
// members of the struct are ignored for the purposes of hashing.
StringIgnoredStructs bool
// UnhashedStructFallback will attempt to make use of the BinaryEncoder and
// Stringer interfaces (in that order) to hash structs that contain no
// exported fields.
UnhashedStructFallback bool
}

// Format specifies the hashing process used. Different formats typically
Expand Down Expand Up @@ -120,27 +122,27 @@ func Hash(v interface{}, format Format, opts *HashOptions) (uint64, error) {

// Create our walker and walk the structure
w := &walker{
format: format,
h: opts.Hasher,
tag: opts.TagName,
zeronil: opts.ZeroNil,
ignorezerovalue: opts.IgnoreZeroValue,
sets: opts.SlicesAsSets,
stringer: opts.UseStringer,
stringignoredstructs: opts.StringIgnoredStructs,
format: format,
h: opts.Hasher,
tag: opts.TagName,
zeronil: opts.ZeroNil,
ignorezerovalue: opts.IgnoreZeroValue,
sets: opts.SlicesAsSets,
stringer: opts.UseStringer,
unhashedstructfallback: opts.UnhashedStructFallback,
}
return w.visit(reflect.ValueOf(v), nil)
}

type walker struct {
format Format
h hash.Hash64
tag string
zeronil bool
ignorezerovalue bool
sets bool
stringer bool
stringignoredstructs bool
format Format
h hash.Hash64
tag string
zeronil bool
ignorezerovalue bool
sets bool
stringer bool
unhashedstructfallback bool
}

type visitOpts struct {
Expand Down Expand Up @@ -390,16 +392,26 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
h = hashFinishUnordered(w.h, h)
}
}
// no fields involved in the hash! try and string instead.
if unhashedfields == l && w.stringignoredstructs {
if impl, ok := parent.(fmt.Stringer); ok {
w.h.Reset()
_, err := w.h.Write([]byte(impl.String()))
// no fields involved in the hash! try binary and string instead.
if unhashedfields == l && w.unhashedstructfallback {
var data []byte
if impl, ok := parent.(encoding.BinaryMarshaler); ok {
data, err = impl.MarshalBinary()
if err != nil {
return 0, err
}
return w.h.Sum64(), nil
}

if impl, ok := parent.(fmt.Stringer); ok {
data = []byte(impl.String())
}

w.h.Reset()
_, err := w.h.Write(data)
if err != nil {
return 0, err
}
return w.h.Sum64(), nil
}

return h, nil
Expand Down
77 changes: 61 additions & 16 deletions hashstructure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,61 +730,106 @@ func (t *testHashablePointer) Hash() (uint64, error) {
return 100, nil
}

type Unexported struct {
type UnexportedStringer struct {
n int
}

func (u Unexported) String() string {
func (u UnexportedStringer) String() string {
return fmt.Sprintf("%d", u.n)
}

type UnexportedBinaryer struct {
n int
}

func (u UnexportedBinaryer) MarshalBinary() (data []byte, err error) {
fmt.Println("Out:", []byte(fmt.Sprintf("%d", u.n)))
return []byte(fmt.Sprintf("%d", u.n)), nil
}

func TestHash_StringIgnoredStructs(t *testing.T) {
cases := []struct {
One, Two interface{}
Match bool
Err string
}{
{
Unexported{n: 1},
Unexported{n: 1},
UnexportedStringer{n: 1},
UnexportedStringer{n: 1},
true,
"",
},
{
UnexportedStringer{n: 1},
UnexportedStringer{n: 2},
false,
"",
},
{
[]interface{}{UnexportedStringer{n: 1}},
[]interface{}{UnexportedStringer{n: 1}},
true,
"",
},
{
[]interface{}{UnexportedStringer{n: 1}},
[]interface{}{UnexportedStringer{n: 2}},
false,
"",
},
{
map[string]interface{}{"v": UnexportedStringer{n: 1}},
map[string]interface{}{"v": UnexportedStringer{n: 1}},
true,
"",
},
{
map[string]interface{}{"v": UnexportedStringer{n: 1}},
map[string]interface{}{"v": UnexportedStringer{n: 2}},
false,
"",
},
{
UnexportedBinaryer{n: 1},
UnexportedBinaryer{n: 1},
true,
"",
},
{
Unexported{n: 1},
Unexported{n: 2},
UnexportedBinaryer{n: 1},
UnexportedBinaryer{n: 2},
false,
"",
},
{
[]interface{}{Unexported{n: 1}},
[]interface{}{Unexported{n: 1}},
[]interface{}{UnexportedBinaryer{n: 1}},
[]interface{}{UnexportedBinaryer{n: 1}},
true,
"",
},
{
[]interface{}{Unexported{n: 1}},
[]interface{}{Unexported{n: 2}},
[]interface{}{UnexportedBinaryer{n: 1}},
[]interface{}{UnexportedBinaryer{n: 2}},
false,
"",
},
{
map[string]interface{}{"v": Unexported{n: 1}},
map[string]interface{}{"v": Unexported{n: 1}},
map[string]interface{}{"v": UnexportedBinaryer{n: 1}},
map[string]interface{}{"v": UnexportedBinaryer{n: 1}},
true,
"",
},
{
map[string]interface{}{"v": Unexported{n: 1}},
map[string]interface{}{"v": Unexported{n: 2}},
map[string]interface{}{"v": UnexportedBinaryer{n: 1}},
map[string]interface{}{"v": UnexportedBinaryer{n: 2}},
false,
"",
},
}

for i, tc := range cases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
one, err := Hash(tc.One, testFormat, &HashOptions{StringIgnoredStructs: true})
one, err := Hash(tc.One, testFormat, &HashOptions{UnhashedStructFallback: true})
if tc.Err != "" {
if err == nil {
t.Fatal("expected error")
Expand All @@ -800,7 +845,7 @@ func TestHash_StringIgnoredStructs(t *testing.T) {
t.Fatalf("Failed to hash %#v: %s", tc.One, err)
}

two, err := Hash(tc.Two, testFormat, &HashOptions{StringIgnoredStructs: true})
two, err := Hash(tc.Two, testFormat, &HashOptions{UnhashedStructFallback: true})
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.Two, err)
}
Expand Down

0 comments on commit caa7370

Please sign in to comment.