opaque_id 1.4.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +206 -0
- data/README.md +93 -79
- data/docs/_config.yml +2 -0
- data/docs/algorithms.md +4 -4
- data/docs/api-reference.md +6 -6
- data/docs/benchmarks.md +385 -0
- data/docs/configuration.md +32 -18
- data/docs/index.md +21 -3
- data/docs/performance.md +3 -3
- data/docs/usage.md +22 -18
- data/lib/generators/opaque_id/install_generator.rb +13 -0
- data/lib/generators/opaque_id/templates/migration.rb.tt +3 -3
- data/lib/opaque_id/model.rb +2 -2
- data/lib/opaque_id/version.rb +1 -1
- data/lib/opaque_id.rb +4 -1
- data/release-please-config.json +2 -1
- metadata +2 -9
- data/tasks/0001-prd-opaque-id-gem.md +0 -202
- data/tasks/0002-prd-publishing-release-automation.md +0 -206
- data/tasks/0003-prd-documentation-site.md +0 -191
- data/tasks/references/opaque_gem_requirements.md +0 -482
- data/tasks/references/original_identifiable_concern_and_nanoid.md +0 -110
- data/tasks/tasks-0001-prd-opaque-id-gem.md +0 -109
- data/tasks/tasks-0002-prd-publishing-release-automation.md +0 -177
- data/tasks/tasks-0003-prd-documentation-site.md +0 -84
data/docs/algorithms.md
CHANGED
@@ -8,7 +8,7 @@ permalink: /algorithms/
|
|
8
8
|
|
9
9
|
# Algorithms
|
10
10
|
|
11
|
-
OpaqueId
|
11
|
+
OpaqueId builds on Ruby's built-in `SecureRandom` methods to generate cryptographically secure, collision-free opaque IDs. This guide explains the technical details behind the generation process, optimization strategies, and mathematical foundations.
|
12
12
|
|
13
13
|
- TOC
|
14
14
|
{:toc}
|
@@ -45,10 +45,10 @@ end
|
|
45
45
|
|
46
46
|
### Key Features
|
47
47
|
|
48
|
-
- **Bitwise Operations**: Uses `byte & 63` instead of `byte % 64` for
|
48
|
+
- **Bitwise Operations**: Uses `byte & 63` instead of `byte % 64` for efficient computation
|
49
49
|
- **No Modulo Bias**: 64 is a power of 2, so bitwise AND provides uniform distribution
|
50
50
|
- **Single Random Call**: One `SecureRandom.random_bytes(1)` call per character
|
51
|
-
- **
|
51
|
+
- **Performance Optimized**: Designed for speed with 64-character alphabets
|
52
52
|
|
53
53
|
### Mathematical Foundation
|
54
54
|
|
@@ -60,7 +60,7 @@ For a 64-character alphabet:
|
|
60
60
|
|
61
61
|
### Performance Characteristics
|
62
62
|
|
63
|
-
- **Optimized for speed**: Uses bitwise operations for
|
63
|
+
- **Optimized for speed**: Uses bitwise operations for efficient performance
|
64
64
|
- **No rejection sampling**: All generated bytes are used efficiently
|
65
65
|
- **Linear time complexity**: O(n) where n is the ID length
|
66
66
|
|
data/docs/api-reference.md
CHANGED
@@ -19,20 +19,20 @@ The main module for generating opaque IDs.
|
|
19
19
|
|
20
20
|
### Constants
|
21
21
|
|
22
|
-
####
|
22
|
+
#### SLUG_LIKE_ALPHABET
|
23
23
|
|
24
24
|
Default alphabet for ID generation.
|
25
25
|
|
26
26
|
```ruby
|
27
|
-
OpaqueId::
|
28
|
-
# => "
|
27
|
+
OpaqueId::SLUG_LIKE_ALPHABET
|
28
|
+
# => "0123456789abcdefghijklmnopqrstuvwxyz"
|
29
29
|
```
|
30
30
|
|
31
31
|
**Characteristics:**
|
32
32
|
|
33
|
-
- **Length**:
|
34
|
-
- **Characters**:
|
35
|
-
- **Use case**:
|
33
|
+
- **Length**: 36 characters
|
34
|
+
- **Characters**: 0-9, a-z
|
35
|
+
- **Use case**: URL-friendly, double-click selectable
|
36
36
|
- **Performance**: Good
|
37
37
|
|
38
38
|
#### STANDARD_ALPHABET
|
data/docs/benchmarks.md
ADDED
@@ -0,0 +1,385 @@
|
|
1
|
+
# OpaqueId Benchmarks
|
2
|
+
|
3
|
+
This document provides benchmark scripts that you can run to test OpaqueId's performance and uniqueness characteristics on your own system.
|
4
|
+
|
5
|
+
## Performance Benchmarks
|
6
|
+
|
7
|
+
### SecureRandom Comparison Test
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
#!/usr/bin/env ruby
|
11
|
+
|
12
|
+
require 'opaque_id'
|
13
|
+
require 'securerandom'
|
14
|
+
|
15
|
+
puts "OpaqueId vs SecureRandom Comparison"
|
16
|
+
puts "=" * 50
|
17
|
+
|
18
|
+
# Test different Ruby standard library methods
|
19
|
+
methods = {
|
20
|
+
'OpaqueId.generate' => -> { OpaqueId.generate },
|
21
|
+
'SecureRandom.urlsafe_base64' => -> { SecureRandom.urlsafe_base64 },
|
22
|
+
'SecureRandom.urlsafe_base64(16)' => -> { SecureRandom.urlsafe_base64(16) },
|
23
|
+
'SecureRandom.hex(9)' => -> { SecureRandom.hex(9) },
|
24
|
+
'SecureRandom.alphanumeric(18)' => -> { SecureRandom.alphanumeric(18) }
|
25
|
+
}
|
26
|
+
|
27
|
+
count = 10000
|
28
|
+
|
29
|
+
puts "Performance comparison (#{count} IDs each):"
|
30
|
+
puts "-" * 50
|
31
|
+
|
32
|
+
methods.each do |name, method|
|
33
|
+
start_time = Time.now
|
34
|
+
ids = count.times.map { method.call }
|
35
|
+
end_time = Time.now
|
36
|
+
duration = end_time - start_time
|
37
|
+
rate = (count / duration).round(0)
|
38
|
+
|
39
|
+
# Check uniqueness
|
40
|
+
unique_count = ids.uniq.length
|
41
|
+
collisions = count - unique_count
|
42
|
+
|
43
|
+
# Check characteristics
|
44
|
+
sample_id = ids.first
|
45
|
+
length = sample_id.length
|
46
|
+
has_uppercase = sample_id.match?(/[A-Z]/)
|
47
|
+
has_lowercase = sample_id.match?(/[a-z]/)
|
48
|
+
has_numbers = sample_id.match?(/[0-9]/)
|
49
|
+
has_special = sample_id.match?(/[^A-Za-z0-9]/)
|
50
|
+
|
51
|
+
puts "#{name.ljust(30)}: #{duration.round(4)}s (#{rate} IDs/sec)"
|
52
|
+
puts " Length: #{length}, Collisions: #{collisions}"
|
53
|
+
puts " Sample: '#{sample_id}'"
|
54
|
+
puts " Chars: #{has_uppercase ? 'A-Z' : ''}#{has_lowercase ? 'a-z' : ''}#{has_numbers ? '0-9' : ''}#{has_special ? 'special' : ''}"
|
55
|
+
puts
|
56
|
+
end
|
57
|
+
|
58
|
+
puts "Comparison completed."
|
59
|
+
```
|
60
|
+
|
61
|
+
### Basic Performance Test
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
#!/usr/bin/env ruby
|
65
|
+
|
66
|
+
require 'opaque_id'
|
67
|
+
|
68
|
+
puts "OpaqueId Performance Benchmark"
|
69
|
+
puts "=" * 40
|
70
|
+
|
71
|
+
# Test different batch sizes
|
72
|
+
[100, 1000, 10000, 100000].each do |count|
|
73
|
+
start_time = Time.now
|
74
|
+
count.times { OpaqueId.generate }
|
75
|
+
end_time = Time.now
|
76
|
+
duration = end_time - start_time
|
77
|
+
rate = (count / duration).round(0)
|
78
|
+
|
79
|
+
puts "#{count.to_s.rjust(6)} IDs: #{duration.round(4)}s (#{rate} IDs/sec)"
|
80
|
+
end
|
81
|
+
|
82
|
+
puts "\nPerformance test completed."
|
83
|
+
```
|
84
|
+
|
85
|
+
### Alphabet Performance Comparison
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
#!/usr/bin/env ruby
|
89
|
+
|
90
|
+
require 'opaque_id'
|
91
|
+
|
92
|
+
puts "Alphabet Performance Comparison"
|
93
|
+
puts "=" * 40
|
94
|
+
|
95
|
+
alphabets = {
|
96
|
+
'SLUG_LIKE_ALPHABET' => OpaqueId::SLUG_LIKE_ALPHABET,
|
97
|
+
'ALPHANUMERIC_ALPHABET' => OpaqueId::ALPHANUMERIC_ALPHABET,
|
98
|
+
'STANDARD_ALPHABET' => OpaqueId::STANDARD_ALPHABET
|
99
|
+
}
|
100
|
+
|
101
|
+
count = 10000
|
102
|
+
|
103
|
+
alphabets.each do |name, alphabet|
|
104
|
+
start_time = Time.now
|
105
|
+
count.times { OpaqueId.generate(alphabet: alphabet) }
|
106
|
+
end_time = Time.now
|
107
|
+
duration = end_time - start_time
|
108
|
+
rate = (count / duration).round(0)
|
109
|
+
|
110
|
+
puts "#{name.ljust(20)}: #{duration.round(4)}s (#{rate} IDs/sec)"
|
111
|
+
end
|
112
|
+
|
113
|
+
puts "\nAlphabet comparison completed."
|
114
|
+
```
|
115
|
+
|
116
|
+
### Size Performance Test
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
#!/usr/bin/env ruby
|
120
|
+
|
121
|
+
require 'opaque_id'
|
122
|
+
|
123
|
+
puts "Size Performance Test"
|
124
|
+
puts "=" * 40
|
125
|
+
|
126
|
+
sizes = [8, 12, 18, 24, 32, 48, 64]
|
127
|
+
count = 10000
|
128
|
+
|
129
|
+
sizes.each do |size|
|
130
|
+
start_time = Time.now
|
131
|
+
count.times { OpaqueId.generate(size: size) }
|
132
|
+
end_time = Time.now
|
133
|
+
duration = end_time - start_time
|
134
|
+
rate = (count / duration).round(0)
|
135
|
+
|
136
|
+
puts "Size #{size.to_s.rjust(2)}: #{duration.round(4)}s (#{rate} IDs/sec)"
|
137
|
+
end
|
138
|
+
|
139
|
+
puts "\nSize performance test completed."
|
140
|
+
```
|
141
|
+
|
142
|
+
## Uniqueness Tests
|
143
|
+
|
144
|
+
### Collision Probability Test
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
#!/usr/bin/env ruby
|
148
|
+
|
149
|
+
require 'opaque_id'
|
150
|
+
|
151
|
+
puts "Collision Probability Test"
|
152
|
+
puts "=" * 40
|
153
|
+
|
154
|
+
# Test different sample sizes
|
155
|
+
[1000, 10000, 100000].each do |count|
|
156
|
+
puts "\nTesting #{count} IDs..."
|
157
|
+
|
158
|
+
start_time = Time.now
|
159
|
+
ids = count.times.map { OpaqueId.generate }
|
160
|
+
end_time = Time.now
|
161
|
+
|
162
|
+
unique_ids = ids.uniq
|
163
|
+
collisions = count - unique_ids.length
|
164
|
+
collision_rate = (collisions.to_f / count * 100).round(6)
|
165
|
+
|
166
|
+
puts " Generated: #{count} IDs in #{(end_time - start_time).round(4)}s"
|
167
|
+
puts " Unique: #{unique_ids.length} IDs"
|
168
|
+
puts " Collisions: #{collisions} (#{collision_rate}%)"
|
169
|
+
puts " Uniqueness: #{collision_rate == 0 ? '✅ Perfect' : '⚠️ Collisions detected'}"
|
170
|
+
end
|
171
|
+
|
172
|
+
puts "\nCollision test completed."
|
173
|
+
```
|
174
|
+
|
175
|
+
### Birthday Paradox Test
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
#!/usr/bin/env ruby
|
179
|
+
|
180
|
+
require 'opaque_id'
|
181
|
+
|
182
|
+
puts "Birthday Paradox Test"
|
183
|
+
puts "=" * 40
|
184
|
+
|
185
|
+
# Test the birthday paradox with different sample sizes
|
186
|
+
# For 18-character slug-like alphabet (36 chars), we have 36^18 possible combinations
|
187
|
+
# This is approximately 10^28, so collisions should be extremely rare
|
188
|
+
|
189
|
+
sample_sizes = [1000, 10000, 50000, 100000]
|
190
|
+
|
191
|
+
sample_sizes.each do |count|
|
192
|
+
puts "\nTesting #{count} IDs for birthday paradox..."
|
193
|
+
|
194
|
+
start_time = Time.now
|
195
|
+
ids = count.times.map { OpaqueId.generate }
|
196
|
+
end_time = Time.now
|
197
|
+
|
198
|
+
unique_ids = ids.uniq
|
199
|
+
collisions = count - unique_ids.length
|
200
|
+
|
201
|
+
# Calculate theoretical collision probability
|
202
|
+
# For 18-char slug-like alphabet: 36^18 ≈ 10^28 possible combinations
|
203
|
+
alphabet_size = 36
|
204
|
+
id_length = 18
|
205
|
+
total_possibilities = alphabet_size ** id_length
|
206
|
+
|
207
|
+
# Approximate birthday paradox probability
|
208
|
+
# P(collision) ≈ 1 - e^(-n(n-1)/(2*N)) where n=sample_size, N=total_possibilities
|
209
|
+
n = count
|
210
|
+
N = total_possibilities
|
211
|
+
theoretical_prob = 1 - Math.exp(-(n * (n - 1)) / (2.0 * N))
|
212
|
+
|
213
|
+
puts " Sample size: #{count}"
|
214
|
+
puts " Total possibilities: #{total_possibilities.to_s.reverse.gsub(/(\d{3})(?=.)/, '\1,').reverse}"
|
215
|
+
puts " Theoretical collision probability: #{theoretical_prob.round(20)}"
|
216
|
+
puts " Actual collisions: #{collisions}"
|
217
|
+
puts " Result: #{collisions == 0 ? '✅ No collisions (as expected)' : '⚠️ Collisions detected'}"
|
218
|
+
end
|
219
|
+
|
220
|
+
puts "\nBirthday paradox test completed."
|
221
|
+
```
|
222
|
+
|
223
|
+
### Alphabet Distribution Test
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
#!/usr/bin/env ruby
|
227
|
+
|
228
|
+
require 'opaque_id'
|
229
|
+
|
230
|
+
puts "Alphabet Distribution Test"
|
231
|
+
puts "=" * 40
|
232
|
+
|
233
|
+
# Test that all characters in the alphabet are used roughly equally
|
234
|
+
alphabet = OpaqueId::SLUG_LIKE_ALPHABET
|
235
|
+
count = 100000
|
236
|
+
|
237
|
+
puts "Testing distribution for #{alphabet.length}-character alphabet..."
|
238
|
+
puts "Sample size: #{count} IDs"
|
239
|
+
|
240
|
+
start_time = Time.now
|
241
|
+
ids = count.times.map { OpaqueId.generate }
|
242
|
+
end_time = Time.now
|
243
|
+
|
244
|
+
# Count character frequency
|
245
|
+
char_counts = Hash.new(0)
|
246
|
+
ids.each do |id|
|
247
|
+
id.each_char { |char| char_counts[char] += 1 }
|
248
|
+
end
|
249
|
+
|
250
|
+
total_chars = ids.join.length
|
251
|
+
expected_per_char = total_chars.to_f / alphabet.length
|
252
|
+
|
253
|
+
puts "\nCharacter distribution:"
|
254
|
+
puts "Character | Count | Expected | Deviation"
|
255
|
+
puts "-" * 45
|
256
|
+
|
257
|
+
alphabet.each_char do |char|
|
258
|
+
count = char_counts[char]
|
259
|
+
deviation = ((count - expected_per_char) / expected_per_char * 100).round(2)
|
260
|
+
puts "#{char.ljust(8)} | #{count.to_s.rjust(5)} | #{expected_per_char.round(1).to_s.rjust(8)} | #{deviation.to_s.rjust(6)}%"
|
261
|
+
end
|
262
|
+
|
263
|
+
# Calculate chi-square test for uniform distribution
|
264
|
+
chi_square = alphabet.each_char.sum do |char|
|
265
|
+
observed = char_counts[char]
|
266
|
+
expected = expected_per_char
|
267
|
+
((observed - expected) ** 2) / expected
|
268
|
+
end
|
269
|
+
|
270
|
+
puts "\nChi-square statistic: #{chi_square.round(4)}"
|
271
|
+
puts "Distribution: #{chi_square < 30 ? '✅ Appears uniform' : '⚠️ May not be uniform'}"
|
272
|
+
|
273
|
+
puts "\nDistribution test completed."
|
274
|
+
```
|
275
|
+
|
276
|
+
## Running the Benchmarks
|
277
|
+
|
278
|
+
### Quick Performance Test
|
279
|
+
|
280
|
+
```bash
|
281
|
+
# Run basic performance test
|
282
|
+
ruby -e "
|
283
|
+
require 'opaque_id'
|
284
|
+
puts 'OpaqueId Performance Test'
|
285
|
+
puts '=' * 30
|
286
|
+
[100, 1000, 10000].each do |count|
|
287
|
+
start = Time.now
|
288
|
+
count.times { OpaqueId.generate }
|
289
|
+
duration = Time.now - start
|
290
|
+
rate = (count / duration).round(0)
|
291
|
+
puts \"#{count.to_s.rjust(5)} IDs: #{duration.round(4)}s (#{rate} IDs/sec)\"
|
292
|
+
end
|
293
|
+
"
|
294
|
+
```
|
295
|
+
|
296
|
+
### Quick Uniqueness Test
|
297
|
+
|
298
|
+
```bash
|
299
|
+
# Run basic uniqueness test
|
300
|
+
ruby -e "
|
301
|
+
require 'opaque_id'
|
302
|
+
puts 'OpaqueId Uniqueness Test'
|
303
|
+
puts '=' * 30
|
304
|
+
count = 10000
|
305
|
+
ids = count.times.map { OpaqueId.generate }
|
306
|
+
unique = ids.uniq
|
307
|
+
collisions = count - unique.length
|
308
|
+
puts \"Generated: #{count} IDs\"
|
309
|
+
puts \"Unique: #{unique.length} IDs\"
|
310
|
+
puts \"Collisions: #{collisions}\"
|
311
|
+
puts \"Result: #{collisions == 0 ? 'Perfect uniqueness' : 'Collisions detected'}\"
|
312
|
+
"
|
313
|
+
```
|
314
|
+
|
315
|
+
## Expected Results
|
316
|
+
|
317
|
+
### Performance Expectations
|
318
|
+
|
319
|
+
On a modern system, you should expect:
|
320
|
+
|
321
|
+
- **100 IDs**: < 0.001s (100,000+ IDs/sec)
|
322
|
+
- **1,000 IDs**: < 0.01s (100,000+ IDs/sec)
|
323
|
+
- **10,000 IDs**: < 0.1s (100,000+ IDs/sec)
|
324
|
+
- **100,000 IDs**: < 1s (100,000+ IDs/sec)
|
325
|
+
|
326
|
+
### Uniqueness Expectations
|
327
|
+
|
328
|
+
- **1,000 IDs**: 0 collisions (100% unique)
|
329
|
+
- **10,000 IDs**: 0 collisions (100% unique)
|
330
|
+
- **100,000 IDs**: 0 collisions (100% unique)
|
331
|
+
- **1,000,000 IDs**: 0 collisions (100% unique)
|
332
|
+
|
333
|
+
The theoretical collision probability for 1 million IDs is approximately 10^-16, making collisions virtually impossible in practice.
|
334
|
+
|
335
|
+
## System Requirements
|
336
|
+
|
337
|
+
These benchmarks require:
|
338
|
+
|
339
|
+
- Ruby 2.7+ (for optimal performance)
|
340
|
+
- OpaqueId gem installed
|
341
|
+
- Sufficient memory for large sample sizes
|
342
|
+
|
343
|
+
For the largest tests (100,000+ IDs), ensure you have at least 100MB of available memory.
|
344
|
+
|
345
|
+
## Why Not Just Use SecureRandom?
|
346
|
+
|
347
|
+
Ruby's `SecureRandom` already provides secure random generation. Here's how OpaqueId compares:
|
348
|
+
|
349
|
+
### SecureRandom.urlsafe_base64 vs OpaqueId
|
350
|
+
|
351
|
+
| Feature | SecureRandom.urlsafe_base64 | OpaqueId.generate |
|
352
|
+
| ---------------------------- | ------------------------------- | ---------------------------- |
|
353
|
+
| **Length** | 22 characters (fixed) | 18 characters (configurable) |
|
354
|
+
| **Alphabet** | A-Z, a-z, 0-9, -, \_ (64 chars) | 0-9, a-z (36 chars) |
|
355
|
+
| **URL Safety** | ✅ Yes | ✅ Yes |
|
356
|
+
| **Double-click selectable** | ❌ No (contains special chars) | ✅ Yes (no special chars) |
|
357
|
+
| **Configurable length** | ❌ No | ✅ Yes |
|
358
|
+
| **Configurable alphabet** | ❌ No | ✅ Yes |
|
359
|
+
| **ActiveRecord integration** | ❌ Manual | ✅ Built-in |
|
360
|
+
| **Rails generator** | ❌ No | ✅ Yes |
|
361
|
+
|
362
|
+
### When to Use Each
|
363
|
+
|
364
|
+
**Use SecureRandom.urlsafe_base64 when:**
|
365
|
+
|
366
|
+
- You need maximum entropy (22 chars vs 18)
|
367
|
+
- You don't mind special characters (-, \_)
|
368
|
+
- You don't need double-click selection
|
369
|
+
- You're building a simple solution
|
370
|
+
|
371
|
+
**Use OpaqueId when:**
|
372
|
+
|
373
|
+
- You want slug-like IDs (no special characters)
|
374
|
+
- You need double-click selectable IDs
|
375
|
+
- You want configurable length and alphabet
|
376
|
+
- You're using ActiveRecord models
|
377
|
+
- You want consistent ID length (default: 18 characters)
|
378
|
+
|
379
|
+
### Performance Comparison
|
380
|
+
|
381
|
+
Run the [SecureRandom Comparison Test](#securerandom-comparison-test) to see how OpaqueId compares to various SecureRandom methods on your system.
|
382
|
+
|
383
|
+
### Migration from nanoid.rb
|
384
|
+
|
385
|
+
The nanoid.rb gem is [considered obsolete](https://github.com/radeno/nanoid.rb/issues/67) for Ruby 2.7+ because SecureRandom provides similar functionality. OpaqueId provides an alternative with different defaults and Rails integration.
|
data/docs/configuration.md
CHANGED
@@ -26,10 +26,10 @@ class User < ApplicationRecord
|
|
26
26
|
# Custom column name
|
27
27
|
self.opaque_id_column = :public_id
|
28
28
|
|
29
|
-
# Custom length (default:
|
29
|
+
# Custom length (default: 18)
|
30
30
|
self.opaque_id_length = 15
|
31
31
|
|
32
|
-
# Custom alphabet (default:
|
32
|
+
# Custom alphabet (default: SLUG_LIKE_ALPHABET)
|
33
33
|
self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET
|
34
34
|
|
35
35
|
# Require letter start (default: false)
|
@@ -45,14 +45,14 @@ end
|
|
45
45
|
|
46
46
|
### Configuration Options Reference
|
47
47
|
|
48
|
-
| Option | Type | Default
|
49
|
-
| -------------------------------- | ------- |
|
50
|
-
| `opaque_id_column` | Symbol | `:opaque_id`
|
51
|
-
| `opaque_id_length` | Integer | `
|
52
|
-
| `opaque_id_alphabet` | String | `
|
53
|
-
| `opaque_id_require_letter_start` | Boolean | `false`
|
54
|
-
| `opaque_id_max_retry` | Integer | `3`
|
55
|
-
| `opaque_id_purge_chars` | Array | `[]`
|
48
|
+
| Option | Type | Default | Description |
|
49
|
+
| -------------------------------- | ------- | -------------------- | --------------------------------------------- |
|
50
|
+
| `opaque_id_column` | Symbol | `:opaque_id` | Column name for storing the opaque ID |
|
51
|
+
| `opaque_id_length` | Integer | `18` | Length of generated IDs |
|
52
|
+
| `opaque_id_alphabet` | String | `SLUG_LIKE_ALPHABET` | Character set for ID generation |
|
53
|
+
| `opaque_id_require_letter_start` | Boolean | `false` | Require IDs to start with a letter |
|
54
|
+
| `opaque_id_max_retry` | Integer | `3` | Maximum retry attempts for collision handling |
|
55
|
+
| `opaque_id_purge_chars` | Array | `[]` | Characters to exclude from generated IDs |
|
56
56
|
|
57
57
|
## Global Configuration
|
58
58
|
|
@@ -97,12 +97,25 @@ end
|
|
97
97
|
|
98
98
|
### Built-in Alphabets
|
99
99
|
|
100
|
-
####
|
100
|
+
#### SLUG_LIKE_ALPHABET (Default)
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
# Characters: 0-9, a-z (36 characters)
|
104
|
+
# Use case: URL-safe, double-click selectable, no confusing characters
|
105
|
+
# Example output: "izkpm55j334u8x9y2a"
|
106
|
+
|
107
|
+
class User < ApplicationRecord
|
108
|
+
include OpaqueId::Model
|
109
|
+
self.opaque_id_alphabet = OpaqueId::SLUG_LIKE_ALPHABET
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
#### ALPHANUMERIC_ALPHABET
|
101
114
|
|
102
115
|
```ruby
|
103
116
|
# Characters: A-Z, a-z, 0-9 (62 characters)
|
104
117
|
# Use case: General purpose, URL-safe
|
105
|
-
# Example output: "
|
118
|
+
# Example output: "V1StGXR8Z5jdHi6BmyT"
|
106
119
|
|
107
120
|
class User < ApplicationRecord
|
108
121
|
include OpaqueId::Model
|
@@ -240,9 +253,9 @@ end
|
|
240
253
|
# Now the methods use the custom column name
|
241
254
|
user = User.create!(name: "John Doe")
|
242
255
|
puts user.public_id
|
243
|
-
# => "
|
256
|
+
# => "izkpm55j334u8x9y2a"
|
244
257
|
|
245
|
-
user = User.find_by_public_id("
|
258
|
+
user = User.find_by_public_id("izkpm55j334u8x9y2a")
|
246
259
|
```
|
247
260
|
|
248
261
|
### Multiple Column Names
|
@@ -279,7 +292,7 @@ end
|
|
279
292
|
# This will retry until it generates an ID starting with a letter
|
280
293
|
user = User.create!(name: "John Doe")
|
281
294
|
puts user.opaque_id
|
282
|
-
# => "
|
295
|
+
# => "izkpm55j334u8x9y2a" (starts with 'i')
|
283
296
|
```
|
284
297
|
|
285
298
|
### Character Purging
|
@@ -295,7 +308,7 @@ end
|
|
295
308
|
# Generated IDs will not contain these characters
|
296
309
|
user = User.create!(name: "John Doe")
|
297
310
|
puts user.opaque_id
|
298
|
-
# => "
|
311
|
+
# => "izkpm55j334u8x9y2a" (no '0', 'O', 'l', 'I')
|
299
312
|
```
|
300
313
|
|
301
314
|
## Collision Handling Configuration
|
@@ -496,8 +509,8 @@ class UserTest < ActiveSupport::TestCase
|
|
496
509
|
user = User.new(name: "Test User")
|
497
510
|
|
498
511
|
assert user.valid?
|
499
|
-
assert_equal
|
500
|
-
assert_equal OpaqueId::
|
512
|
+
assert_equal 18, user.class.opaque_id_length
|
513
|
+
assert_equal OpaqueId::SLUG_LIKE_ALPHABET, user.class.opaque_id_alphabet
|
501
514
|
end
|
502
515
|
|
503
516
|
test "opaque_id generation works with custom configuration" do
|
@@ -523,6 +536,7 @@ end
|
|
523
536
|
|
524
537
|
### 2. Select Suitable Alphabets
|
525
538
|
|
539
|
+
- **SLUG_LIKE_ALPHABET** (default): URL-safe, double-click selectable, no confusing characters
|
526
540
|
- **ALPHANUMERIC_ALPHABET**: General purpose, URL-safe
|
527
541
|
- **STANDARD_ALPHABET**: Fastest generation, URL-safe
|
528
542
|
- **Custom alphabets**: Specific requirements (numeric, hexadecimal, etc.)
|
data/docs/index.md
CHANGED
@@ -12,7 +12,7 @@ permalink: /
|
|
12
12
|
[](https://github.com/rubocop/rubocop)
|
13
13
|
[](https://rubygems.org/gems/opaque_id)
|
14
14
|
|
15
|
-
A Ruby gem for generating
|
15
|
+
A simple Ruby gem for generating secure, opaque IDs for ActiveRecord models. OpaqueId provides a drop-in replacement for `nanoid.rb` using Ruby's built-in `SecureRandom` methods, with slug-like IDs as the default for optimal URL safety and user experience.
|
16
16
|
|
17
17
|
- TOC
|
18
18
|
{:toc}
|
@@ -57,10 +57,10 @@ end
|
|
57
57
|
|
58
58
|
# Create a user - opaque_id is automatically generated
|
59
59
|
user = User.create!(name: "John Doe")
|
60
|
-
puts user.opaque_id # => "
|
60
|
+
puts user.opaque_id # => "izkpm55j334u8x9y2a"
|
61
61
|
|
62
62
|
# Find by opaque_id
|
63
|
-
user = User.find_by_opaque_id("
|
63
|
+
user = User.find_by_opaque_id("izkpm55j334u8x9y2a")
|
64
64
|
```
|
65
65
|
|
66
66
|
## Why OpaqueId?
|
@@ -128,6 +128,24 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
128
128
|
|
129
129
|
This project follows an "open source, closed contribution" model. We welcome bug reports, feature requests, and documentation improvements through GitHub Issues.
|
130
130
|
|
131
|
+
## Performance & Benchmarks
|
132
|
+
|
133
|
+
You can run benchmarks to test OpaqueId's performance and uniqueness characteristics on your system.
|
134
|
+
|
135
|
+
**Quick Test:**
|
136
|
+
|
137
|
+
```bash
|
138
|
+
# Test 10,000 ID generation
|
139
|
+
ruby -e "require 'opaque_id'; start=Time.now; 10000.times{OpaqueId.generate}; puts \"Generated 10,000 IDs in #{(Time.now-start).round(4)}s\""
|
140
|
+
```
|
141
|
+
|
142
|
+
**Expected Results:**
|
143
|
+
|
144
|
+
- **Performance**: 100,000+ IDs per second on modern hardware
|
145
|
+
- **Uniqueness**: Zero collisions in practice (theoretical probability < 10^-16 for 1M IDs)
|
146
|
+
|
147
|
+
For comprehensive benchmarks including collision tests, alphabet distribution analysis, and performance comparisons, see the [Benchmarks Guide](benchmarks.md).
|
148
|
+
|
131
149
|
## Acknowledgements
|
132
150
|
|
133
151
|
- [nanoid.rb](https://github.com/radeno/nanoid.rb) - Original inspiration and reference implementation
|
data/docs/performance.md
CHANGED
@@ -8,7 +8,7 @@ permalink: /performance/
|
|
8
8
|
|
9
9
|
# Performance
|
10
10
|
|
11
|
-
OpaqueId is designed for
|
11
|
+
OpaqueId is designed for efficient ID generation with optimized algorithms and memory usage. This guide covers performance characteristics, optimization strategies, and scalability considerations.
|
12
12
|
|
13
13
|
- TOC
|
14
14
|
{:toc}
|
@@ -17,11 +17,11 @@ OpaqueId is designed for high performance with optimized algorithms and efficien
|
|
17
17
|
|
18
18
|
### Generation Speed
|
19
19
|
|
20
|
-
OpaqueId is designed for
|
20
|
+
OpaqueId is designed for efficient ID generation with optimized algorithms for different alphabet sizes and ID lengths.
|
21
21
|
|
22
22
|
#### Algorithm Performance
|
23
23
|
|
24
|
-
- **Fast Path (64-character alphabets)**: Uses bitwise operations for
|
24
|
+
- **Fast Path (64-character alphabets)**: Uses bitwise operations for efficient generation with no rejection sampling overhead
|
25
25
|
- **Unbiased Path (Other alphabets)**: Uses rejection sampling for unbiased distribution with slight performance overhead
|
26
26
|
- **Performance scales linearly** with ID length and batch size
|
27
27
|
|