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

add hash:"string" tags support #11

Merged
merged 2 commits into from
May 11, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
29 changes: 23 additions & 6 deletions hashstructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ package hashstructure

import (
"encoding/binary"
"errors"
"fmt"
"hash"
"hash/fnv"
"reflect"
)

var (
ErrNotStringer = errors.New("field tag set to string, but struct does not implement fmt.Stringer")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should include the field name in the error output otherwise this can be pretty vague.

)

// 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 +32,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 +57,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 +209,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 +223,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 +237,18 @@ 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
}
}

// 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 +267,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
27 changes: 27 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 Down