├── go.mod ├── hashstructure_examples_test.go ├── include.go ├── LICENSE ├── README.md ├── hashstructure.go └── hashstructure_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tetratelabs/hashstructure 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /hashstructure_examples_test.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func ExampleHash() { 8 | type ComplexStruct struct { 9 | Name string 10 | Age uint 11 | Metadata map[string]interface{} 12 | } 13 | 14 | v := ComplexStruct{ 15 | Name: "mitchellh", 16 | Age: 64, 17 | Metadata: map[string]interface{}{ 18 | "car": true, 19 | "location": "California", 20 | "siblings": []string{"Bob", "John"}, 21 | }, 22 | } 23 | 24 | hash, err := Hash(v, nil) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | fmt.Printf("%d", hash) 30 | // Output: 31 | // 6691276962590150517 32 | } 33 | -------------------------------------------------------------------------------- /include.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | // Includable is an interface that can optionally be implemented by 4 | // a struct. It will be called for each field in the struct to check whether 5 | // it should be included in the hash. 6 | type Includable interface { 7 | HashInclude(field string, v interface{}) (bool, error) 8 | } 9 | 10 | // IncludableMap is an interface that can optionally be implemented by 11 | // a struct. It will be called when a map-type field is found to ask the 12 | // struct if the map item should be included in the hash. 13 | type IncludableMap interface { 14 | HashIncludeMap(field string, k, v interface{}) (bool, error) 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mitchell Hashimoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hashstructure [![GoDoc](https://godoc.org/github.com/mitchellh/hashstructure?status.svg)](https://godoc.org/github.com/mitchellh/hashstructure) 2 | 3 | hashstructure is a Go library for creating a unique hash value 4 | for arbitrary values in Go. 5 | 6 | This can be used to key values in a hash (for use in a map, set, etc.) 7 | that are complex. The most common use case is comparing two values without 8 | sending data across the network, caching values locally (de-dup), and so on. 9 | 10 | ## Features 11 | 12 | * Hash any arbitrary Go value, including complex types. 13 | 14 | * Tag a struct field to ignore it and not affect the hash value. 15 | 16 | * Tag a slice type struct field to treat it as a set where ordering 17 | doesn't affect the hash code but the field itself is still taken into 18 | account to create the hash value. 19 | 20 | * Optionally specify a custom hash function to optimize for speed, collision 21 | avoidance for your data set, etc. 22 | 23 | * Optionally hash the output of `.String()` on structs that implement fmt.Stringer, 24 | allowing effective hashing of time.Time 25 | 26 | ## Installation 27 | 28 | Standard `go get`: 29 | 30 | ``` 31 | $ go get github.com/mitchellh/hashstructure 32 | ``` 33 | 34 | ## Usage & Example 35 | 36 | For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/hashstructure). 37 | 38 | A quick code example is shown below: 39 | 40 | ```go 41 | type ComplexStruct struct { 42 | Name string 43 | Age uint 44 | Metadata map[string]interface{} 45 | } 46 | 47 | v := ComplexStruct{ 48 | Name: "mitchellh", 49 | Age: 64, 50 | Metadata: map[string]interface{}{ 51 | "car": true, 52 | "location": "California", 53 | "siblings": []string{"Bob", "John"}, 54 | }, 55 | } 56 | 57 | hash, err := hashstructure.Hash(v, nil) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | fmt.Printf("%d", hash) 63 | // Output: 64 | // 2307517237273902113 65 | ``` 66 | -------------------------------------------------------------------------------- /hashstructure.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "hash" 7 | "hash/fnv" 8 | "reflect" 9 | ) 10 | 11 | // ErrNotStringer is returned when there's an error with hash:"string" 12 | type ErrNotStringer struct { 13 | Field string 14 | } 15 | 16 | // Error implements error for ErrNotStringer 17 | func (ens *ErrNotStringer) Error() string { 18 | return fmt.Sprintf("hashstructure: %s has hash:\"string\" set, but does not implement fmt.Stringer", ens.Field) 19 | } 20 | 21 | // HashOptions are options that are available for hashing. 22 | type HashOptions struct { 23 | // Hasher is the hash function to use. If this isn't set, it will 24 | // default to FNV. 25 | Hasher hash.Hash64 26 | 27 | // TagName is the struct tag to look at when hashing the structure. 28 | // By default this is "hash". 29 | TagName string 30 | 31 | // ZeroNil is flag determining if nil pointer should be treated equal 32 | // to a zero value of pointed type. By default this is false. 33 | ZeroNil bool 34 | } 35 | 36 | // Hash returns the hash value of an arbitrary value. 37 | // 38 | // If opts is nil, then default options will be used. See HashOptions 39 | // for the default values. The same *HashOptions value cannot be used 40 | // concurrently. None of the values within a *HashOptions struct are 41 | // safe to read/write while hashing is being done. 42 | // 43 | // Notes on the value: 44 | // 45 | // * Unexported fields on structs are ignored and do not affect the 46 | // hash value. 47 | // 48 | // * Adding an exported field to a struct with the zero value will change 49 | // the hash value. 50 | // 51 | // For structs, the hashing can be controlled using tags. For example: 52 | // 53 | // struct { 54 | // Name string 55 | // UUID string `hash:"ignore"` 56 | // } 57 | // 58 | // The available tag values are: 59 | // 60 | // * "ignore" or "-" - The field will be ignored and not affect the hash code. 61 | // 62 | // * "set" - The field will be treated as a set, where ordering doesn't 63 | // affect the hash code. This only works for slices. 64 | // 65 | // * "string" - The field will be hashed as a string, only works when the 66 | // field implements fmt.Stringer 67 | // 68 | func Hash(v interface{}, opts *HashOptions) (uint64, error) { 69 | // Create default options 70 | if opts == nil { 71 | opts = &HashOptions{} 72 | } 73 | if opts.Hasher == nil { 74 | opts.Hasher = fnv.New64() 75 | } 76 | if opts.TagName == "" { 77 | opts.TagName = "hash" 78 | } 79 | 80 | // Reset the hash 81 | opts.Hasher.Reset() 82 | 83 | // Create our walker and walk the structure 84 | w := &walker{ 85 | h: opts.Hasher, 86 | tag: opts.TagName, 87 | zeronil: opts.ZeroNil, 88 | } 89 | return w.visit(reflect.ValueOf(v), nil) 90 | } 91 | 92 | type walker struct { 93 | h hash.Hash64 94 | tag string 95 | zeronil bool 96 | } 97 | 98 | type visitOpts struct { 99 | // Flags are a bitmask of flags to affect behavior of this visit 100 | Flags visitFlag 101 | 102 | // Information about the struct containing this field 103 | Struct interface{} 104 | StructField string 105 | } 106 | 107 | func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { 108 | t := reflect.TypeOf(0) 109 | 110 | // Loop since these can be wrapped in multiple layers of pointers 111 | // and interfaces. 112 | for { 113 | // If we have an interface, dereference it. We have to do this up 114 | // here because it might be a nil in there and the check below must 115 | // catch that. 116 | if v.Kind() == reflect.Interface { 117 | v = v.Elem() 118 | continue 119 | } 120 | 121 | if v.Kind() == reflect.Ptr { 122 | if w.zeronil { 123 | t = v.Type().Elem() 124 | } 125 | v = reflect.Indirect(v) 126 | continue 127 | } 128 | 129 | break 130 | } 131 | 132 | // If it is nil, treat it like a zero. 133 | if !v.IsValid() { 134 | v = reflect.Zero(t) 135 | } 136 | 137 | // Binary writing can use raw ints, we have to convert to 138 | // a sized-int, we'll choose the largest... 139 | switch v.Kind() { 140 | case reflect.Int: 141 | v = reflect.ValueOf(int64(v.Int())) 142 | case reflect.Uint: 143 | v = reflect.ValueOf(uint64(v.Uint())) 144 | case reflect.Bool: 145 | var tmp int8 146 | if v.Bool() { 147 | tmp = 1 148 | } 149 | v = reflect.ValueOf(tmp) 150 | } 151 | 152 | k := v.Kind() 153 | 154 | // We can shortcut numeric values by directly binary writing them 155 | if k >= reflect.Int && k <= reflect.Complex64 { 156 | // A direct hash calculation 157 | w.h.Reset() 158 | err := binary.Write(w.h, binary.LittleEndian, v.Interface()) 159 | return w.h.Sum64(), err 160 | } 161 | 162 | switch k { 163 | case reflect.Array: 164 | var h uint64 165 | l := v.Len() 166 | for i := 0; i < l; i++ { 167 | current, err := w.visit(v.Index(i), nil) 168 | if err != nil { 169 | return 0, err 170 | } 171 | 172 | h = hashUpdateOrdered(w.h, h, current) 173 | } 174 | 175 | return h, nil 176 | 177 | case reflect.Map: 178 | var includeMap IncludableMap 179 | if opts != nil && opts.Struct != nil { 180 | if v, ok := opts.Struct.(IncludableMap); ok { 181 | includeMap = v 182 | } 183 | } 184 | 185 | // Build the hash for the map. We do this by XOR-ing all the key 186 | // and value hashes. This makes it deterministic despite ordering. 187 | var h uint64 188 | for _, k := range v.MapKeys() { 189 | v := v.MapIndex(k) 190 | if includeMap != nil { 191 | incl, err := includeMap.HashIncludeMap( 192 | opts.StructField, k.Interface(), v.Interface()) 193 | if err != nil { 194 | return 0, err 195 | } 196 | if !incl { 197 | continue 198 | } 199 | } 200 | 201 | kh, err := w.visit(k, nil) 202 | if err != nil { 203 | return 0, err 204 | } 205 | vh, err := w.visit(v, nil) 206 | if err != nil { 207 | return 0, err 208 | } 209 | 210 | fieldHash := hashUpdateOrdered(w.h, kh, vh) 211 | h = hashUpdateUnordered(h, fieldHash) 212 | } 213 | 214 | return h, nil 215 | 216 | case reflect.Struct: 217 | parent := v.Interface() 218 | var include Includable 219 | if impl, ok := parent.(Includable); ok { 220 | include = impl 221 | } 222 | 223 | t := v.Type() 224 | h, err := w.visit(reflect.ValueOf(t.Name()), nil) 225 | if err != nil { 226 | return 0, err 227 | } 228 | 229 | l := v.NumField() 230 | for i := 0; i < l; i++ { 231 | if innerV := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { 232 | var f visitFlag 233 | fieldType := t.Field(i) 234 | if fieldType.PkgPath != "" { 235 | // Unexported 236 | continue 237 | } 238 | 239 | tag := fieldType.Tag.Get(w.tag) 240 | if tag == "ignore" || tag == "-" { 241 | // Ignore this field 242 | continue 243 | } 244 | 245 | // if string is set, use the string value 246 | if tag == "string" { 247 | if impl, ok := innerV.Interface().(fmt.Stringer); ok { 248 | innerV = reflect.ValueOf(impl.String()) 249 | } else { 250 | return 0, &ErrNotStringer{ 251 | Field: v.Type().Field(i).Name, 252 | } 253 | } 254 | } 255 | 256 | // Check if we implement includable and check it 257 | if include != nil { 258 | incl, err := include.HashInclude(fieldType.Name, innerV) 259 | if err != nil { 260 | return 0, err 261 | } 262 | if !incl { 263 | continue 264 | } 265 | } 266 | 267 | switch tag { 268 | case "set": 269 | f |= visitFlagSet 270 | } 271 | 272 | kh, err := w.visit(reflect.ValueOf(fieldType.Name), nil) 273 | if err != nil { 274 | return 0, err 275 | } 276 | 277 | vh, err := w.visit(innerV, &visitOpts{ 278 | Flags: f, 279 | Struct: parent, 280 | StructField: fieldType.Name, 281 | }) 282 | if err != nil { 283 | return 0, err 284 | } 285 | 286 | fieldHash := hashUpdateOrdered(w.h, kh, vh) 287 | h = hashUpdateUnordered(h, fieldHash) 288 | } 289 | } 290 | 291 | return h, nil 292 | 293 | case reflect.Slice: 294 | // We have two behaviors here. If it isn't a set, then we just 295 | // visit all the elements. If it is a set, then we do a deterministic 296 | // hash code. 297 | var h uint64 298 | var set bool 299 | if opts != nil { 300 | set = (opts.Flags & visitFlagSet) != 0 301 | } 302 | l := v.Len() 303 | for i := 0; i < l; i++ { 304 | current, err := w.visit(v.Index(i), nil) 305 | if err != nil { 306 | return 0, err 307 | } 308 | 309 | if set { 310 | h = hashUpdateUnordered(h, current) 311 | } else { 312 | h = hashUpdateOrdered(w.h, h, current) 313 | } 314 | } 315 | 316 | return h, nil 317 | 318 | case reflect.String: 319 | // Directly hash 320 | w.h.Reset() 321 | _, err := w.h.Write([]byte(v.String())) 322 | return w.h.Sum64(), err 323 | 324 | default: 325 | return 0, fmt.Errorf("unknown kind to hash: %s", k) 326 | } 327 | 328 | } 329 | 330 | func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 { 331 | // For ordered updates, use a real hash function 332 | h.Reset() 333 | 334 | // We just panic if the binary writes fail because we are writing 335 | // an int64 which should never be fail-able. 336 | e1 := binary.Write(h, binary.LittleEndian, a) 337 | e2 := binary.Write(h, binary.LittleEndian, b) 338 | if e1 != nil { 339 | panic(e1) 340 | } 341 | if e2 != nil { 342 | panic(e2) 343 | } 344 | 345 | return h.Sum64() 346 | } 347 | 348 | func hashUpdateUnordered(a, b uint64) uint64 { 349 | return a ^ b 350 | } 351 | 352 | // visitFlag is used as a bitmask for affecting visit behavior 353 | type visitFlag uint 354 | 355 | const ( 356 | visitFlagInvalid visitFlag = iota 357 | visitFlagSet = iota << 1 358 | ) 359 | -------------------------------------------------------------------------------- /hashstructure_test.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestHash_identity(t *testing.T) { 10 | cases := []interface{}{ 11 | nil, 12 | "foo", 13 | 42, 14 | true, 15 | false, 16 | []string{"foo", "bar"}, 17 | []interface{}{1, nil, "foo"}, 18 | map[string]string{"foo": "bar"}, 19 | map[interface{}]string{"foo": "bar"}, 20 | map[interface{}]interface{}{"foo": "bar", "bar": 0}, 21 | struct { 22 | Foo string 23 | Bar []interface{} 24 | }{ 25 | Foo: "foo", 26 | Bar: []interface{}{nil, nil, nil}, 27 | }, 28 | &struct { 29 | Foo string 30 | Bar []interface{} 31 | }{ 32 | Foo: "foo", 33 | Bar: []interface{}{nil, nil, nil}, 34 | }, 35 | } 36 | 37 | for _, tc := range cases { 38 | // We run the test 100 times to try to tease out variability 39 | // in the runtime in terms of ordering. 40 | valuelist := make([]uint64, 100) 41 | for i, _ := range valuelist { 42 | v, err := Hash(tc, nil) 43 | if err != nil { 44 | t.Fatalf("Error: %s\n\n%#v", err, tc) 45 | } 46 | 47 | valuelist[i] = v 48 | } 49 | 50 | // Zero is always wrong 51 | if valuelist[0] == 0 { 52 | t.Fatalf("zero hash: %#v", tc) 53 | } 54 | 55 | // Make sure all the values match 56 | t.Logf("%#v: %d", tc, valuelist[0]) 57 | for i := 1; i < len(valuelist); i++ { 58 | if valuelist[i] != valuelist[0] { 59 | t.Fatalf("non-matching: %d, %d\n\n%#v", i, 0, tc) 60 | } 61 | } 62 | } 63 | } 64 | 65 | func TestHash_equal(t *testing.T) { 66 | type testFoo struct{ Name string } 67 | type testBar struct{ Name string } 68 | 69 | cases := []struct { 70 | One, Two interface{} 71 | Match bool 72 | }{ 73 | { 74 | map[string]string{"foo": "bar"}, 75 | map[interface{}]string{"foo": "bar"}, 76 | true, 77 | }, 78 | 79 | { 80 | map[string]interface{}{"1": "1"}, 81 | map[string]interface{}{"1": "1", "2": "2"}, 82 | false, 83 | }, 84 | 85 | { 86 | struct{ Fname, Lname string }{"foo", "bar"}, 87 | struct{ Fname, Lname string }{"bar", "foo"}, 88 | false, 89 | }, 90 | 91 | { 92 | struct{ Lname, Fname string }{"foo", "bar"}, 93 | struct{ Fname, Lname string }{"foo", "bar"}, 94 | false, 95 | }, 96 | 97 | { 98 | struct{ Lname, Fname string }{"foo", "bar"}, 99 | struct{ Fname, Lname string }{"bar", "foo"}, 100 | true, 101 | }, 102 | 103 | { 104 | testFoo{"foo"}, 105 | testBar{"foo"}, 106 | false, 107 | }, 108 | 109 | { 110 | struct { 111 | Foo string 112 | unexported string 113 | }{ 114 | Foo: "bar", 115 | unexported: "baz", 116 | }, 117 | struct { 118 | Foo string 119 | unexported string 120 | }{ 121 | Foo: "bar", 122 | unexported: "bang", 123 | }, 124 | true, 125 | }, 126 | 127 | { 128 | struct { 129 | testFoo 130 | Foo string 131 | }{ 132 | Foo: "bar", 133 | testFoo: testFoo{Name: "baz"}, 134 | }, 135 | struct { 136 | testFoo 137 | Foo string 138 | }{ 139 | Foo: "bar", 140 | }, 141 | true, 142 | }, 143 | 144 | { 145 | struct { 146 | Foo string 147 | }{ 148 | Foo: "bar", 149 | }, 150 | struct { 151 | testFoo 152 | Foo string 153 | }{ 154 | Foo: "bar", 155 | }, 156 | true, 157 | }, 158 | } 159 | 160 | for i, tc := range cases { 161 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 162 | t.Logf("Hashing: %#v", tc.One) 163 | one, err := Hash(tc.One, nil) 164 | t.Logf("Result: %d", one) 165 | if err != nil { 166 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 167 | } 168 | t.Logf("Hashing: %#v", tc.Two) 169 | two, err := Hash(tc.Two, nil) 170 | t.Logf("Result: %d", two) 171 | if err != nil { 172 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 173 | } 174 | 175 | // Zero is always wrong 176 | if one == 0 { 177 | t.Fatalf("zero hash: %#v", tc.One) 178 | } 179 | 180 | // Compare 181 | if (one == two) != tc.Match { 182 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 183 | } 184 | }) 185 | } 186 | } 187 | 188 | func TestHash_equalIgnore(t *testing.T) { 189 | type Test1 struct { 190 | Name string 191 | UUID string `hash:"ignore"` 192 | } 193 | 194 | type Test2 struct { 195 | Name string 196 | UUID string `hash:"-"` 197 | } 198 | 199 | type TestTime struct { 200 | Name string 201 | Time time.Time `hash:"string"` 202 | } 203 | 204 | type TestTime2 struct { 205 | Name string 206 | Time time.Time 207 | } 208 | 209 | now := time.Now() 210 | cases := []struct { 211 | One, Two interface{} 212 | Match bool 213 | }{ 214 | { 215 | Test1{Name: "foo", UUID: "foo"}, 216 | Test1{Name: "foo", UUID: "bar"}, 217 | true, 218 | }, 219 | 220 | { 221 | Test1{Name: "foo", UUID: "foo"}, 222 | Test1{Name: "foo", UUID: "foo"}, 223 | true, 224 | }, 225 | 226 | { 227 | Test2{Name: "foo", UUID: "foo"}, 228 | Test2{Name: "foo", UUID: "bar"}, 229 | true, 230 | }, 231 | 232 | { 233 | Test2{Name: "foo", UUID: "foo"}, 234 | Test2{Name: "foo", UUID: "foo"}, 235 | true, 236 | }, 237 | { 238 | TestTime{Name: "foo", Time: now}, 239 | TestTime{Name: "foo", Time: time.Time{}}, 240 | false, 241 | }, 242 | { 243 | TestTime{Name: "foo", Time: now}, 244 | TestTime{Name: "foo", Time: now}, 245 | true, 246 | }, 247 | { 248 | TestTime2{Name: "foo", Time: now}, 249 | TestTime2{Name: "foo", Time: time.Time{}}, 250 | true, 251 | }, 252 | } 253 | 254 | for _, tc := range cases { 255 | one, err := Hash(tc.One, nil) 256 | if err != nil { 257 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 258 | } 259 | two, err := Hash(tc.Two, nil) 260 | if err != nil { 261 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 262 | } 263 | 264 | // Zero is always wrong 265 | if one == 0 { 266 | t.Fatalf("zero hash: %#v", tc.One) 267 | } 268 | 269 | // Compare 270 | if (one == two) != tc.Match { 271 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 272 | } 273 | } 274 | } 275 | 276 | func TestHash_stringTagError(t *testing.T) { 277 | type Test1 struct { 278 | Name string 279 | BrokenField string `hash:"string"` 280 | } 281 | 282 | type Test2 struct { 283 | Name string 284 | BustedField int `hash:"string"` 285 | } 286 | 287 | type Test3 struct { 288 | Name string 289 | Time time.Time `hash:"string"` 290 | } 291 | 292 | cases := []struct { 293 | Test interface{} 294 | Field string 295 | }{ 296 | { 297 | Test1{Name: "foo", BrokenField: "bar"}, 298 | "BrokenField", 299 | }, 300 | { 301 | Test2{Name: "foo", BustedField: 23}, 302 | "BustedField", 303 | }, 304 | { 305 | Test3{Name: "foo", Time: time.Now()}, 306 | "", 307 | }, 308 | } 309 | 310 | for _, tc := range cases { 311 | _, err := Hash(tc.Test, nil) 312 | if err != nil { 313 | if ens, ok := err.(*ErrNotStringer); ok { 314 | if ens.Field != tc.Field { 315 | t.Fatalf("did not get expected field %#v: got %s wanted %s", tc.Test, ens.Field, tc.Field) 316 | } 317 | } else { 318 | t.Fatalf("unknown error %#v: got %s", tc, err) 319 | } 320 | } 321 | } 322 | } 323 | 324 | func TestHash_equalNil(t *testing.T) { 325 | type Test struct { 326 | Str *string 327 | Int *int 328 | Map map[string]string 329 | Slice []string 330 | } 331 | 332 | cases := []struct { 333 | One, Two interface{} 334 | ZeroNil bool 335 | Match bool 336 | }{ 337 | { 338 | Test{ 339 | Str: nil, 340 | Int: nil, 341 | Map: nil, 342 | Slice: nil, 343 | }, 344 | Test{ 345 | Str: new(string), 346 | Int: new(int), 347 | Map: make(map[string]string), 348 | Slice: make([]string, 0), 349 | }, 350 | true, 351 | true, 352 | }, 353 | { 354 | Test{ 355 | Str: nil, 356 | Int: nil, 357 | Map: nil, 358 | Slice: nil, 359 | }, 360 | Test{ 361 | Str: new(string), 362 | Int: new(int), 363 | Map: make(map[string]string), 364 | Slice: make([]string, 0), 365 | }, 366 | false, 367 | false, 368 | }, 369 | { 370 | nil, 371 | 0, 372 | true, 373 | true, 374 | }, 375 | { 376 | nil, 377 | 0, 378 | false, 379 | true, 380 | }, 381 | } 382 | 383 | for _, tc := range cases { 384 | one, err := Hash(tc.One, &HashOptions{ZeroNil: tc.ZeroNil}) 385 | if err != nil { 386 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 387 | } 388 | two, err := Hash(tc.Two, &HashOptions{ZeroNil: tc.ZeroNil}) 389 | if err != nil { 390 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 391 | } 392 | 393 | // Zero is always wrong 394 | if one == 0 { 395 | t.Fatalf("zero hash: %#v", tc.One) 396 | } 397 | 398 | // Compare 399 | if (one == two) != tc.Match { 400 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 401 | } 402 | } 403 | } 404 | 405 | func TestHash_equalSet(t *testing.T) { 406 | type Test struct { 407 | Name string 408 | Friends []string `hash:"set"` 409 | } 410 | 411 | cases := []struct { 412 | One, Two interface{} 413 | Match bool 414 | }{ 415 | { 416 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 417 | Test{Name: "foo", Friends: []string{"bar", "foo"}}, 418 | true, 419 | }, 420 | 421 | { 422 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 423 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 424 | true, 425 | }, 426 | } 427 | 428 | for _, tc := range cases { 429 | one, err := Hash(tc.One, nil) 430 | if err != nil { 431 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 432 | } 433 | two, err := Hash(tc.Two, nil) 434 | if err != nil { 435 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 436 | } 437 | 438 | // Zero is always wrong 439 | if one == 0 { 440 | t.Fatalf("zero hash: %#v", tc.One) 441 | } 442 | 443 | // Compare 444 | if (one == two) != tc.Match { 445 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 446 | } 447 | } 448 | } 449 | 450 | func TestHash_includable(t *testing.T) { 451 | cases := []struct { 452 | One, Two interface{} 453 | Match bool 454 | }{ 455 | { 456 | testIncludable{Value: "foo"}, 457 | testIncludable{Value: "foo"}, 458 | true, 459 | }, 460 | 461 | { 462 | testIncludable{Value: "foo", Ignore: "bar"}, 463 | testIncludable{Value: "foo"}, 464 | true, 465 | }, 466 | 467 | { 468 | testIncludable{Value: "foo", Ignore: "bar"}, 469 | testIncludable{Value: "bar"}, 470 | false, 471 | }, 472 | } 473 | 474 | for _, tc := range cases { 475 | one, err := Hash(tc.One, nil) 476 | if err != nil { 477 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 478 | } 479 | two, err := Hash(tc.Two, nil) 480 | if err != nil { 481 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 482 | } 483 | 484 | // Zero is always wrong 485 | if one == 0 { 486 | t.Fatalf("zero hash: %#v", tc.One) 487 | } 488 | 489 | // Compare 490 | if (one == two) != tc.Match { 491 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 492 | } 493 | } 494 | } 495 | 496 | func TestHash_includableMap(t *testing.T) { 497 | cases := []struct { 498 | One, Two interface{} 499 | Match bool 500 | }{ 501 | { 502 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 503 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 504 | true, 505 | }, 506 | 507 | { 508 | testIncludableMap{Map: map[string]string{"foo": "bar", "ignore": "true"}}, 509 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 510 | true, 511 | }, 512 | 513 | { 514 | testIncludableMap{Map: map[string]string{"foo": "bar", "ignore": "true"}}, 515 | testIncludableMap{Map: map[string]string{"bar": "baz"}}, 516 | false, 517 | }, 518 | } 519 | 520 | for _, tc := range cases { 521 | one, err := Hash(tc.One, nil) 522 | if err != nil { 523 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 524 | } 525 | two, err := Hash(tc.Two, nil) 526 | if err != nil { 527 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 528 | } 529 | 530 | // Zero is always wrong 531 | if one == 0 { 532 | t.Fatalf("zero hash: %#v", tc.One) 533 | } 534 | 535 | // Compare 536 | if (one == two) != tc.Match { 537 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 538 | } 539 | } 540 | } 541 | 542 | type testIncludable struct { 543 | Value string 544 | Ignore string 545 | } 546 | 547 | func (t testIncludable) HashInclude(field string, v interface{}) (bool, error) { 548 | return field != "Ignore", nil 549 | } 550 | 551 | type testIncludableMap struct { 552 | Map map[string]string 553 | } 554 | 555 | func (t testIncludableMap) HashIncludeMap(field string, k, v interface{}) (bool, error) { 556 | if field != "Map" { 557 | return true, nil 558 | } 559 | 560 | if s, ok := k.(string); ok && s == "ignore" { 561 | return false, nil 562 | } 563 | 564 | return true, nil 565 | } 566 | --------------------------------------------------------------------------------