├── go.mod ├── hashstructure_examples_test.go ├── errors.go ├── include.go ├── LICENSE ├── .github └── workflows │ └── test.yml ├── README.md ├── hashstructure.go └── hashstructure_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gohugoio/hashstructure 2 | 3 | go 1.18 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 | // 1839806922502695369 32 | } 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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.21.x, 1.22.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /hashstructure.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "hash" 7 | "hash/fnv" 8 | "reflect" 9 | "time" 10 | ) 11 | 12 | // HashOptions are options that are available for hashing. 13 | type HashOptions struct { 14 | // Hasher is the hash function to use. If this isn't set, it will 15 | // default to FNV. 16 | Hasher hash.Hash64 17 | 18 | // TagName is the struct tag to look at when hashing the structure. 19 | // By default this is "hash". 20 | TagName string 21 | 22 | // ZeroNil is flag determining if nil pointer should be treated equal 23 | // to a zero value of pointed type. By default this is false. 24 | ZeroNil bool 25 | 26 | // IgnoreZeroValue is determining if zero value fields should be 27 | // ignored for hash calculation. 28 | IgnoreZeroValue bool 29 | 30 | // SlicesAsSets assumes that a `set` tag is always present for slices. 31 | // Default is false (in which case the tag is used instead) 32 | SlicesAsSets bool 33 | 34 | // UseStringer will attempt to use fmt.Stringer always. If the struct 35 | // doesn't implement fmt.Stringer, it'll fall back to trying usual tricks. 36 | // If this is true, and the "string" tag is also set, the tag takes 37 | // precedence (meaning that if the type doesn't implement fmt.Stringer, we 38 | // panic) 39 | UseStringer bool 40 | } 41 | 42 | // Hash returns the hash value of an arbitrary value. 43 | // 44 | // If opts is nil, then default options will be used. See HashOptions 45 | // for the default values. The same *HashOptions value cannot be used 46 | // concurrently. None of the values within a *HashOptions struct are 47 | // safe to read/write while hashing is being done. 48 | // 49 | // The "format" is required and must be one of the format values defined 50 | // by this library. You should probably just use "FormatV2". This allows 51 | // generated hashes uses alternate logic to maintain compatibility with 52 | // older versions. 53 | // 54 | // Notes on the value: 55 | // 56 | // - Unexported fields on structs are ignored and do not affect the 57 | // hash value. 58 | // 59 | // - Adding an exported field to a struct with the zero value will change 60 | // the hash value. 61 | // 62 | // For structs, the hashing can be controlled using tags. For example: 63 | // 64 | // struct { 65 | // Name string 66 | // UUID string `hash:"ignore"` 67 | // } 68 | // 69 | // The available tag values are: 70 | // 71 | // - "ignore" or "-" - The field will be ignored and not affect the hash code. 72 | // 73 | // - "set" - The field will be treated as a set, where ordering doesn't 74 | // affect the hash code. This only works for slices. 75 | // 76 | // - "string" - The field will be hashed as a string, only works when the 77 | // field implements fmt.Stringer 78 | func Hash(v interface{}, opts *HashOptions) (uint64, error) { 79 | // Create default options 80 | if opts == nil { 81 | opts = &HashOptions{} 82 | } 83 | if opts.Hasher == nil { 84 | opts.Hasher = fnv.New64() 85 | } 86 | if opts.TagName == "" { 87 | opts.TagName = "hash" 88 | } 89 | 90 | // Reset the hash 91 | opts.Hasher.Reset() 92 | 93 | // Create our walker and walk the structure 94 | w := &walker{ 95 | h: opts.Hasher, 96 | tag: opts.TagName, 97 | zeronil: opts.ZeroNil, 98 | ignorezerovalue: opts.IgnoreZeroValue, 99 | sets: opts.SlicesAsSets, 100 | stringer: opts.UseStringer, 101 | } 102 | return w.visit(reflect.ValueOf(v), nil) 103 | } 104 | 105 | type walker struct { 106 | h hash.Hash64 107 | tag string 108 | zeronil bool 109 | ignorezerovalue bool 110 | sets bool 111 | stringer bool 112 | } 113 | 114 | type visitOpts struct { 115 | // Flags are a bitmask of flags to affect behavior of this visit 116 | Flags visitFlag 117 | 118 | // Information about the struct containing this field 119 | Struct interface{} 120 | StructField string 121 | } 122 | 123 | var timeType = reflect.TypeOf(time.Time{}) 124 | 125 | func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { 126 | t := reflect.TypeOf(0) 127 | 128 | // Loop since these can be wrapped in multiple layers of pointers 129 | // and interfaces. 130 | for { 131 | // If we have an interface, dereference it. We have to do this up 132 | // here because it might be a nil in there and the check below must 133 | // catch that. 134 | if v.Kind() == reflect.Interface { 135 | v = v.Elem() 136 | continue 137 | } 138 | 139 | if v.Kind() == reflect.Ptr { 140 | if w.zeronil { 141 | t = v.Type().Elem() 142 | } 143 | v = reflect.Indirect(v) 144 | continue 145 | } 146 | 147 | break 148 | } 149 | 150 | // If it is nil, treat it like a zero. 151 | if !v.IsValid() { 152 | v = reflect.Zero(t) 153 | } 154 | 155 | // Binary writing can use raw ints, we have to convert to 156 | // a sized-int, we'll choose the largest... 157 | switch v.Kind() { 158 | case reflect.Int: 159 | v = reflect.ValueOf(int64(v.Int())) 160 | case reflect.Uint: 161 | v = reflect.ValueOf(uint64(v.Uint())) 162 | case reflect.Bool: 163 | var tmp int8 164 | if v.Bool() { 165 | tmp = 1 166 | } 167 | v = reflect.ValueOf(tmp) 168 | } 169 | 170 | k := v.Kind() 171 | 172 | // We can shortcut numeric values by directly binary writing them 173 | if k >= reflect.Int && k <= reflect.Complex64 { 174 | // A direct hash calculation 175 | w.h.Reset() 176 | err := binary.Write(w.h, binary.LittleEndian, v.Interface()) 177 | return w.h.Sum64(), err 178 | } 179 | 180 | switch v.Type() { 181 | case timeType: 182 | w.h.Reset() 183 | b, err := v.Interface().(time.Time).MarshalBinary() 184 | if err != nil { 185 | return 0, err 186 | } 187 | 188 | err = binary.Write(w.h, binary.LittleEndian, b) 189 | return w.h.Sum64(), err 190 | } 191 | 192 | switch k { 193 | case reflect.Array: 194 | var h uint64 195 | l := v.Len() 196 | for i := 0; i < l; i++ { 197 | current, err := w.visit(v.Index(i), nil) 198 | if err != nil { 199 | return 0, err 200 | } 201 | 202 | h = hashUpdateOrdered(w.h, h, current) 203 | } 204 | 205 | return h, nil 206 | 207 | case reflect.Map: 208 | var includeMap IncludableMap 209 | if opts != nil && opts.Struct != nil { 210 | if v, ok := opts.Struct.(IncludableMap); ok { 211 | includeMap = v 212 | } 213 | } 214 | 215 | // Build the hash for the map. We do this by XOR-ing all the key 216 | // and value hashes. This makes it deterministic despite ordering. 217 | var h uint64 218 | for _, k := range v.MapKeys() { 219 | v := v.MapIndex(k) 220 | if includeMap != nil { 221 | incl, err := includeMap.HashIncludeMap( 222 | opts.StructField, k.Interface(), v.Interface()) 223 | if err != nil { 224 | return 0, err 225 | } 226 | if !incl { 227 | continue 228 | } 229 | } 230 | 231 | kh, err := w.visit(k, nil) 232 | if err != nil { 233 | return 0, err 234 | } 235 | vh, err := w.visit(v, nil) 236 | if err != nil { 237 | return 0, err 238 | } 239 | 240 | fieldHash := hashUpdateOrdered(w.h, kh, vh) 241 | h = hashUpdateUnordered(h, fieldHash) 242 | } 243 | 244 | // Important: read the docs for hashFinishUnordered 245 | h = hashFinishUnordered(w.h, h) 246 | 247 | return h, nil 248 | 249 | case reflect.Struct: 250 | parent := v.Interface() 251 | var include Includable 252 | if impl, ok := parent.(Includable); ok { 253 | include = impl 254 | } 255 | 256 | if impl, ok := parent.(Hashable); ok { 257 | return impl.Hash() 258 | } 259 | 260 | // If we can address this value, check if the pointer value 261 | // implements our interfaces and use that if so. 262 | if v.CanAddr() { 263 | vptr := v.Addr() 264 | parentptr := vptr.Interface() 265 | if impl, ok := parentptr.(Includable); ok { 266 | include = impl 267 | } 268 | 269 | if impl, ok := parentptr.(Hashable); ok { 270 | return impl.Hash() 271 | } 272 | } 273 | 274 | t := v.Type() 275 | h, err := w.visit(reflect.ValueOf(t.Name()), nil) 276 | if err != nil { 277 | return 0, err 278 | } 279 | 280 | l := v.NumField() 281 | for i := 0; i < l; i++ { 282 | if innerV := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { 283 | var f visitFlag 284 | fieldType := t.Field(i) 285 | if fieldType.PkgPath != "" { 286 | // Unexported 287 | continue 288 | } 289 | 290 | tag := fieldType.Tag.Get(w.tag) 291 | if tag == "ignore" || tag == "-" { 292 | // Ignore this field 293 | continue 294 | } 295 | 296 | if w.ignorezerovalue { 297 | if innerV.IsZero() { 298 | continue 299 | } 300 | } 301 | 302 | // if string is set, use the string value 303 | if tag == "string" || w.stringer { 304 | if impl, ok := innerV.Interface().(fmt.Stringer); ok { 305 | innerV = reflect.ValueOf(impl.String()) 306 | } else if tag == "string" { 307 | // We only show this error if the tag explicitly 308 | // requests a stringer. 309 | return 0, &ErrNotStringer{ 310 | Field: v.Type().Field(i).Name, 311 | } 312 | } 313 | } 314 | 315 | // Check if we implement includable and check it 316 | if include != nil { 317 | incl, err := include.HashInclude(fieldType.Name, innerV) 318 | if err != nil { 319 | return 0, err 320 | } 321 | if !incl { 322 | continue 323 | } 324 | } 325 | 326 | switch tag { 327 | case "set": 328 | f |= visitFlagSet 329 | } 330 | 331 | kh, err := w.visit(reflect.ValueOf(fieldType.Name), nil) 332 | if err != nil { 333 | return 0, err 334 | } 335 | 336 | vh, err := w.visit(innerV, &visitOpts{ 337 | Flags: f, 338 | Struct: parent, 339 | StructField: fieldType.Name, 340 | }) 341 | if err != nil { 342 | return 0, err 343 | } 344 | 345 | fieldHash := hashUpdateOrdered(w.h, kh, vh) 346 | h = hashUpdateUnordered(h, fieldHash) 347 | } 348 | // Important: read the docs for hashFinishUnordered 349 | h = hashFinishUnordered(w.h, h) 350 | } 351 | 352 | return h, nil 353 | 354 | case reflect.Slice: 355 | // We have two behaviors here. If it isn't a set, then we just 356 | // visit all the elements. If it is a set, then we do a deterministic 357 | // hash code. 358 | var h uint64 359 | var set bool 360 | if opts != nil { 361 | set = (opts.Flags & visitFlagSet) != 0 362 | } 363 | l := v.Len() 364 | for i := 0; i < l; i++ { 365 | current, err := w.visit(v.Index(i), nil) 366 | if err != nil { 367 | return 0, err 368 | } 369 | 370 | if set || w.sets { 371 | h = hashUpdateUnordered(h, current) 372 | } else { 373 | h = hashUpdateOrdered(w.h, h, current) 374 | } 375 | } 376 | 377 | if set { 378 | // Important: read the docs for hashFinishUnordered 379 | h = hashFinishUnordered(w.h, h) 380 | } 381 | 382 | return h, nil 383 | 384 | case reflect.String: 385 | // Directly hash 386 | w.h.Reset() 387 | _, err := w.h.Write([]byte(v.String())) 388 | return w.h.Sum64(), err 389 | 390 | default: 391 | return 0, fmt.Errorf("unknown kind to hash: %s", k) 392 | } 393 | } 394 | 395 | func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 { 396 | // For ordered updates, use a real hash function 397 | h.Reset() 398 | 399 | // We just panic if the binary writes fail because we are writing 400 | // an int64 which should never be fail-able. 401 | e1 := binary.Write(h, binary.LittleEndian, a) 402 | e2 := binary.Write(h, binary.LittleEndian, b) 403 | if e1 != nil { 404 | panic(e1) 405 | } 406 | if e2 != nil { 407 | panic(e2) 408 | } 409 | 410 | return h.Sum64() 411 | } 412 | 413 | func hashUpdateUnordered(a, b uint64) uint64 { 414 | return a ^ b 415 | } 416 | 417 | // After mixing a group of unique hashes with hashUpdateUnordered, it's always 418 | // necessary to call hashFinishUnordered. Why? Because hashUpdateUnordered 419 | // is a simple XOR, and calling hashUpdateUnordered on hashes produced by 420 | // hashUpdateUnordered can effectively cancel out a previous change to the hash 421 | // result if the same hash value appears later on. For example, consider: 422 | // 423 | // hashUpdateUnordered(hashUpdateUnordered("A", "B"), hashUpdateUnordered("A", "C")) = 424 | // H("A") ^ H("B")) ^ (H("A") ^ H("C")) = 425 | // (H("A") ^ H("A")) ^ (H("B") ^ H(C)) = 426 | // H(B) ^ H(C) = 427 | // hashUpdateUnordered(hashUpdateUnordered("Z", "B"), hashUpdateUnordered("Z", "C")) 428 | // 429 | // hashFinishUnordered "hardens" the result, so that encountering partially 430 | // overlapping input data later on in a different context won't cancel out. 431 | func hashFinishUnordered(h hash.Hash64, a uint64) uint64 { 432 | h.Reset() 433 | 434 | // We just panic if the writes fail 435 | e1 := binary.Write(h, binary.LittleEndian, a) 436 | if e1 != nil { 437 | panic(e1) 438 | } 439 | 440 | return h.Sum64() 441 | } 442 | 443 | // visitFlag is used as a bitmask for affecting visit behavior 444 | type visitFlag uint 445 | 446 | const ( 447 | visitFlagInvalid visitFlag = iota 448 | visitFlagSet = iota << 1 449 | ) 450 | -------------------------------------------------------------------------------- /hashstructure_test.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestHash_identity(t *testing.T) { 11 | cases := []interface{}{ 12 | nil, 13 | "foo", 14 | 42, 15 | true, 16 | false, 17 | []string{"foo", "bar"}, 18 | []interface{}{1, nil, "foo"}, 19 | map[string]string{"foo": "bar"}, 20 | map[interface{}]string{"foo": "bar"}, 21 | map[interface{}]interface{}{"foo": "bar", "bar": 0}, 22 | struct { 23 | Foo string 24 | Bar []interface{} 25 | }{ 26 | Foo: "foo", 27 | Bar: []interface{}{nil, nil, nil}, 28 | }, 29 | &struct { 30 | Foo string 31 | Bar []interface{} 32 | }{ 33 | Foo: "foo", 34 | Bar: []interface{}{nil, nil, nil}, 35 | }, 36 | } 37 | 38 | for _, tc := range cases { 39 | // We run the test 100 times to try to tease out variability 40 | // in the runtime in terms of ordering. 41 | valuelist := make([]uint64, 100) 42 | for i := range valuelist { 43 | v, err := Hash(tc, nil) 44 | if err != nil { 45 | t.Fatalf("Error: %s\n\n%#v", err, tc) 46 | } 47 | 48 | valuelist[i] = v 49 | } 50 | 51 | // Zero is always wrong 52 | if valuelist[0] == 0 { 53 | t.Fatalf("zero hash: %#v", tc) 54 | } 55 | 56 | // Make sure all the values match 57 | t.Logf("%#v: %d", tc, valuelist[0]) 58 | for i := 1; i < len(valuelist); i++ { 59 | if valuelist[i] != valuelist[0] { 60 | t.Fatalf("non-matching: %d, %d\n\n%#v", i, 0, tc) 61 | } 62 | } 63 | } 64 | } 65 | 66 | func TestHash_equal(t *testing.T) { 67 | type testFoo struct{ Name string } 68 | type testBar struct{ Name string } 69 | 70 | now := time.Now() 71 | 72 | cases := []struct { 73 | One, Two interface{} 74 | Match bool 75 | }{ 76 | { 77 | map[string]string{"foo": "bar"}, 78 | map[interface{}]string{"foo": "bar"}, 79 | true, 80 | }, 81 | 82 | { 83 | map[string]interface{}{"1": "1"}, 84 | map[string]interface{}{"1": "1", "2": "2"}, 85 | false, 86 | }, 87 | 88 | { 89 | struct{ Fname, Lname string }{"foo", "bar"}, 90 | struct{ Fname, Lname string }{"bar", "foo"}, 91 | false, 92 | }, 93 | 94 | { 95 | struct{ Lname, Fname string }{"foo", "bar"}, 96 | struct{ Fname, Lname string }{"foo", "bar"}, 97 | false, 98 | }, 99 | 100 | { 101 | struct{ Lname, Fname string }{"foo", "bar"}, 102 | struct{ Fname, Lname string }{"bar", "foo"}, 103 | false, 104 | }, 105 | 106 | { 107 | testFoo{"foo"}, 108 | testBar{"foo"}, 109 | false, 110 | }, 111 | 112 | { 113 | struct { 114 | Foo string 115 | unexported string 116 | }{ 117 | Foo: "bar", 118 | unexported: "baz", 119 | }, 120 | struct { 121 | Foo string 122 | unexported string 123 | }{ 124 | Foo: "bar", 125 | unexported: "bang", 126 | }, 127 | true, 128 | }, 129 | 130 | { 131 | struct { 132 | testFoo 133 | Foo string 134 | }{ 135 | Foo: "bar", 136 | testFoo: testFoo{Name: "baz"}, 137 | }, 138 | struct { 139 | testFoo 140 | Foo string 141 | }{ 142 | Foo: "bar", 143 | }, 144 | true, 145 | }, 146 | 147 | { 148 | struct { 149 | Foo string 150 | }{ 151 | Foo: "bar", 152 | }, 153 | struct { 154 | testFoo 155 | Foo string 156 | }{ 157 | Foo: "bar", 158 | }, 159 | true, 160 | }, 161 | { 162 | now, // contains monotonic clock 163 | time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 164 | now.Minute(), now.Second(), now.Nanosecond(), now.Location()), // does not contain monotonic clock 165 | true, 166 | }, 167 | } 168 | 169 | for i, tc := range cases { 170 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 171 | t.Logf("Hashing: %#v", tc.One) 172 | one, err := Hash(tc.One, nil) 173 | t.Logf("Result: %d", one) 174 | if err != nil { 175 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 176 | } 177 | t.Logf("Hashing: %#v", tc.Two) 178 | two, err := Hash(tc.Two, nil) 179 | t.Logf("Result: %d", two) 180 | if err != nil { 181 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 182 | } 183 | 184 | // Zero is always wrong 185 | if one == 0 { 186 | t.Fatalf("zero hash: %#v", tc.One) 187 | } 188 | 189 | // Compare 190 | if (one == two) != tc.Match { 191 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 192 | } 193 | }) 194 | } 195 | } 196 | 197 | func TestHash_equalIgnore(t *testing.T) { 198 | type Test1 struct { 199 | Name string 200 | UUID string `hash:"ignore"` 201 | } 202 | 203 | type Test2 struct { 204 | Name string 205 | UUID string `hash:"-"` 206 | } 207 | 208 | type TestTime struct { 209 | Name string 210 | Time time.Time `hash:"string"` 211 | } 212 | 213 | type TestTime2 struct { 214 | Name string 215 | Time time.Time 216 | } 217 | 218 | now := time.Now() 219 | cases := []struct { 220 | One, Two interface{} 221 | Match bool 222 | }{ 223 | { 224 | Test1{Name: "foo", UUID: "foo"}, 225 | Test1{Name: "foo", UUID: "bar"}, 226 | true, 227 | }, 228 | 229 | { 230 | Test1{Name: "foo", UUID: "foo"}, 231 | Test1{Name: "foo", UUID: "foo"}, 232 | true, 233 | }, 234 | 235 | { 236 | Test2{Name: "foo", UUID: "foo"}, 237 | Test2{Name: "foo", UUID: "bar"}, 238 | true, 239 | }, 240 | 241 | { 242 | Test2{Name: "foo", UUID: "foo"}, 243 | Test2{Name: "foo", UUID: "foo"}, 244 | true, 245 | }, 246 | { 247 | TestTime{Name: "foo", Time: now}, 248 | TestTime{Name: "foo", Time: time.Time{}}, 249 | false, 250 | }, 251 | { 252 | TestTime{Name: "foo", Time: now}, 253 | TestTime{Name: "foo", Time: now}, 254 | true, 255 | }, 256 | { 257 | TestTime2{Name: "foo", Time: now}, 258 | TestTime2{Name: "foo", Time: time.Time{}}, 259 | false, 260 | }, 261 | { 262 | TestTime2{Name: "foo", Time: now}, 263 | TestTime2{ 264 | Name: "foo", Time: time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 265 | now.Minute(), now.Second(), now.Nanosecond(), now.Location()), 266 | }, 267 | true, 268 | }, 269 | } 270 | 271 | for _, tc := range cases { 272 | one, err := Hash(tc.One, nil) 273 | if err != nil { 274 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 275 | } 276 | two, err := Hash(tc.Two, nil) 277 | if err != nil { 278 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 279 | } 280 | 281 | // Zero is always wrong 282 | if one == 0 { 283 | t.Fatalf("zero hash: %#v", tc.One) 284 | } 285 | 286 | // Compare 287 | if (one == two) != tc.Match { 288 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 289 | } 290 | } 291 | } 292 | 293 | func TestHash_stringTagError(t *testing.T) { 294 | type Test1 struct { 295 | Name string 296 | BrokenField string `hash:"string"` 297 | } 298 | 299 | type Test2 struct { 300 | Name string 301 | BustedField int `hash:"string"` 302 | } 303 | 304 | type Test3 struct { 305 | Name string 306 | Time time.Time `hash:"string"` 307 | } 308 | 309 | cases := []struct { 310 | Test interface{} 311 | Field string 312 | }{ 313 | { 314 | Test1{Name: "foo", BrokenField: "bar"}, 315 | "BrokenField", 316 | }, 317 | { 318 | Test2{Name: "foo", BustedField: 23}, 319 | "BustedField", 320 | }, 321 | { 322 | Test3{Name: "foo", Time: time.Now()}, 323 | "", 324 | }, 325 | } 326 | 327 | for _, tc := range cases { 328 | _, err := Hash(tc.Test, nil) 329 | if err != nil { 330 | if ens, ok := err.(*ErrNotStringer); ok { 331 | if ens.Field != tc.Field { 332 | t.Fatalf("did not get expected field %#v: got %s wanted %s", tc.Test, ens.Field, tc.Field) 333 | } 334 | } else { 335 | t.Fatalf("unknown error %#v: got %s", tc, err) 336 | } 337 | } 338 | } 339 | } 340 | 341 | func TestHash_equalNil(t *testing.T) { 342 | type Test struct { 343 | Str *string 344 | Int *int 345 | Map map[string]string 346 | Slice []string 347 | } 348 | 349 | cases := []struct { 350 | One, Two interface{} 351 | ZeroNil bool 352 | Match bool 353 | }{ 354 | { 355 | Test{ 356 | Str: nil, 357 | Int: nil, 358 | Map: nil, 359 | Slice: nil, 360 | }, 361 | Test{ 362 | Str: new(string), 363 | Int: new(int), 364 | Map: make(map[string]string), 365 | Slice: make([]string, 0), 366 | }, 367 | true, 368 | true, 369 | }, 370 | { 371 | Test{ 372 | Str: nil, 373 | Int: nil, 374 | Map: nil, 375 | Slice: nil, 376 | }, 377 | Test{ 378 | Str: new(string), 379 | Int: new(int), 380 | Map: make(map[string]string), 381 | Slice: make([]string, 0), 382 | }, 383 | false, 384 | false, 385 | }, 386 | { 387 | nil, 388 | 0, 389 | true, 390 | true, 391 | }, 392 | { 393 | nil, 394 | 0, 395 | false, 396 | true, 397 | }, 398 | } 399 | 400 | for _, tc := range cases { 401 | one, err := Hash(tc.One, &HashOptions{ZeroNil: tc.ZeroNil}) 402 | if err != nil { 403 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 404 | } 405 | two, err := Hash(tc.Two, &HashOptions{ZeroNil: tc.ZeroNil}) 406 | if err != nil { 407 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 408 | } 409 | 410 | // Zero is always wrong 411 | if one == 0 { 412 | t.Fatalf("zero hash: %#v", tc.One) 413 | } 414 | 415 | // Compare 416 | if (one == two) != tc.Match { 417 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 418 | } 419 | } 420 | } 421 | 422 | func TestHash_equalSet(t *testing.T) { 423 | type Test struct { 424 | Name string 425 | Friends []string `hash:"set"` 426 | } 427 | 428 | cases := []struct { 429 | One, Two interface{} 430 | Match bool 431 | }{ 432 | { 433 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 434 | Test{Name: "foo", Friends: []string{"bar", "foo"}}, 435 | true, 436 | }, 437 | 438 | { 439 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 440 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 441 | true, 442 | }, 443 | } 444 | 445 | for _, tc := range cases { 446 | one, err := Hash(tc.One, nil) 447 | if err != nil { 448 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 449 | } 450 | two, err := Hash(tc.Two, nil) 451 | if err != nil { 452 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 453 | } 454 | 455 | // Zero is always wrong 456 | if one == 0 { 457 | t.Fatalf("zero hash: %#v", tc.One) 458 | } 459 | 460 | // Compare 461 | if (one == two) != tc.Match { 462 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 463 | } 464 | } 465 | } 466 | 467 | func TestHash_includable(t *testing.T) { 468 | cases := []struct { 469 | One, Two interface{} 470 | Match bool 471 | }{ 472 | { 473 | testIncludable{Value: "foo"}, 474 | testIncludable{Value: "foo"}, 475 | true, 476 | }, 477 | 478 | { 479 | testIncludable{Value: "foo", Ignore: "bar"}, 480 | testIncludable{Value: "foo"}, 481 | true, 482 | }, 483 | 484 | { 485 | testIncludable{Value: "foo", Ignore: "bar"}, 486 | testIncludable{Value: "bar"}, 487 | false, 488 | }, 489 | } 490 | 491 | for _, tc := range cases { 492 | one, err := Hash(tc.One, nil) 493 | if err != nil { 494 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 495 | } 496 | two, err := Hash(tc.Two, nil) 497 | if err != nil { 498 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 499 | } 500 | 501 | // Zero is always wrong 502 | if one == 0 { 503 | t.Fatalf("zero hash: %#v", tc.One) 504 | } 505 | 506 | // Compare 507 | if (one == two) != tc.Match { 508 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 509 | } 510 | } 511 | } 512 | 513 | func TestHash_ignoreZeroValue(t *testing.T) { 514 | cases := []struct { 515 | IgnoreZeroValue bool 516 | }{ 517 | { 518 | IgnoreZeroValue: true, 519 | }, 520 | { 521 | IgnoreZeroValue: false, 522 | }, 523 | } 524 | structA := struct { 525 | Foo string 526 | Bar string 527 | Map map[string]int 528 | }{ 529 | Foo: "foo", 530 | Bar: "bar", 531 | } 532 | structB := struct { 533 | Foo string 534 | Bar string 535 | Baz string 536 | Map map[string]int 537 | }{ 538 | Foo: "foo", 539 | Bar: "bar", 540 | } 541 | 542 | for _, tc := range cases { 543 | hashA, err := Hash(structA, &HashOptions{IgnoreZeroValue: tc.IgnoreZeroValue}) 544 | if err != nil { 545 | t.Fatalf("Failed to hash %#v: %s", structA, err) 546 | } 547 | hashB, err := Hash(structB, &HashOptions{IgnoreZeroValue: tc.IgnoreZeroValue}) 548 | if err != nil { 549 | t.Fatalf("Failed to hash %#v: %s", structB, err) 550 | } 551 | if (hashA == hashB) != tc.IgnoreZeroValue { 552 | t.Fatalf("bad, expected: %#v\n\n%d\n\n%d", tc.IgnoreZeroValue, hashA, hashB) 553 | } 554 | } 555 | } 556 | 557 | func TestHash_includableMap(t *testing.T) { 558 | cases := []struct { 559 | One, Two interface{} 560 | Match bool 561 | }{ 562 | { 563 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 564 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 565 | true, 566 | }, 567 | 568 | { 569 | testIncludableMap{Map: map[string]string{"foo": "bar", "ignore": "true"}}, 570 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 571 | true, 572 | }, 573 | 574 | { 575 | testIncludableMap{Map: map[string]string{"foo": "bar", "ignore": "true"}}, 576 | testIncludableMap{Map: map[string]string{"bar": "baz"}}, 577 | false, 578 | }, 579 | } 580 | 581 | for _, tc := range cases { 582 | one, err := Hash(tc.One, nil) 583 | if err != nil { 584 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 585 | } 586 | two, err := Hash(tc.Two, nil) 587 | if err != nil { 588 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 589 | } 590 | 591 | // Zero is always wrong 592 | if one == 0 { 593 | t.Fatalf("zero hash: %#v", tc.One) 594 | } 595 | 596 | // Compare 597 | if (one == two) != tc.Match { 598 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 599 | } 600 | } 601 | } 602 | 603 | func TestHash_hashable(t *testing.T) { 604 | cases := []struct { 605 | One, Two interface{} 606 | Match bool 607 | Err string 608 | }{ 609 | { 610 | testHashable{Value: "foo"}, 611 | &testHashablePointer{Value: "foo"}, 612 | true, 613 | "", 614 | }, 615 | 616 | { 617 | testHashable{Value: "foo1"}, 618 | &testHashablePointer{Value: "foo2"}, 619 | true, 620 | "", 621 | }, 622 | { 623 | testHashable{Value: "foo"}, 624 | &testHashablePointer{Value: "bar"}, 625 | false, 626 | "", 627 | }, 628 | { 629 | testHashable{Value: "nofoo"}, 630 | &testHashablePointer{Value: "bar"}, 631 | true, 632 | "", 633 | }, 634 | { 635 | testHashable{Value: "bar", Err: fmt.Errorf("oh no")}, 636 | testHashable{Value: "bar"}, 637 | true, 638 | "oh no", 639 | }, 640 | } 641 | 642 | for i, tc := range cases { 643 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 644 | one, err := Hash(tc.One, nil) 645 | if tc.Err != "" { 646 | if err == nil { 647 | t.Fatal("expected error") 648 | } 649 | 650 | if !strings.Contains(err.Error(), tc.Err) { 651 | t.Fatalf("expected error to contain %q, got: %s", tc.Err, err) 652 | } 653 | 654 | return 655 | } 656 | if err != nil { 657 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 658 | } 659 | 660 | two, err := Hash(tc.Two, nil) 661 | if err != nil { 662 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 663 | } 664 | 665 | // Zero is always wrong 666 | if one == 0 { 667 | t.Fatalf("zero hash: %#v", tc.One) 668 | } 669 | 670 | // Compare 671 | if (one == two) != tc.Match { 672 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 673 | } 674 | }) 675 | } 676 | } 677 | 678 | type testIncludable struct { 679 | Value string 680 | Ignore string 681 | } 682 | 683 | func (t testIncludable) HashInclude(field string, v interface{}) (bool, error) { 684 | return field != "Ignore", nil 685 | } 686 | 687 | type testIncludableMap struct { 688 | Map map[string]string 689 | } 690 | 691 | func (t testIncludableMap) HashIncludeMap(field string, k, v interface{}) (bool, error) { 692 | if field != "Map" { 693 | return true, nil 694 | } 695 | 696 | if s, ok := k.(string); ok && s == "ignore" { 697 | return false, nil 698 | } 699 | 700 | return true, nil 701 | } 702 | 703 | type testHashable struct { 704 | Value string 705 | Err error 706 | } 707 | 708 | func (t testHashable) Hash() (uint64, error) { 709 | if t.Err != nil { 710 | return 0, t.Err 711 | } 712 | 713 | if strings.HasPrefix(t.Value, "foo") { 714 | return 500, nil 715 | } 716 | 717 | return 100, nil 718 | } 719 | 720 | type testHashablePointer struct { 721 | Value string 722 | } 723 | 724 | func (t *testHashablePointer) Hash() (uint64, error) { 725 | if strings.HasPrefix(t.Value, "foo") { 726 | return 500, nil 727 | } 728 | 729 | return 100, nil 730 | } 731 | --------------------------------------------------------------------------------