-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathflexid.go
More file actions
280 lines (242 loc) · 7.72 KB
/
Copy pathflexid.go
File metadata and controls
280 lines (242 loc) · 7.72 KB
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
package flexid
import (
"crypto/rand"
"errors"
"fmt"
"io"
"math" // Import needed for the comment explanation
"strings"
"time"
)
// DefaultAlphabet is the standard base62 alphabet (0-9, A-Z, a-z).
const (
DefaultAlphabet = Base62Alphabet
Base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
Base36Alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
Base16LowerAlphabet = "0123456789abcdef"
Base16UpperAlphabet = "0123456789ABCDEF"
Base64UrlAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
// CrockfordBase32Alphabet is designed for human readability and is case-insensitive (excludes I, L, O, U).
CrockfordBase32Alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
)
// Common tick size durations.
const (
Nanosecond = time.Nanosecond
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Centisecond = 10 * Millisecond
Decisecond = 100 * Millisecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
Day = 24 * Hour
)
// Config holds the configuration for generating short TIDs.
type Config struct {
epoch time.Time // The starting point for the time component (UTC recommended).
tickSize time.Duration // The tick size of the time component.
alphabet string // The alphabet used for encoding timestamp and random parts.
numRandomChars int // The number of random characters to append.
timeProvider func() time.Time // Function to provide the current time (for testing).
randomSource io.Reader // Source of randomness (for testing).
}
// Generator is responsible for generating TIDs based on a fixed configuration.
type Generator struct {
config Config
base int // Cache the base (length of alphabet)
}
var (
DefaultEpoch = time.Unix(0, 0).UTC()
defaultGenerator *Generator
)
func init() {
var err error
defaultGenerator, err = NewGenerator(NewConfig())
if err != nil {
panic("flexid: failed to initialize default generator: " + err.Error())
}
}
// NewConfig returns a default configuration:
// - epoch: Unix epoch (1970-01-01 00:00:00 UTC)
// - TickSize: Millisecond
// - alphabet: Base62
// - numRandomChars: 5
func NewConfig() Config {
return Config{
epoch: DefaultEpoch,
tickSize: Millisecond,
alphabet: DefaultAlphabet,
numRandomChars: 5,
randomSource: rand.Reader,
timeProvider: time.Now,
}
}
// WithEpoch sets the epoch for the generator.
func (c Config) WithEpoch(epoch time.Time) Config {
c.epoch = epoch
return c
}
// WithTickSize sets the tick size for the time component of the generator.
func (c Config) WithTickSize(tickSize time.Duration) Config {
c.tickSize = tickSize
return c
}
// WithAlphabet sets the alphabet for the generator.
func (c Config) WithAlphabet(alphabet string) Config {
c.alphabet = alphabet
return c
}
// WithNumRandomChars sets the number of random characters for the generator.
func (c Config) WithNumRandomChars(numRandomChars int) Config {
c.numRandomChars = numRandomChars
return c
}
// WithRandomSource sets the random source for the generator.
func (c Config) WithRandomSource(randomSource io.Reader) Config {
c.randomSource = randomSource
return c
}
// WithTimeProvider sets the time provider for the generator.
func (c Config) WithTimeProvider(timeProvider func() time.Time) Config {
c.timeProvider = timeProvider
return c
}
// NewGenerator creates a new Generator instance with the given configuration.
// It validates the configuration upon creation.
func NewGenerator(config Config) (*Generator, error) {
if len(config.alphabet) < 2 {
return nil, errors.New("alphabet must contain at least 2 characters")
}
if config.numRandomChars < 0 {
return nil, errors.New("number of random characters cannot be negative")
}
err := validateAlphabet(config.alphabet)
if err != nil {
return nil, err
}
return &Generator{
config: config,
base: len(config.alphabet),
}, nil
}
func MustNewGenerator(config Config) *Generator {
generator, err := NewGenerator(config)
if err != nil {
panic("flexid: failed to create generator: " + err.Error())
}
return generator
}
// Generate creates a new short TID using the generator's configuration.
func (g *Generator) Generate() (string, error) {
// 1. Calculate timestamp ticks since configured epoch
now := g.config.timeProvider().UTC()
// Check if current time is before the configured epoch. This must be done
// here, as 'now' is only known at generation time. Allow generation at epoch time.
if now.Before(g.config.epoch) {
return "", errors.New("current time is before the configured epoch")
}
// 2. Encode timestamp ticks (if applicable)
encodedTimestamp := ""
if g.config.tickSize > 0 {
delta := now.Sub(g.config.epoch)
ticks := uint64(delta.Nanoseconds() / int64(g.config.tickSize))
encoded, err := g.encodeBaseN(ticks)
if err != nil {
return "", err
}
encodedTimestamp = encoded
}
// 3. Generate random part
randomPart := ""
if g.config.numRandomChars > 0 {
chars, err := g.generateRandomChars(g.config.numRandomChars)
if err != nil {
return "", err
}
randomPart = chars
}
// 4. Combine parts
var sb strings.Builder
sb.WriteString(encodedTimestamp)
sb.WriteString(randomPart)
return sb.String(), nil
}
// Generate generates a TID using the default configuration.
// It panics if the internal default generator failed to initialize.
func Generate() (string, error) {
if defaultGenerator == nil {
panic("flexid: default generator not initialized")
}
return defaultGenerator.Generate()
}
func MustGenerate() string {
id, err := Generate()
if err != nil {
panic("flexid: failed to generate TID: " + err.Error())
}
return id
}
func (g *Generator) MustGenerate() string {
id, err := g.Generate()
if err != nil {
panic("flexid: failed to generate TID: " + err.Error())
}
return id
}
// encodeBaseN encodes a non-negative integer using the generator's alphabet.
func (g *Generator) encodeBaseN(number uint64) (string, error) {
if number == 0 {
return string(g.config.alphabet[0]), nil
}
// Estimate buffer size: log_base(number). Rough estimate is fine.
bufSize := int(64/math.Log2(float64(g.base))) + 2 // Add 2 for safety
buf := make([]byte, bufSize)
i := bufSize - 1
for number > 0 {
if i < 0 {
return "", errors.New("buffer size estimation failed in encodeBaseN")
}
remainder := number % uint64(g.base)
buf[i] = g.config.alphabet[remainder]
number /= uint64(g.base)
i--
}
return string(buf[i+1:]), nil
}
// generateRandomChars generates a cryptographically secure random string of the specified length
// using the generator's alphabet, avoiding modulo bias via rejection sampling.
func (g *Generator) generateRandomChars(length int) (string, error) {
if length == 0 {
return "", nil
}
bytes := make([]byte, length) // Buffer for resulting characters
randomBytes := make([]byte, length) // Temporary buffer for OS random bytes
maxValidByte := byte((256/g.base)*g.base - 1)
for i := 0; i < length; {
if _, err := io.ReadFull(g.config.randomSource, randomBytes); err != nil {
return "", errors.New("failed to read random bytes: " + err.Error())
}
for _, randomByte := range randomBytes {
if randomByte <= maxValidByte {
bytes[i] = g.config.alphabet[int(randomByte)%g.base]
i++
if i == length {
break // Got all required characters
}
}
// Discard biased byte (randomByte > maxValidByte) and continue
}
}
return string(bytes), nil
}
// ensure alphabet doesn't contain duplicates
func validateAlphabet(alphabet string) error {
seen := make(map[rune]struct{})
for _, ch := range alphabet {
if _, exists := seen[ch]; exists {
return fmt.Errorf("alphabet contains duplicate character: %c", ch)
}
seen[ch] = struct{}{}
}
return nil
}