forked from Nordstrom/choices
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhash.go
160 lines (133 loc) · 3.79 KB
/
hash.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// Copyright 2016 Andrew O'Neill, Nordstrom
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package choices
import (
"bytes"
"crypto/sha1"
"encoding/binary"
"errors"
)
const longScale = float64(0xFFFFFFFFFFFFFFFF)
var (
// globalSalt should only ever be set once.
globalSalt = "choices"
)
var (
calledSet bool
// ErrGlobalSaltAlreadySet is the error returned when a
// SetGlobalSalt has been called more than once.
ErrGlobalSaltAlreadySet = errors.New("global salt already set")
)
// SetGlobalSalt this sets the global salt used for hashing users. It
// should only ever be called once. It returns an error if it is
// called more than once.
func SetGlobalSalt(s string) error {
if calledSet {
return ErrGlobalSaltAlreadySet
}
globalSalt = s
calledSet = true
return nil
}
// WithGlobalSalt is a configuration option for Config that sets the
// globalSalt to something other than the default.
func WithGlobalSalt(s string) ConfigOpt {
return func(c *Config) error {
globalSalt = s
return nil
}
}
// hashConfig is a struct to store the hash data.
type hashConfig struct {
// salt has 3 parts namespace is index 0, experiment is index 1, and param is index 2
salt [3]string
userID string
}
// HashExperience takes the supplied arguments and returns the hashed
// uint64 that can be used for determining a segment.
func HashExperience(namespace, experiment, param, userID string) (uint64, error) {
h := hashConfig{userID: userID}
h.setNs(namespace)
h.setExp(experiment)
h.setParam(param)
return hash(h)
}
// setNs sets the namespaces portion of the salt
func (h *hashConfig) setNs(ns string) {
h.salt[0] = ns
}
// setExp sets the experiment portion of the salt
func (h *hashConfig) setExp(exp string) {
h.salt[1] = exp
}
// setParam sets the param portion of the salt
func (h *hashConfig) setParam(p string) {
h.salt[2] = p
}
// setUserID sets the userID
func (h *hashConfig) setUserID(u string) {
h.userID = u
}
// errWriter is a convenience struct to eliminate lots of if err !=
// nil.
type errWriter struct {
buf bytes.Buffer
err error
}
// writeString writes the given string to the buf or nothing if err !=
// nil
func (e *errWriter) writeString(s string) {
if e.err == nil {
_, e.err = e.buf.WriteString(s)
}
}
// writeByte writes a single byte to the buf or nothing if err != nil
func (e *errWriter) writeByte(b byte) {
if e.err == nil {
e.err = e.buf.WriteByte(b)
}
}
// Bytes returns a []byte that represents the entire salt+userID the
// format is as follows.
// "globalSalt.namespace.experiment.param@userID"
func (h *hashConfig) Bytes() ([]byte, error) {
ew := errWriter{}
ew.writeString(globalSalt)
ew.writeByte('.')
for i, v := range h.salt {
ew.writeString(v)
if i < len(h.salt)-1 {
ew.writeByte('.')
}
}
ew.writeByte('@')
ew.writeString(h.userID)
if ew.err != nil {
return nil, ew.err
}
return ew.buf.Bytes(), nil
}
// hash hashes the hashConfig returning a uint64 hashed value or an
// error.
func hash(h hashConfig) (uint64, error) {
b, err := h.Bytes()
if err != nil {
return 0, err
}
hash := sha1.Sum(b)
i := binary.BigEndian.Uint64(hash[:8])
return i, nil
}
// uniform returns a uniformly random value between the min and max
// values supplied.
func uniform(hash uint64, min, max float64) float64 {
return min + (max-min)*(float64(hash)/longScale)
}