├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── errors.go ├── go.mod ├── go.sum ├── hashstructure.go ├── hashstructure_examples_test.go ├── hashstructure_test.go └── include.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [master] 4 | pull_request: 5 | name: Test 6 | permissions: 7 | contents: read 8 | jobs: 9 | test: 10 | strategy: 11 | matrix: 12 | go-version: [1.22.x, 1.23.x] 13 | platform: [ubuntu-latest, macos-latest, windows-latest] 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - name: Install Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - name: Install staticcheck 21 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 22 | shell: bash 23 | - name: Install golint 24 | run: go install golang.org/x/lint/golint@latest 25 | shell: bash 26 | - name: Update PATH 27 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 28 | shell: bash 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | - name: Fmt 32 | if: matrix.platform != 'windows-latest' # :( 33 | run: "diff <(gofmt -d .) <(printf '')" 34 | shell: bash 35 | - name: Vet 36 | run: go vet ./... 37 | - name: Staticcheck 38 | run: staticcheck ./... 39 | - name: Lint 40 | run: golint ./... 41 | - name: Test 42 | run: go test -race ./... 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.test -------------------------------------------------------------------------------- /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 | * Optionally, override the hashing process by implementing `Hashable`. 27 | 28 | ## Installation 29 | 30 | Standard `go get`: 31 | 32 | ``` 33 | $ go get github.com/gohugoio/hashstructure 34 | ``` 35 | 36 | ## Usage & Example 37 | 38 | For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/hashstructure). 39 | 40 | A quick code example is shown below: 41 | 42 | ```go 43 | type ComplexStruct struct { 44 | Name string 45 | Age uint 46 | Metadata map[string]interface{} 47 | } 48 | 49 | v := ComplexStruct{ 50 | Name: "mitchellh", 51 | Age: 64, 52 | Metadata: map[string]interface{}{ 53 | "car": true, 54 | "location": "California", 55 | "siblings": []string{"Bob", "John"}, 56 | }, 57 | } 58 | 59 | hash, err := hashstructure.Hash(v, nil) 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | fmt.Printf("%d", hash) 65 | // Output: 66 | // 2307517237273902113 67 | ``` 68 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ErrNotStringer is returned when there's an error with hash:"string" 8 | type ErrNotStringer struct { 9 | Field string 10 | } 11 | 12 | // Error implements error for ErrNotStringer 13 | func (ens *ErrNotStringer) Error() string { 14 | return fmt.Sprintf("hashstructure: %s has hash:\"string\" set, but does not implement fmt.Stringer", ens.Field) 15 | } 16 | 17 | // ErrFormat is returned when an invalid format is given to the Hash function. 18 | type ErrFormat struct{} 19 | 20 | func (*ErrFormat) Error() string { 21 | return "format must be one of the defined Format values in the hashstructure library" 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gohugoio/hashstructure 2 | 3 | go 1.18 4 | 5 | require github.com/cespare/xxhash/v2 v2.3.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 2 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | -------------------------------------------------------------------------------- /hashstructure.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "hash" 7 | "hash/fnv" 8 | "io" 9 | "reflect" 10 | "time" 11 | ) 12 | 13 | // HashOptions are options that are available for hashing. 14 | type HashOptions struct { 15 | // Hasher is the hash function to use. If this isn't set, it will 16 | // default to FNV. 17 | Hasher hash.Hash64 18 | 19 | // TagName is the struct tag to look at when hashing the structure. 20 | // By default this is "hash". 21 | TagName string 22 | 23 | // ZeroNil is flag determining if nil pointer should be treated equal 24 | // to a zero value of pointed type. By default this is false. 25 | ZeroNil bool 26 | 27 | // IgnoreZeroValue is determining if zero value fields should be 28 | // ignored for hash calculation. 29 | IgnoreZeroValue bool 30 | 31 | // SlicesAsSets assumes that a `set` tag is always present for slices. 32 | // Default is false (in which case the tag is used instead) 33 | SlicesAsSets bool 34 | 35 | // UseStringer will attempt to use fmt.Stringer always. If the struct 36 | // doesn't implement fmt.Stringer, it'll fall back to trying usual tricks. 37 | // If this is true, and the "string" tag is also set, the tag takes 38 | // precedence (meaning that if the type doesn't implement fmt.Stringer, we 39 | // panic) 40 | UseStringer bool 41 | } 42 | 43 | // Hash returns the hash value of an arbitrary value. 44 | // 45 | // If opts is nil, then default options will be used. See HashOptions 46 | // for the default values. The same *HashOptions value cannot be used 47 | // concurrently. None of the values within a *HashOptions struct are 48 | // safe to read/write while hashing is being done. 49 | // 50 | // The "format" is required and must be one of the format values defined 51 | // by this library. You should probably just use "FormatV2". This allows 52 | // generated hashes uses alternate logic to maintain compatibility with 53 | // older versions. 54 | // 55 | // Notes on the value: 56 | // 57 | // - Unexported fields on structs are ignored and do not affect the 58 | // hash value. 59 | // 60 | // - Adding an exported field to a struct with the zero value will change 61 | // the hash value. 62 | // 63 | // For structs, the hashing can be controlled using tags. For example: 64 | // 65 | // struct { 66 | // Name string 67 | // UUID string `hash:"ignore"` 68 | // } 69 | // 70 | // The available tag values are: 71 | // 72 | // - "ignore" or "-" - The field will be ignored and not affect the hash code. 73 | // 74 | // - "set" - The field will be treated as a set, where ordering doesn't 75 | // affect the hash code. This only works for slices. 76 | // 77 | // - "string" - The field will be hashed as a string, only works when the 78 | // field implements fmt.Stringer 79 | func Hash(v interface{}, opts *HashOptions) (uint64, error) { 80 | // Create default options 81 | if opts == nil { 82 | opts = &HashOptions{} 83 | } 84 | if opts.Hasher == nil { 85 | opts.Hasher = fnv.New64() 86 | } 87 | if opts.TagName == "" { 88 | opts.TagName = "hash" 89 | } 90 | 91 | // Reset the hash 92 | opts.Hasher.Reset() 93 | 94 | // Fast path for strings. 95 | if s, ok := v.(string); ok { 96 | return hashString(opts.Hasher, s) 97 | } 98 | 99 | // Create our walker and walk the structure 100 | w := &walker{ 101 | h: opts.Hasher, 102 | tag: opts.TagName, 103 | zeronil: opts.ZeroNil, 104 | ignorezerovalue: opts.IgnoreZeroValue, 105 | sets: opts.SlicesAsSets, 106 | stringer: opts.UseStringer, 107 | } 108 | return w.visit(reflect.ValueOf(v), nil) 109 | } 110 | 111 | type walker struct { 112 | h hash.Hash64 113 | tag string 114 | zeronil bool 115 | ignorezerovalue bool 116 | sets bool 117 | stringer bool 118 | } 119 | 120 | type visitOpts struct { 121 | // Flags are a bitmask of flags to affect behavior of this visit 122 | Flags visitFlag 123 | 124 | // Information about the struct containing this field 125 | Struct interface{} 126 | StructField string 127 | } 128 | 129 | var timeType = reflect.TypeOf(time.Time{}) 130 | 131 | // A direct hash calculation used for numeric and bool values. 132 | func (w *walker) hashDirect(v any) (uint64, error) { 133 | w.h.Reset() 134 | err := binary.Write(w.h, binary.LittleEndian, v) 135 | return w.h.Sum64(), err 136 | } 137 | 138 | // A direct hash calculation used for strings. 139 | func (w *walker) hashString(s string) (uint64, error) { 140 | return hashString(w.h, s) 141 | } 142 | 143 | // A direct hash calculation used for strings. 144 | func hashString(h hash.Hash64, s string) (uint64, error) { 145 | h.Reset() 146 | 147 | // io.WriteString uses io.StringWriter if it exists, which is 148 | // implemented by e.g. github.com/cespare/xxhash. 149 | _, err := io.WriteString(h, s) 150 | return h.Sum64(), err 151 | } 152 | 153 | func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { 154 | t := reflect.TypeOf(0) 155 | 156 | // Loop since these can be wrapped in multiple layers of pointers 157 | // and interfaces. 158 | for { 159 | // If we have an interface, dereference it. We have to do this up 160 | // here because it might be a nil in there and the check below must 161 | // catch that. 162 | if v.Kind() == reflect.Interface { 163 | v = v.Elem() 164 | continue 165 | } 166 | 167 | if v.Kind() == reflect.Ptr { 168 | if w.zeronil { 169 | t = v.Type().Elem() 170 | } 171 | v = reflect.Indirect(v) 172 | continue 173 | } 174 | 175 | break 176 | } 177 | 178 | // If it is nil, treat it like a zero. 179 | if !v.IsValid() { 180 | v = reflect.Zero(t) 181 | } 182 | 183 | if v.CanInt() { 184 | if v.Kind() == reflect.Int { 185 | // binary.Write requires a fixed-size value. 186 | return w.hashDirect(v.Int()) 187 | } 188 | return w.hashDirect(v.Interface()) 189 | } 190 | 191 | if v.CanUint() { 192 | if v.Kind() == reflect.Uint { 193 | // binary.Write requires a fixed-size value. 194 | return w.hashDirect(v.Uint()) 195 | } 196 | return w.hashDirect(v.Interface()) 197 | } 198 | 199 | if v.CanFloat() || v.CanComplex() { 200 | return w.hashDirect(v.Interface()) 201 | } 202 | 203 | k := v.Kind() 204 | 205 | if k == reflect.Bool { 206 | var tmp int8 207 | if v.Bool() { 208 | tmp = 1 209 | } 210 | return w.hashDirect(tmp) 211 | } 212 | 213 | switch v.Type() { 214 | case timeType: 215 | w.h.Reset() 216 | b, err := v.Interface().(time.Time).MarshalBinary() 217 | if err != nil { 218 | return 0, err 219 | } 220 | 221 | err = binary.Write(w.h, binary.LittleEndian, b) 222 | return w.h.Sum64(), err 223 | } 224 | 225 | switch k { 226 | case reflect.Array: 227 | var h uint64 228 | l := v.Len() 229 | for i := 0; i < l; i++ { 230 | current, err := w.visit(v.Index(i), nil) 231 | if err != nil { 232 | return 0, err 233 | } 234 | 235 | h = hashUpdateOrdered(w.h, h, current) 236 | } 237 | 238 | return h, nil 239 | 240 | case reflect.Map: 241 | var includeMap IncludableMap 242 | var field string 243 | 244 | if v, ok := v.Interface().(IncludableMap); ok { 245 | includeMap = v 246 | } else if opts != nil && opts.Struct != nil { 247 | if v, ok := opts.Struct.(IncludableMap); ok { 248 | includeMap, field = v, opts.StructField 249 | } 250 | } 251 | 252 | // Build the hash for the map. We do this by XOR-ing all the key 253 | // and value hashes. This makes it deterministic despite ordering. 254 | var h uint64 255 | 256 | k := reflect.New(v.Type().Key()).Elem() 257 | vv := reflect.New(v.Type().Elem()).Elem() 258 | 259 | iter := v.MapRange() 260 | 261 | for iter.Next() { 262 | k.SetIterKey(iter) 263 | vv.SetIterValue(iter) 264 | if includeMap != nil { 265 | incl, err := includeMap.HashIncludeMap(field, k.Interface(), vv.Interface()) 266 | if err != nil { 267 | return 0, err 268 | } 269 | if !incl { 270 | continue 271 | } 272 | } 273 | 274 | kh, err := w.visit(k, nil) 275 | if err != nil { 276 | return 0, err 277 | } 278 | vh, err := w.visit(vv, nil) 279 | if err != nil { 280 | return 0, err 281 | } 282 | 283 | fieldHash := hashUpdateOrdered(w.h, kh, vh) 284 | h = hashUpdateUnordered(h, fieldHash) 285 | } 286 | 287 | // Important: read the docs for hashFinishUnordered 288 | h = hashFinishUnordered(w.h, h) 289 | 290 | return h, nil 291 | 292 | case reflect.Struct: 293 | parent := v.Interface() 294 | var include Includable 295 | if impl, ok := parent.(Includable); ok { 296 | include = impl 297 | } 298 | 299 | if impl, ok := parent.(Hashable); ok { 300 | return impl.Hash() 301 | } 302 | 303 | // If we can address this value, check if the pointer value 304 | // implements our interfaces and use that if so. 305 | if v.CanAddr() { 306 | vptr := v.Addr() 307 | parentptr := vptr.Interface() 308 | if impl, ok := parentptr.(Includable); ok { 309 | include = impl 310 | } 311 | 312 | if impl, ok := parentptr.(Hashable); ok { 313 | return impl.Hash() 314 | } 315 | } 316 | 317 | t := v.Type() 318 | h, err := w.visit(reflect.ValueOf(t.Name()), nil) 319 | if err != nil { 320 | return 0, err 321 | } 322 | 323 | l := v.NumField() 324 | for i := 0; i < l; i++ { 325 | if innerV := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { 326 | var f visitFlag 327 | fieldType := t.Field(i) 328 | if fieldType.PkgPath != "" { 329 | // Unexported 330 | continue 331 | } 332 | 333 | tag := fieldType.Tag.Get(w.tag) 334 | if tag == "ignore" || tag == "-" { 335 | // Ignore this field 336 | continue 337 | } 338 | 339 | if w.ignorezerovalue { 340 | if innerV.IsZero() { 341 | continue 342 | } 343 | } 344 | 345 | // if string is set, use the string value 346 | if tag == "string" || w.stringer { 347 | if impl, ok := innerV.Interface().(fmt.Stringer); ok { 348 | innerV = reflect.ValueOf(impl.String()) 349 | } else if tag == "string" { 350 | // We only show this error if the tag explicitly 351 | // requests a stringer. 352 | return 0, &ErrNotStringer{ 353 | Field: v.Type().Field(i).Name, 354 | } 355 | } 356 | } 357 | 358 | // Check if we implement includable and check it 359 | if include != nil { 360 | incl, err := include.HashInclude(fieldType.Name, innerV) 361 | if err != nil { 362 | return 0, err 363 | } 364 | if !incl { 365 | continue 366 | } 367 | } 368 | 369 | switch tag { 370 | case "set": 371 | f |= visitFlagSet 372 | } 373 | 374 | kh, err := w.visit(reflect.ValueOf(fieldType.Name), nil) 375 | if err != nil { 376 | return 0, err 377 | } 378 | 379 | vh, err := w.visit(innerV, &visitOpts{ 380 | Flags: f, 381 | Struct: parent, 382 | StructField: fieldType.Name, 383 | }) 384 | if err != nil { 385 | return 0, err 386 | } 387 | 388 | fieldHash := hashUpdateOrdered(w.h, kh, vh) 389 | h = hashUpdateUnordered(h, fieldHash) 390 | } 391 | // Important: read the docs for hashFinishUnordered 392 | h = hashFinishUnordered(w.h, h) 393 | } 394 | 395 | return h, nil 396 | 397 | case reflect.Slice: 398 | // We have two behaviors here. If it isn't a set, then we just 399 | // visit all the elements. If it is a set, then we do a deterministic 400 | // hash code. 401 | var h uint64 402 | var set bool 403 | if opts != nil { 404 | set = (opts.Flags & visitFlagSet) != 0 405 | } 406 | l := v.Len() 407 | for i := 0; i < l; i++ { 408 | current, err := w.visit(v.Index(i), nil) 409 | if err != nil { 410 | return 0, err 411 | } 412 | 413 | if set || w.sets { 414 | h = hashUpdateUnordered(h, current) 415 | } else { 416 | h = hashUpdateOrdered(w.h, h, current) 417 | } 418 | } 419 | 420 | if set { 421 | // Important: read the docs for hashFinishUnordered 422 | h = hashFinishUnordered(w.h, h) 423 | } 424 | 425 | return h, nil 426 | 427 | case reflect.String: 428 | return w.hashString(v.String()) 429 | default: 430 | return 0, fmt.Errorf("unknown kind to hash: %s", k) 431 | } 432 | } 433 | 434 | func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 { 435 | // For ordered updates, use a real hash function 436 | h.Reset() 437 | 438 | // We just panic if the binary writes fail because we are writing 439 | // an int64 which should never be fail-able. 440 | e1 := binary.Write(h, binary.LittleEndian, a) 441 | e2 := binary.Write(h, binary.LittleEndian, b) 442 | if e1 != nil { 443 | panic(e1) 444 | } 445 | if e2 != nil { 446 | panic(e2) 447 | } 448 | 449 | return h.Sum64() 450 | } 451 | 452 | func hashUpdateUnordered(a, b uint64) uint64 { 453 | return a ^ b 454 | } 455 | 456 | // After mixing a group of unique hashes with hashUpdateUnordered, it's always 457 | // necessary to call hashFinishUnordered. Why? Because hashUpdateUnordered 458 | // is a simple XOR, and calling hashUpdateUnordered on hashes produced by 459 | // hashUpdateUnordered can effectively cancel out a previous change to the hash 460 | // result if the same hash value appears later on. For example, consider: 461 | // 462 | // hashUpdateUnordered(hashUpdateUnordered("A", "B"), hashUpdateUnordered("A", "C")) = 463 | // H("A") ^ H("B")) ^ (H("A") ^ H("C")) = 464 | // (H("A") ^ H("A")) ^ (H("B") ^ H(C)) = 465 | // H(B) ^ H(C) = 466 | // hashUpdateUnordered(hashUpdateUnordered("Z", "B"), hashUpdateUnordered("Z", "C")) 467 | // 468 | // hashFinishUnordered "hardens" the result, so that encountering partially 469 | // overlapping input data later on in a different context won't cancel out. 470 | func hashFinishUnordered(h hash.Hash64, a uint64) uint64 { 471 | h.Reset() 472 | 473 | // We just panic if the writes fail 474 | e1 := binary.Write(h, binary.LittleEndian, a) 475 | if e1 != nil { 476 | panic(e1) 477 | } 478 | 479 | return h.Sum64() 480 | } 481 | 482 | // visitFlag is used as a bitmask for affecting visit behavior 483 | type visitFlag uint 484 | 485 | const ( 486 | visitFlagInvalid visitFlag = iota 487 | visitFlagSet = iota << 1 488 | ) 489 | -------------------------------------------------------------------------------- /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 | // 1839806922502695369 32 | } 33 | -------------------------------------------------------------------------------- /hashstructure_test.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/cespare/xxhash/v2" 10 | ) 11 | 12 | func TestHash_identity(t *testing.T) { 13 | cases := []interface{}{ 14 | nil, 15 | "foo", 16 | 42, 17 | true, 18 | false, 19 | []string{"foo", "bar"}, 20 | []interface{}{1, nil, "foo"}, 21 | map[string]string{"foo": "bar"}, 22 | map[interface{}]string{"foo": "bar"}, 23 | map[interface{}]interface{}{"foo": "bar", "bar": 0}, 24 | struct { 25 | Foo string 26 | Bar []interface{} 27 | }{ 28 | Foo: "foo", 29 | Bar: []interface{}{nil, nil, nil}, 30 | }, 31 | &struct { 32 | Foo string 33 | Bar []interface{} 34 | }{ 35 | Foo: "foo", 36 | Bar: []interface{}{nil, nil, nil}, 37 | }, 38 | } 39 | 40 | for _, tc := range cases { 41 | // We run the test 100 times to try to tease out variability 42 | // in the runtime in terms of ordering. 43 | valuelist := make([]uint64, 100) 44 | for i := range valuelist { 45 | v, err := Hash(tc, nil) 46 | if err != nil { 47 | t.Fatalf("Error: %s\n\n%#v", err, tc) 48 | } 49 | 50 | valuelist[i] = v 51 | } 52 | 53 | // Zero is always wrong 54 | if valuelist[0] == 0 { 55 | t.Fatalf("zero hash: %#v", tc) 56 | } 57 | 58 | // Make sure all the values match 59 | t.Logf("%#v: %d", tc, valuelist[0]) 60 | for i := 1; i < len(valuelist); i++ { 61 | if valuelist[i] != valuelist[0] { 62 | t.Fatalf("non-matching: %d, %d\n\n%#v", i, 0, tc) 63 | } 64 | } 65 | } 66 | } 67 | 68 | func TestHash_equal(t *testing.T) { 69 | type testFoo struct{ Name string } 70 | type testBar struct{ Name string } 71 | 72 | now := time.Now() 73 | 74 | cases := []struct { 75 | One, Two interface{} 76 | Match bool 77 | }{ 78 | { 79 | map[string]string{"foo": "bar"}, 80 | map[interface{}]string{"foo": "bar"}, 81 | true, 82 | }, 83 | 84 | { 85 | map[string]interface{}{"1": "1"}, 86 | map[string]interface{}{"1": "1", "2": "2"}, 87 | false, 88 | }, 89 | 90 | { 91 | struct{ Fname, Lname string }{"foo", "bar"}, 92 | struct{ Fname, Lname string }{"bar", "foo"}, 93 | false, 94 | }, 95 | 96 | { 97 | struct{ Lname, Fname string }{"foo", "bar"}, 98 | struct{ Fname, Lname string }{"foo", "bar"}, 99 | false, 100 | }, 101 | 102 | { 103 | struct{ Lname, Fname string }{"foo", "bar"}, 104 | struct{ Fname, Lname string }{"bar", "foo"}, 105 | false, 106 | }, 107 | 108 | { 109 | testFoo{"foo"}, 110 | testBar{"foo"}, 111 | false, 112 | }, 113 | 114 | { 115 | struct { 116 | Foo string 117 | unexported string 118 | }{ 119 | Foo: "bar", 120 | unexported: "baz", 121 | }, 122 | struct { 123 | Foo string 124 | unexported string 125 | }{ 126 | Foo: "bar", 127 | unexported: "bang", 128 | }, 129 | true, 130 | }, 131 | 132 | { 133 | struct { 134 | testFoo 135 | Foo string 136 | }{ 137 | Foo: "bar", 138 | testFoo: testFoo{Name: "baz"}, 139 | }, 140 | struct { 141 | testFoo 142 | Foo string 143 | }{ 144 | Foo: "bar", 145 | }, 146 | true, 147 | }, 148 | 149 | { 150 | struct { 151 | Foo string 152 | }{ 153 | Foo: "bar", 154 | }, 155 | struct { 156 | testFoo 157 | Foo string 158 | }{ 159 | Foo: "bar", 160 | }, 161 | true, 162 | }, 163 | { 164 | now, // contains monotonic clock 165 | time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 166 | now.Minute(), now.Second(), now.Nanosecond(), now.Location()), // does not contain monotonic clock 167 | true, 168 | }, 169 | } 170 | 171 | for i, tc := range cases { 172 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 173 | t.Logf("Hashing: %#v", tc.One) 174 | one, err := Hash(tc.One, nil) 175 | t.Logf("Result: %d", one) 176 | if err != nil { 177 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 178 | } 179 | t.Logf("Hashing: %#v", tc.Two) 180 | two, err := Hash(tc.Two, nil) 181 | t.Logf("Result: %d", two) 182 | if err != nil { 183 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 184 | } 185 | 186 | // Zero is always wrong 187 | if one == 0 { 188 | t.Fatalf("zero hash: %#v", tc.One) 189 | } 190 | 191 | // Compare 192 | if (one == two) != tc.Match { 193 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 194 | } 195 | }) 196 | } 197 | } 198 | 199 | func TestHash_equalIgnore(t *testing.T) { 200 | type Test1 struct { 201 | Name string 202 | UUID string `hash:"ignore"` 203 | } 204 | 205 | type Test2 struct { 206 | Name string 207 | UUID string `hash:"-"` 208 | } 209 | 210 | type TestTime struct { 211 | Name string 212 | Time time.Time `hash:"string"` 213 | } 214 | 215 | type TestTime2 struct { 216 | Name string 217 | Time time.Time 218 | } 219 | 220 | now := time.Now() 221 | cases := []struct { 222 | One, Two interface{} 223 | Match bool 224 | }{ 225 | { 226 | Test1{Name: "foo", UUID: "foo"}, 227 | Test1{Name: "foo", UUID: "bar"}, 228 | true, 229 | }, 230 | 231 | { 232 | Test1{Name: "foo", UUID: "foo"}, 233 | Test1{Name: "foo", UUID: "foo"}, 234 | true, 235 | }, 236 | 237 | { 238 | Test2{Name: "foo", UUID: "foo"}, 239 | Test2{Name: "foo", UUID: "bar"}, 240 | true, 241 | }, 242 | 243 | { 244 | Test2{Name: "foo", UUID: "foo"}, 245 | Test2{Name: "foo", UUID: "foo"}, 246 | true, 247 | }, 248 | { 249 | TestTime{Name: "foo", Time: now}, 250 | TestTime{Name: "foo", Time: time.Time{}}, 251 | false, 252 | }, 253 | { 254 | TestTime{Name: "foo", Time: now}, 255 | TestTime{Name: "foo", Time: now}, 256 | true, 257 | }, 258 | { 259 | TestTime2{Name: "foo", Time: now}, 260 | TestTime2{Name: "foo", Time: time.Time{}}, 261 | false, 262 | }, 263 | { 264 | TestTime2{Name: "foo", Time: now}, 265 | TestTime2{ 266 | Name: "foo", Time: time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 267 | now.Minute(), now.Second(), now.Nanosecond(), now.Location()), 268 | }, 269 | true, 270 | }, 271 | } 272 | 273 | for _, tc := range cases { 274 | one, err := Hash(tc.One, nil) 275 | if err != nil { 276 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 277 | } 278 | two, err := Hash(tc.Two, nil) 279 | if err != nil { 280 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 281 | } 282 | 283 | // Zero is always wrong 284 | if one == 0 { 285 | t.Fatalf("zero hash: %#v", tc.One) 286 | } 287 | 288 | // Compare 289 | if (one == two) != tc.Match { 290 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 291 | } 292 | } 293 | } 294 | 295 | func TestHash_stringTagError(t *testing.T) { 296 | type Test1 struct { 297 | Name string 298 | BrokenField string `hash:"string"` 299 | } 300 | 301 | type Test2 struct { 302 | Name string 303 | BustedField int `hash:"string"` 304 | } 305 | 306 | type Test3 struct { 307 | Name string 308 | Time time.Time `hash:"string"` 309 | } 310 | 311 | cases := []struct { 312 | Test interface{} 313 | Field string 314 | }{ 315 | { 316 | Test1{Name: "foo", BrokenField: "bar"}, 317 | "BrokenField", 318 | }, 319 | { 320 | Test2{Name: "foo", BustedField: 23}, 321 | "BustedField", 322 | }, 323 | { 324 | Test3{Name: "foo", Time: time.Now()}, 325 | "", 326 | }, 327 | } 328 | 329 | for _, tc := range cases { 330 | _, err := Hash(tc.Test, nil) 331 | if err != nil { 332 | if ens, ok := err.(*ErrNotStringer); ok { 333 | if ens.Field != tc.Field { 334 | t.Fatalf("did not get expected field %#v: got %s wanted %s", tc.Test, ens.Field, tc.Field) 335 | } 336 | } else { 337 | t.Fatalf("unknown error %#v: got %s", tc, err) 338 | } 339 | } 340 | } 341 | } 342 | 343 | func TestHash_equalNil(t *testing.T) { 344 | type Test struct { 345 | Str *string 346 | Int *int 347 | Map map[string]string 348 | Slice []string 349 | } 350 | 351 | cases := []struct { 352 | One, Two interface{} 353 | ZeroNil bool 354 | Match bool 355 | }{ 356 | { 357 | Test{ 358 | Str: nil, 359 | Int: nil, 360 | Map: nil, 361 | Slice: nil, 362 | }, 363 | Test{ 364 | Str: new(string), 365 | Int: new(int), 366 | Map: make(map[string]string), 367 | Slice: make([]string, 0), 368 | }, 369 | true, 370 | true, 371 | }, 372 | { 373 | Test{ 374 | Str: nil, 375 | Int: nil, 376 | Map: nil, 377 | Slice: nil, 378 | }, 379 | Test{ 380 | Str: new(string), 381 | Int: new(int), 382 | Map: make(map[string]string), 383 | Slice: make([]string, 0), 384 | }, 385 | false, 386 | false, 387 | }, 388 | { 389 | nil, 390 | 0, 391 | true, 392 | true, 393 | }, 394 | { 395 | nil, 396 | 0, 397 | false, 398 | true, 399 | }, 400 | } 401 | 402 | for _, tc := range cases { 403 | one, err := Hash(tc.One, &HashOptions{ZeroNil: tc.ZeroNil}) 404 | if err != nil { 405 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 406 | } 407 | two, err := Hash(tc.Two, &HashOptions{ZeroNil: tc.ZeroNil}) 408 | if err != nil { 409 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 410 | } 411 | 412 | // Zero is always wrong 413 | if one == 0 { 414 | t.Fatalf("zero hash: %#v", tc.One) 415 | } 416 | 417 | // Compare 418 | if (one == two) != tc.Match { 419 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 420 | } 421 | } 422 | } 423 | 424 | func TestHash_equalSet(t *testing.T) { 425 | type Test struct { 426 | Name string 427 | Friends []string `hash:"set"` 428 | } 429 | 430 | cases := []struct { 431 | One, Two interface{} 432 | Match bool 433 | }{ 434 | { 435 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 436 | Test{Name: "foo", Friends: []string{"bar", "foo"}}, 437 | true, 438 | }, 439 | 440 | { 441 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 442 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 443 | true, 444 | }, 445 | } 446 | 447 | for _, tc := range cases { 448 | one, err := Hash(tc.One, nil) 449 | if err != nil { 450 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 451 | } 452 | two, err := Hash(tc.Two, nil) 453 | if err != nil { 454 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 455 | } 456 | 457 | // Zero is always wrong 458 | if one == 0 { 459 | t.Fatalf("zero hash: %#v", tc.One) 460 | } 461 | 462 | // Compare 463 | if (one == two) != tc.Match { 464 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 465 | } 466 | } 467 | } 468 | 469 | func TestHash_includable(t *testing.T) { 470 | cases := []struct { 471 | One, Two interface{} 472 | Match bool 473 | }{ 474 | { 475 | testIncludable{Value: "foo"}, 476 | testIncludable{Value: "foo"}, 477 | true, 478 | }, 479 | 480 | { 481 | testIncludable{Value: "foo", Ignore: "bar"}, 482 | testIncludable{Value: "foo"}, 483 | true, 484 | }, 485 | 486 | { 487 | testIncludable{Value: "foo", Ignore: "bar"}, 488 | testIncludable{Value: "bar"}, 489 | false, 490 | }, 491 | } 492 | 493 | for _, tc := range cases { 494 | one, err := Hash(tc.One, nil) 495 | if err != nil { 496 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 497 | } 498 | two, err := Hash(tc.Two, nil) 499 | if err != nil { 500 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 501 | } 502 | 503 | // Zero is always wrong 504 | if one == 0 { 505 | t.Fatalf("zero hash: %#v", tc.One) 506 | } 507 | 508 | // Compare 509 | if (one == two) != tc.Match { 510 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 511 | } 512 | } 513 | } 514 | 515 | func TestHash_ignoreZeroValue(t *testing.T) { 516 | cases := []struct { 517 | IgnoreZeroValue bool 518 | }{ 519 | { 520 | IgnoreZeroValue: true, 521 | }, 522 | { 523 | IgnoreZeroValue: false, 524 | }, 525 | } 526 | structA := struct { 527 | Foo string 528 | Bar string 529 | Map map[string]int 530 | }{ 531 | Foo: "foo", 532 | Bar: "bar", 533 | } 534 | structB := struct { 535 | Foo string 536 | Bar string 537 | Baz string 538 | Map map[string]int 539 | }{ 540 | Foo: "foo", 541 | Bar: "bar", 542 | } 543 | 544 | for _, tc := range cases { 545 | hashA, err := Hash(structA, &HashOptions{IgnoreZeroValue: tc.IgnoreZeroValue}) 546 | if err != nil { 547 | t.Fatalf("Failed to hash %#v: %s", structA, err) 548 | } 549 | hashB, err := Hash(structB, &HashOptions{IgnoreZeroValue: tc.IgnoreZeroValue}) 550 | if err != nil { 551 | t.Fatalf("Failed to hash %#v: %s", structB, err) 552 | } 553 | if (hashA == hashB) != tc.IgnoreZeroValue { 554 | t.Fatalf("bad, expected: %#v\n\n%d\n\n%d", tc.IgnoreZeroValue, hashA, hashB) 555 | } 556 | } 557 | } 558 | 559 | func TestHash_includableMap(t *testing.T) { 560 | cases := []struct { 561 | One, Two interface{} 562 | Match bool 563 | }{ 564 | { 565 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 566 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 567 | true, 568 | }, 569 | 570 | { 571 | testIncludableMap{Map: map[string]string{"foo": "bar", "ignore": "true"}}, 572 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 573 | true, 574 | }, 575 | 576 | { 577 | testIncludableMap{Map: map[string]string{"foo": "bar", "ignore": "true"}}, 578 | testIncludableMap{Map: map[string]string{"bar": "baz"}}, 579 | false, 580 | }, 581 | { 582 | testIncludableMapMap{"foo": "bar"}, 583 | testIncludableMapMap{"foo": "bar"}, 584 | true, 585 | }, 586 | 587 | { 588 | testIncludableMapMap{"foo": "bar", "ignore": "true"}, 589 | testIncludableMapMap{"foo": "bar"}, 590 | true, 591 | }, 592 | 593 | { 594 | testIncludableMapMap{"foo": "bar", "ignore": "true"}, 595 | testIncludableMapMap{"bar": "baz"}, 596 | false, 597 | }, 598 | } 599 | 600 | for _, tc := range cases { 601 | one, err := Hash(tc.One, nil) 602 | if err != nil { 603 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 604 | } 605 | two, err := Hash(tc.Two, nil) 606 | if err != nil { 607 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 608 | } 609 | 610 | // Zero is always wrong 611 | if one == 0 { 612 | t.Fatalf("zero hash: %#v", tc.One) 613 | } 614 | 615 | // Compare 616 | if (one == two) != tc.Match { 617 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 618 | } 619 | } 620 | } 621 | 622 | func TestHash_hashable(t *testing.T) { 623 | cases := []struct { 624 | One, Two interface{} 625 | Match bool 626 | Err string 627 | }{ 628 | { 629 | testHashable{Value: "foo"}, 630 | &testHashablePointer{Value: "foo"}, 631 | true, 632 | "", 633 | }, 634 | 635 | { 636 | testHashable{Value: "foo1"}, 637 | &testHashablePointer{Value: "foo2"}, 638 | true, 639 | "", 640 | }, 641 | { 642 | testHashable{Value: "foo"}, 643 | &testHashablePointer{Value: "bar"}, 644 | false, 645 | "", 646 | }, 647 | { 648 | testHashable{Value: "nofoo"}, 649 | &testHashablePointer{Value: "bar"}, 650 | true, 651 | "", 652 | }, 653 | { 654 | testHashable{Value: "bar", Err: fmt.Errorf("oh no")}, 655 | testHashable{Value: "bar"}, 656 | true, 657 | "oh no", 658 | }, 659 | } 660 | 661 | for i, tc := range cases { 662 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 663 | one, err := Hash(tc.One, nil) 664 | if tc.Err != "" { 665 | if err == nil { 666 | t.Fatal("expected error") 667 | } 668 | 669 | if !strings.Contains(err.Error(), tc.Err) { 670 | t.Fatalf("expected error to contain %q, got: %s", tc.Err, err) 671 | } 672 | 673 | return 674 | } 675 | if err != nil { 676 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 677 | } 678 | 679 | two, err := Hash(tc.Two, nil) 680 | if err != nil { 681 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 682 | } 683 | 684 | // Zero is always wrong 685 | if one == 0 { 686 | t.Fatalf("zero hash: %#v", tc.One) 687 | } 688 | 689 | // Compare 690 | if (one == two) != tc.Match { 691 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 692 | } 693 | }) 694 | } 695 | } 696 | 697 | func TestHash_golden(t *testing.T) { 698 | foo := "foo" 699 | 700 | cases := []struct { 701 | In any 702 | Expect uint64 703 | }{ 704 | { 705 | In: nil, 706 | Expect: 12161962213042174405, 707 | }, 708 | { 709 | In: "foo", 710 | Expect: 15621798640163566899, 711 | }, 712 | { 713 | In: 42, 714 | Expect: 11375694726533372055, 715 | }, 716 | { 717 | In: uint8(42), 718 | Expect: 12638153115695167477, 719 | }, 720 | { 721 | In: int16(42), 722 | Expect: 590708257076254031, 723 | }, 724 | { 725 | In: int32(42), 726 | Expect: 843871326190827175, 727 | }, 728 | { 729 | In: int64(42), 730 | Expect: 11375694726533372055, 731 | }, 732 | 733 | { 734 | In: uint16(42), 735 | Expect: 590708257076254031, 736 | }, 737 | { 738 | In: uint32(42), 739 | Expect: 843871326190827175, 740 | }, 741 | { 742 | In: uint64(42), 743 | Expect: 11375694726533372055, 744 | }, 745 | { 746 | In: float32(42), 747 | Expect: 5558953217260120943, 748 | }, 749 | { 750 | In: float64(42), 751 | Expect: 12162027084228238918, 752 | }, 753 | { 754 | In: complex64(42), 755 | Expect: 13187391128804187615, 756 | }, 757 | { 758 | In: complex128(42), 759 | Expect: 4635205179288363782, 760 | }, 761 | { 762 | In: true, 763 | Expect: 12638153115695167454, 764 | }, 765 | { 766 | In: false, 767 | Expect: 12638153115695167455, 768 | }, 769 | { 770 | In: []string{"foo", "bar"}, 771 | Expect: 18333885979647637445, 772 | }, 773 | { 774 | In: []interface{}{1, nil, "foo"}, 775 | Expect: 636613494442026145, 776 | }, 777 | { 778 | In: map[string]string{"foo": "bar"}, 779 | Expect: 5334326627423288605, 780 | }, 781 | { 782 | In: map[string]*string{"foo": &foo}, 783 | Expect: 4615367350888355399, 784 | }, 785 | { 786 | In: map[*string]string{&foo: "bar"}, 787 | Expect: 5334326627423288605, 788 | }, 789 | { 790 | In: map[interface{}]string{"foo": "bar"}, 791 | Expect: 5334326627423288605, 792 | }, 793 | { 794 | In: map[interface{}]interface{}{"foo": "bar", "bar": 0}, 795 | Expect: 10207098687398820730, 796 | }, 797 | { 798 | In: map[interface{}]interface{}{"foo": "bar", "bar": map[interface{}]interface{}{"foo": "bar", "bar": map[interface{}]interface{}{"foo": "bar", "bar": map[interface{}]interface{}{&foo: "bar", "bar": 0}}}}, 799 | Expect: 18346441822047112296, 800 | }, 801 | { 802 | In: struct { 803 | Foo string 804 | Bar []interface{} 805 | }{ 806 | Foo: "foo", 807 | Bar: []interface{}{nil, nil, nil}, 808 | }, 809 | Expect: 14887393564066082535, 810 | }, 811 | } 812 | 813 | for i, tc := range cases { 814 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 815 | got, err := Hash(tc.In, nil) 816 | if err != nil { 817 | t.Fatalf("unexpected error: %s", err) 818 | } 819 | 820 | if got != tc.Expect { 821 | t.Fatalf("expected: %d, got: %d", tc.Expect, got) 822 | } 823 | }) 824 | } 825 | } 826 | 827 | func BenchmarkMap(b *testing.B) { 828 | m := map[string]any{ 829 | "int16": int16(42), 830 | "int32": int32(42), 831 | "int64": int64(42), 832 | "int": int(42), 833 | "uint16": uint16(42), 834 | "uint32": uint32(42), 835 | "uint64": uint64(42), 836 | "uint": uint(42), 837 | "float32": float32(42), 838 | "float64": float64(42), 839 | "complex64": complex64(42), 840 | "complex128": complex128(42), 841 | "string": "foo", 842 | "bool": true, 843 | "slice": []string{"foo", "bar"}, 844 | "sliceint": []int{1, 2, 3}, 845 | "map": map[string]string{"foo": "bar"}, 846 | "struct": struct { 847 | Foo string 848 | Bar []interface{} 849 | }{ 850 | Foo: "foo", 851 | Bar: []interface{}{nil, nil, nil}, 852 | }, 853 | } 854 | 855 | for i := 0; i < b.N; i++ { 856 | Hash(m, nil) 857 | } 858 | } 859 | 860 | func BenchmarkString(b *testing.B) { 861 | s := "lorem ipsum dolor sit amet" 862 | b.Run("default", func(b *testing.B) { 863 | for i := 0; i < b.N; i++ { 864 | Hash(s, nil) 865 | } 866 | }) 867 | 868 | b.Run("xxhash", func(b *testing.B) { 869 | opts := &HashOptions{Hasher: xxhash.New()} 870 | for i := 0; i < b.N; i++ { 871 | Hash(s, opts) 872 | } 873 | }) 874 | } 875 | 876 | type testIncludable struct { 877 | Value string 878 | Ignore string 879 | } 880 | 881 | func (t testIncludable) HashInclude(field string, v interface{}) (bool, error) { 882 | return field != "Ignore", nil 883 | } 884 | 885 | type testIncludableMap struct { 886 | Map map[string]string 887 | } 888 | 889 | func (t testIncludableMap) HashIncludeMap(field string, k, v interface{}) (bool, error) { 890 | if field != "Map" { 891 | return true, nil 892 | } 893 | 894 | if s, ok := k.(string); ok && s == "ignore" { 895 | return false, nil 896 | } 897 | 898 | return true, nil 899 | } 900 | 901 | type testHashable struct { 902 | Value string 903 | Err error 904 | } 905 | 906 | func (t testHashable) Hash() (uint64, error) { 907 | if t.Err != nil { 908 | return 0, t.Err 909 | } 910 | 911 | if strings.HasPrefix(t.Value, "foo") { 912 | return 500, nil 913 | } 914 | 915 | return 100, nil 916 | } 917 | 918 | type testHashablePointer struct { 919 | Value string 920 | } 921 | 922 | func (t *testHashablePointer) Hash() (uint64, error) { 923 | if strings.HasPrefix(t.Value, "foo") { 924 | return 500, nil 925 | } 926 | 927 | return 100, nil 928 | } 929 | 930 | type testIncludableMapMap map[string]string 931 | 932 | func (t testIncludableMapMap) HashIncludeMap(_ string, k, _ interface{}) (bool, error) { 933 | return k.(string) != "ignore", nil 934 | } 935 | -------------------------------------------------------------------------------- /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 | 17 | // Hashable is an interface that can optionally be implemented by a struct 18 | // to override the hash value. This value will override the hash value for 19 | // the entire struct. Entries in the struct will not be hashed. 20 | type Hashable interface { 21 | Hash() (uint64, error) 22 | } 23 | --------------------------------------------------------------------------------