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

Commit

Permalink
Merge pull request #11 from fortytw2/master
Browse files Browse the repository at this point in the history
add hash:"string" tags support
  • Loading branch information
mitchellh authored May 11, 2017
2 parents ab25296 + fe40ba3 commit 9204ce5
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 6 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ sending data across the network, caching values locally (de-dup), and so on.

* Optionally specify a custom hash function to optimize for speed, collision
avoidance for your data set, etc.

* Optionally hash the output of `.String()` on structs that implement fmt.Stringer,
allowing effective hashing of time.Time

## Installation

Expand Down
36 changes: 30 additions & 6 deletions hashstructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import (
"reflect"
)

// ErrNotStringer is returned when there's an error with hash:"string"
type ErrNotStringer struct {
Field string
}

// Error implements error for ErrNotStringer
func (ens *ErrNotStringer) Error() string {
return fmt.Sprintf("hashstructure: %s has hash:\"string\" set, but does not implement fmt.Stringer", ens.Field)
}

// HashOptions are options that are available for hashing.
type HashOptions struct {
// Hasher is the hash function to use. If this isn't set, it will
Expand All @@ -27,8 +37,8 @@ type HashOptions struct {
//
// If opts is nil, then default options will be used. See HashOptions
// for the default values. The same *HashOptions value cannot be used
// concurrently. None of the values within a *HashOptions struct are
// safe to read/write while hashing is being done.
// concurrently. None of the values within a *HashOptions struct are
// safe to read/write while hashing is being done.
//
// Notes on the value:
//
Expand All @@ -52,6 +62,9 @@ type HashOptions struct {
// * "set" - The field will be treated as a set, where ordering doesn't
// affect the hash code. This only works for slices.
//
// * "string" - The field will be hashed as a string, only works when the
// field implements fmt.Stringer
//
func Hash(v interface{}, opts *HashOptions) (uint64, error) {
// Create default options
if opts == nil {
Expand Down Expand Up @@ -201,8 +214,8 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
return h, nil

case reflect.Struct:
var include Includable
parent := v.Interface()
var include Includable
if impl, ok := parent.(Includable); ok {
include = impl
}
Expand All @@ -215,7 +228,7 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {

l := v.NumField()
for i := 0; i < l; i++ {
if v := v.Field(i); v.CanSet() || t.Field(i).Name != "_" {
if innerV := v.Field(i); v.CanSet() || t.Field(i).Name != "_" {
var f visitFlag
fieldType := t.Field(i)
if fieldType.PkgPath != "" {
Expand All @@ -229,9 +242,20 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
continue
}

// if string is set, use the string value
if tag == "string" {
if impl, ok := innerV.Interface().(fmt.Stringer); ok {
innerV = reflect.ValueOf(impl.String())
} else {
return 0, &ErrNotStringer{
Field: v.Type().Field(i).Name,
}
}
}

// Check if we implement includable and check it
if include != nil {
incl, err := include.HashInclude(fieldType.Name, v)
incl, err := include.HashInclude(fieldType.Name, innerV)
if err != nil {
return 0, err
}
Expand All @@ -250,7 +274,7 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
return 0, err
}

vh, err := w.visit(v, &visitOpts{
vh, err := w.visit(innerV, &visitOpts{
Flags: f,
Struct: parent,
StructField: fieldType.Name,
Expand Down
75 changes: 75 additions & 0 deletions hashstructure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package hashstructure
import (
"fmt"
"testing"
"time"
)

func TestHash_identity(t *testing.T) {
Expand Down Expand Up @@ -195,6 +196,17 @@ func TestHash_equalIgnore(t *testing.T) {
UUID string `hash:"-"`
}

type TestTime struct {
Name string
Time time.Time `hash:"string"`
}

type TestTime2 struct {
Name string
Time time.Time
}

now := time.Now()
cases := []struct {
One, Two interface{}
Match bool
Expand Down Expand Up @@ -222,6 +234,21 @@ func TestHash_equalIgnore(t *testing.T) {
Test2{Name: "foo", UUID: "foo"},
true,
},
{
TestTime{Name: "foo", Time: now},
TestTime{Name: "foo", Time: time.Time{}},
false,
},
{
TestTime{Name: "foo", Time: now},
TestTime{Name: "foo", Time: now},
true,
},
{
TestTime2{Name: "foo", Time: now},
TestTime2{Name: "foo", Time: time.Time{}},
true,
},
}

for _, tc := range cases {
Expand All @@ -246,6 +273,54 @@ func TestHash_equalIgnore(t *testing.T) {
}
}

func TestHash_stringTagError(t *testing.T) {
type Test1 struct {
Name string
BrokenField string `hash:"string"`
}

type Test2 struct {
Name string
BustedField int `hash:"string"`
}

type Test3 struct {
Name string
Time time.Time `hash:"string"`
}

cases := []struct {
Test interface{}
Field string
}{
{
Test1{Name: "foo", BrokenField: "bar"},
"BrokenField",
},
{
Test2{Name: "foo", BustedField: 23},
"BustedField",
},
{
Test3{Name: "foo", Time: time.Now()},
"",
},
}

for _, tc := range cases {
_, err := Hash(tc.Test, nil)
if err != nil {
if ens, ok := err.(*ErrNotStringer); ok {
if ens.Field != tc.Field {
t.Fatalf("did not get expected field %#v: got %s wanted %s", tc.Test, ens.Field, tc.Field)
}
} else {
t.Fatalf("unknown error %#v: got %s", tc, err)
}
}
}
}

func TestHash_equalNil(t *testing.T) {
type Test struct {
Str *string
Expand Down

0 comments on commit 9204ce5

Please sign in to comment.