diff --git a/hashstructure.go b/hashstructure.go index 42778fe..83b52e1 100644 --- a/hashstructure.go +++ b/hashstructure.go @@ -281,6 +281,10 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { if impl, ok := parent.(Includable); ok { include = impl } + var sliceAsSets SliceAsSetsable + if impl, ok := parent.(SliceAsSetsable); ok { + sliceAsSets = impl + } if impl, ok := parent.(Hashable); ok { return impl.Hash() @@ -294,6 +298,9 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { if impl, ok := parentptr.(Includable); ok { include = impl } + if impl, ok := parentptr.(SliceAsSetsable); ok { + sliceAsSets = impl + } if impl, ok := parentptr.(Hashable); ok { return impl.Hash() @@ -353,6 +360,17 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { } } + // Check if we implement SliceAsSetsable + if sliceAsSets != nil { + sas, err := sliceAsSets.SliceAsSets(fieldType.Name, innerV) + if err != nil { + return 0, err + } + if sas { + f |= visitFlagSet + } + } + switch tag { case "set": f |= visitFlagSet diff --git a/hashstructure_test.go b/hashstructure_test.go index de88f8e..a3896be 100644 --- a/hashstructure_test.go +++ b/hashstructure_test.go @@ -674,6 +674,55 @@ func TestHash_hashable(t *testing.T) { } } +func TestHash_sliceAsSetsable(t *testing.T) { + cases := []struct { + One, Two interface{} + Match bool + }{ + { + testSliceAsSetsable{Kind: "map", Slice: []string{"1", "2"}, SliceSet: []string{"a", "b"}}, + testSliceAsSetsable{Kind: "map", Slice: []string{"2", "1"}, SliceSet: []string{"a", "b"}}, + true, + }, + { + testSliceAsSetsable{Kind: "map", Slice: []string{"1", "2"}, SliceSet: []string{"a", "b"}}, + testSliceAsSetsable{Kind: "map", Slice: []string{"2", "1"}, SliceSet: []string{"b", "a"}}, + true, + }, + { + testSliceAsSetsable{Kind: "seq", Slice: []string{"1", "2"}, SliceSet: []string{"a", "b"}}, + testSliceAsSetsable{Kind: "seq", Slice: []string{"2", "1"}, SliceSet: []string{"a", "b"}}, + false, + }, + { + testSliceAsSetsable{Kind: "seq", Slice: []string{"1", "2"}, SliceSet: []string{"a", "b"}}, + testSliceAsSetsable{Kind: "seq", Slice: []string{"1", "2"}, SliceSet: []string{"b", "a"}}, + true, + }, + } + + for _, tc := range cases { + one, err := Hash(tc.One, testFormat, nil) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.One, err) + } + two, err := Hash(tc.Two, testFormat, nil) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.Two, err) + } + + // Zero is always wrong + if one == 0 { + t.Fatalf("zero hash: %#v", tc.One) + } + + // Compare + if (one == two) != tc.Match { + t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) + } + } +} + type testIncludable struct { Value string Ignore string @@ -727,3 +776,19 @@ func (t *testHashablePointer) Hash() (uint64, error) { return 100, nil } + +type testSliceAsSetsable struct { + Kind string + Slice []string + SliceSet []string +} + +func (t testSliceAsSetsable) SliceAsSets(field string, v interface{}) (bool, error) { + switch t.Kind { + case "map": + return true, nil + case "seq": + return field == "SliceSet", nil + } + return false, nil +} diff --git a/include.go b/include.go index 702d354..3001231 100644 --- a/include.go +++ b/include.go @@ -20,3 +20,10 @@ type IncludableMap interface { type Hashable interface { Hash() (uint64, error) } + +// SliceAsSetsable is an interface that can optionally be implemented by +// a struct. It will be called for each field in the struct to check whether +// the filed should be treated as set. +type SliceAsSetsable interface { + SliceAsSets(field string, v interface{}) (bool, error) +}