opaque_id 1.6.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 +90 -0
- data/README.md +45 -23
- data/docs/api-reference.md +6 -6
- data/docs/benchmarks.md +385 -0
- data/docs/configuration.md +32 -18
- data/docs/index.md +20 -2
- data/docs/usage.md +20 -16
- 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
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f3a94f08b1d9f3e21a513fe3f7ae6422c7288c2ccef38c78394209e835d264d
|
4
|
+
data.tar.gz: 114701a6d202fa5d41ac77da871f86ffc88182d54ffb58ea8ab2324bda177fee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66f5027ec5599b164599a7c5f4d6bc6c7396787671ea7a2238bc95385482150bfb5b4e1341173264fc870af0b851d45c4faad6c9396519f96e11c00b5ff5978c
|
7
|
+
data.tar.gz: 50b3da266692f3b004d2c00f9a485d5bdfe0af51465dd4f87f5189d109cbfd3849c29a741ce4c3a4fb8565970594696e48467bf2e1fc8312b7356c7f1a87bd80
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,96 @@ All notable changes to the OpaqueId gem will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [1.7.0](https://github.com/nyaggah/opaque_id/compare/opaque_id-v1.6.0...opaque_id/v1.7.0) (2025-10-04)
|
9
|
+
|
10
|
+
|
11
|
+
### Features
|
12
|
+
|
13
|
+
* add external links to sidebar using JavaScript ([cd77079](https://github.com/nyaggah/opaque_id/commit/cd770795936ca76633a18f80cba8482ec09db8e2))
|
14
|
+
* add SLUG_LIKE_ALPHABET as default for URL-safe IDs ([1e52b17](https://github.com/nyaggah/opaque_id/commit/1e52b174d70ca72b74badf399401605b307e3604))
|
15
|
+
* add Table of Contents to all documentation pages ([9472175](https://github.com/nyaggah/opaque_id/commit/94721752270211d5b7db460da61d79a8a81218b2))
|
16
|
+
* complete Table of Contents implementation ([5b037e4](https://github.com/nyaggah/opaque_id/commit/5b037e42692803204bc4d8991b6d2e0c9639a05c))
|
17
|
+
* implement dynamic copyright year ([b86cea6](https://github.com/nyaggah/opaque_id/commit/b86cea66e60939377c6b0a67c1d385c9bf4bece3))
|
18
|
+
* improve documentation site theme and navigation ([304f845](https://github.com/nyaggah/opaque_id/commit/304f845a47397ea2a15f12d2c635144b39be431b))
|
19
|
+
* improve generator API and add custom column configuration ([5c1a570](https://github.com/nyaggah/opaque_id/commit/5c1a5703dee269f6985d26096942c086787296f3))
|
20
|
+
* initial release of OpaqueId gem v0.1.0 ([3a90274](https://github.com/nyaggah/opaque_id/commit/3a9027403552f8160e3aaf413d1e99ce8c63bbe4))
|
21
|
+
* update defaults to slug-like alphabet and 18-char length ([ec76440](https://github.com/nyaggah/opaque_id/commit/ec764400b0f40a93784c70c7c99ec15a9e386d36))
|
22
|
+
* update defaults to slug-like alphabet and length 18 ([87c978f](https://github.com/nyaggah/opaque_id/commit/87c978f3601270e990bc976344fd2113ba0573b3))
|
23
|
+
|
24
|
+
|
25
|
+
### Bug Fixes
|
26
|
+
|
27
|
+
* add frozen string literal comment to docs/Gemfile ([2d653a7](https://github.com/nyaggah/opaque_id/commit/2d653a7bbc071e67e94e63ebc1602768b8d04456))
|
28
|
+
* add Release Please manifest and update workflow configuration ([f33ce6c](https://github.com/nyaggah/opaque_id/commit/f33ce6c96805b6c670cd1eedf5ebed2a46e8ffa6))
|
29
|
+
* add required permissions for Release Please job ([cdf8d2b](https://github.com/nyaggah/opaque_id/commit/cdf8d2bfc5dbea9a9ec40411a988191ba91913fa))
|
30
|
+
* configure just-the-docs theme for GitHub Pages compatibility ([c2fb96c](https://github.com/nyaggah/opaque_id/commit/c2fb96c46e99fd5287520e01c3bc2dd7bd5d4817))
|
31
|
+
* correct aux_links configuration format ([61b3fb9](https://github.com/nyaggah/opaque_id/commit/61b3fb9441ce3cfc3cf30637c277d19135f89dab))
|
32
|
+
* correct release-please tag format ([b4c31e5](https://github.com/nyaggah/opaque_id/commit/b4c31e5d0f4878878dd728e82fe9f5eb09ce5b82))
|
33
|
+
* improve publish workflow trigger reliability ([9c6e240](https://github.com/nyaggah/opaque_id/commit/9c6e2402474d8ecf8a3f754a9f2ca149a63bf1d1))
|
34
|
+
* improve statistical test reliability for CI ([c9b0d52](https://github.com/nyaggah/opaque_id/commit/c9b0d52fb4e9559aad489fcf58bdff1917d613a6))
|
35
|
+
* remove unsupported Release Please configuration parameters ([e3b0d2a](https://github.com/nyaggah/opaque_id/commit/e3b0d2ac96b905316a78992004fae83423991632))
|
36
|
+
* resolve CSS linting warnings ([d01e8c0](https://github.com/nyaggah/opaque_id/commit/d01e8c05628b7f2bd8b39a13325112168cf4e689))
|
37
|
+
* resolve dependency version conflicts and improve test robustness ([af43e13](https://github.com/nyaggah/opaque_id/commit/af43e13393a452f11ce32bafb26a267d1460736c))
|
38
|
+
* resolve TOC rendering and improve workflow configuration ([244d532](https://github.com/nyaggah/opaque_id/commit/244d5321e5b18091d8599bd772a28f5a30239c80))
|
39
|
+
* update deprecated platform specifications in docs/Gemfile ([cf96134](https://github.com/nyaggah/opaque_id/commit/cf9613499150dd7152bff38aa8f8fe7ab9923eb5))
|
40
|
+
* use correct aux_links format ([d4a4bc0](https://github.com/nyaggah/opaque_id/commit/d4a4bc0c11bc66b661034915b471ed762611d821))
|
41
|
+
|
42
|
+
|
43
|
+
### Documentation
|
44
|
+
|
45
|
+
* final cleanup of over-inflated claims ([795b5b0](https://github.com/nyaggah/opaque_id/commit/795b5b0071a098f718a40e84e81ceba4525b8d3b))
|
46
|
+
* implement comprehensive documentation site with dark theme ([19cc9e3](https://github.com/nyaggah/opaque_id/commit/19cc9e30c584658559c5404a4518e871039cc223))
|
47
|
+
* implement comprehensive documentation site with dark theme ([a249c6a](https://github.com/nyaggah/opaque_id/commit/a249c6ad2253439f0d070620a914ff87597cf7cb))
|
48
|
+
* tone down over-inflated claims and remove unsubstantiated benchmarks ([6536b87](https://github.com/nyaggah/opaque_id/commit/6536b87f9a9c2049497328319641ff21368c6032))
|
49
|
+
* tone down pretentious language in algorithms intro ([58c0caf](https://github.com/nyaggah/opaque_id/commit/58c0caf116228401b6218ec589a35b350d068b49))
|
50
|
+
|
51
|
+
|
52
|
+
### Styles
|
53
|
+
|
54
|
+
* improve code formatting in generator ([ce52021](https://github.com/nyaggah/opaque_id/commit/ce52021cd60e3594dc1ee34e5b96d48568008b5a))
|
55
|
+
|
56
|
+
|
57
|
+
### Miscellaneous Chores
|
58
|
+
|
59
|
+
* configure just-the-docs theme ([3dfcac8](https://github.com/nyaggah/opaque_id/commit/3dfcac88099e65e61931e558e9242050bdbf0bc9))
|
60
|
+
* **main:** release 1.0.0 ([43282c5](https://github.com/nyaggah/opaque_id/commit/43282c5865aae3f136c9eaa49f013066a2359826))
|
61
|
+
* **main:** release 1.0.0 ([b8271ad](https://github.com/nyaggah/opaque_id/commit/b8271ad43cf6fab4687276258f0105c06a87bff7))
|
62
|
+
* **main:** release 1.0.1 ([4ee2e2c](https://github.com/nyaggah/opaque_id/commit/4ee2e2c69905e71d6c5048617af09b65cbb9e12a))
|
63
|
+
* **main:** release 1.0.1 ([e6cd2f8](https://github.com/nyaggah/opaque_id/commit/e6cd2f8f8ff545ad3010598488f71956e36a88c5))
|
64
|
+
* **main:** release 1.0.2 ([d5c7423](https://github.com/nyaggah/opaque_id/commit/d5c7423cf06ae7638d95cdd29b2631049efed727))
|
65
|
+
* **main:** release 1.0.2 ([365dff8](https://github.com/nyaggah/opaque_id/commit/365dff87a044aa967906866d6ace8d1ccad08c78))
|
66
|
+
* **main:** release opaque_id 1.1.0 ([df65c79](https://github.com/nyaggah/opaque_id/commit/df65c79d0efaee1dcd89b8af2f1b1c712c3cc2cd))
|
67
|
+
* **main:** release opaque_id 1.1.0 ([e2a4ee0](https://github.com/nyaggah/opaque_id/commit/e2a4ee0fd2d31132bc5bc0de6b6115f3ceae8afb))
|
68
|
+
* **main:** release opaque_id 1.2.0 ([1987b07](https://github.com/nyaggah/opaque_id/commit/1987b07b3db92216f3fbbe807c3ad66ca105866e))
|
69
|
+
* **main:** release opaque_id 1.2.0 ([aacd20a](https://github.com/nyaggah/opaque_id/commit/aacd20aa5eaa1fe2fa76d89cc6bdab26214b744d))
|
70
|
+
* **main:** release opaque_id 1.3.0 ([964b1b4](https://github.com/nyaggah/opaque_id/commit/964b1b421b1fac3fe43b15549e0e6b216b600357))
|
71
|
+
* **main:** release opaque_id 1.3.0 ([76453ad](https://github.com/nyaggah/opaque_id/commit/76453ad18b2d2b81548e49dafb5dde6fb52abcca))
|
72
|
+
* **main:** release opaque_id 1.4.0 ([6c3ccd1](https://github.com/nyaggah/opaque_id/commit/6c3ccd108b3eea0bbf0ac71fda4ccc0e47e97b95))
|
73
|
+
* **main:** release opaque_id 1.4.0 ([3d9d818](https://github.com/nyaggah/opaque_id/commit/3d9d818b34771ad1ca729f3f9b443784a6965568))
|
74
|
+
* **main:** release opaque_id 1.5.0 ([113af74](https://github.com/nyaggah/opaque_id/commit/113af74dda07330bf7108977147362385dc806ae))
|
75
|
+
* **main:** release opaque_id 1.5.0 ([ff14fe9](https://github.com/nyaggah/opaque_id/commit/ff14fe92d94cf4ecbe5aaeb31ad6cd044bfa89d0))
|
76
|
+
* **main:** release opaque_id 1.6.0 ([9c95d04](https://github.com/nyaggah/opaque_id/commit/9c95d0461bcca6e8eeccc4f985f7e993ba651bb3))
|
77
|
+
* **main:** release opaque_id 1.6.0 ([74f09bd](https://github.com/nyaggah/opaque_id/commit/74f09bd860fc6f097140e1cc4efb4164407be30d))
|
78
|
+
* remove tasks/ directory from source control ([e007ead](https://github.com/nyaggah/opaque_id/commit/e007ead90fa3fbb62ab17e334563e69899afb259))
|
79
|
+
* sync version to 1.0.2 ([2f870c8](https://github.com/nyaggah/opaque_id/commit/2f870c89d486f0a5cda8ef37e5544c29c8fa2919))
|
80
|
+
* update author name and GitHub URLs ([e911548](https://github.com/nyaggah/opaque_id/commit/e911548e89e1e60ac3281157059f7c7b812750bd))
|
81
|
+
* update bundle and add Linux platform support ([661842f](https://github.com/nyaggah/opaque_id/commit/661842f0ba66ce36962bb11f75a1b0e35eb5a3e7))
|
82
|
+
* update Gemfile.lock after dependency check ([058281f](https://github.com/nyaggah/opaque_id/commit/058281f6e07f3c9a1057c6fbeb0200bccf221558))
|
83
|
+
|
84
|
+
|
85
|
+
### Code Refactoring
|
86
|
+
|
87
|
+
* improve generator code quality and resolve RuboCop issues ([b658bbc](https://github.com/nyaggah/opaque_id/commit/b658bbcb29f6c0134f2bf256b24c5b3c2bccb265))
|
88
|
+
* integrate Release Please into main CI workflow ([8a0b189](https://github.com/nyaggah/opaque_id/commit/8a0b189a8bfbfcc07275c9bdda3397ff367b2054))
|
89
|
+
* simplify external links implementation ([5cb84c1](https://github.com/nyaggah/opaque_id/commit/5cb84c1e8952a0fd1aa4b9089b9ff11829dc397c))
|
90
|
+
|
91
|
+
|
92
|
+
### Tests
|
93
|
+
|
94
|
+
* add test for lowercase model names ([8ef4675](https://github.com/nyaggah/opaque_id/commit/8ef4675a0698d2cda054084360c9e80d956ddb14))
|
95
|
+
* update tests for SLUG_LIKE_ALPHABET defaults ([d7296c7](https://github.com/nyaggah/opaque_id/commit/d7296c77b1b7a995e7a42b3ba546fdeb8e41c297))
|
96
|
+
* update tests for SLUG_LIKE_ALPHABET defaults ([d1f8f26](https://github.com/nyaggah/opaque_id/commit/d1f8f26e7be80f6cbefc98563c0ffa45033372dc))
|
97
|
+
|
8
98
|
## [1.6.0](https://github.com/nyaggah/opaque_id/compare/opaque_id-v1.5.0...opaque_id/v1.6.0) (2025-10-03)
|
9
99
|
|
10
100
|
|
data/README.md
CHANGED
@@ -27,7 +27,7 @@ A simple Ruby gem for generating secure, opaque IDs for ActiveRecord models. Opa
|
|
27
27
|
- [Configuration Options](#configuration-options)
|
28
28
|
- [Configuration Details](#configuration-details)
|
29
29
|
- [Built-in Alphabets](#built-in-alphabets)
|
30
|
-
- [`
|
30
|
+
- [`SLUG_LIKE_ALPHABET` (Default)](#slug_like_alphabet-default)
|
31
31
|
- [`STANDARD_ALPHABET`](#standard_alphabet)
|
32
32
|
- [Alphabet Comparison](#alphabet-comparison)
|
33
33
|
- [Custom Alphabets](#custom-alphabets)
|
@@ -36,6 +36,7 @@ A simple Ruby gem for generating secure, opaque IDs for ActiveRecord models. Opa
|
|
36
36
|
- [Fast Path Algorithm (64-character alphabets)](#fast-path-algorithm-64-character-alphabets)
|
37
37
|
- [Unbiased Path Algorithm (other alphabets)](#unbiased-path-algorithm-other-alphabets)
|
38
38
|
- [Algorithm Selection](#algorithm-selection)
|
39
|
+
- [Performance & Benchmarks](#performance--benchmarks)
|
39
40
|
- [Performance Benchmarks](#performance-benchmarks)
|
40
41
|
- [Generation Speed (IDs per second)](#generation-speed-ids-per-second)
|
41
42
|
- [Memory Usage](#memory-usage)
|
@@ -176,11 +177,11 @@ end
|
|
176
177
|
|
177
178
|
# IDs are automatically generated on creation
|
178
179
|
user = User.create!(name: "John Doe")
|
179
|
-
puts user.opaque_id # => "
|
180
|
+
puts user.opaque_id # => "izkpm55j334u8x9y2a"
|
180
181
|
|
181
182
|
# Find by opaque ID
|
182
|
-
user = User.find_by_opaque_id("
|
183
|
-
user = User.find_by_opaque_id!("
|
183
|
+
user = User.find_by_opaque_id("izkpm55j334u8x9y2a")
|
184
|
+
user = User.find_by_opaque_id!("izkpm55j334u8x9y2a") # raises if not found
|
184
185
|
```
|
185
186
|
|
186
187
|
## Usage
|
@@ -197,7 +198,7 @@ OpaqueId defaults to generating **slug-like IDs** that are perfect for URLs and
|
|
197
198
|
```ruby
|
198
199
|
# Default generation creates slug-like IDs
|
199
200
|
id = OpaqueId.generate
|
200
|
-
# => "
|
201
|
+
# => "izkpm55j334u8x9y2a" # Perfect for URLs and user selection
|
201
202
|
|
202
203
|
# Compare to UUIDs
|
203
204
|
uuid = SecureRandom.uuid
|
@@ -213,7 +214,7 @@ OpaqueId can be used independently of ActiveRecord for generating secure IDs in
|
|
213
214
|
```ruby
|
214
215
|
# Generate with default settings (18 characters, slug-like)
|
215
216
|
id = OpaqueId.generate
|
216
|
-
# => "
|
217
|
+
# => "izkpm55j334u8x9y2a"
|
217
218
|
|
218
219
|
# Custom length
|
219
220
|
id = OpaqueId.generate(size: 10)
|
@@ -248,7 +249,7 @@ class BackgroundJob
|
|
248
249
|
end
|
249
250
|
|
250
251
|
job_id = BackgroundJob.enqueue(ProcessDataJob, user_id: 123)
|
251
|
-
# => "
|
252
|
+
# => "izkpm55j334u8x9y2a"
|
252
253
|
```
|
253
254
|
|
254
255
|
##### Temporary File Names
|
@@ -347,7 +348,7 @@ class ApiLogger
|
|
347
348
|
end
|
348
349
|
|
349
350
|
request_id = ApiLogger.log_request("/api/users", { page: 1 })
|
350
|
-
# => "
|
351
|
+
# => "izkpm55j334u8x9y2a"
|
351
352
|
```
|
352
353
|
|
353
354
|
##### Batch Processing IDs
|
@@ -369,7 +370,7 @@ class BatchProcessor
|
|
369
370
|
end
|
370
371
|
|
371
372
|
batch_id = BatchProcessor.process_batch([1, 2, 3, 4, 5])
|
372
|
-
# => "
|
373
|
+
# => "izkpm55j334u8x9y2a"
|
373
374
|
# => Processing item izkpm55j334u8x9y2_000: 1
|
374
375
|
# => Processing item izkpm55j334u8x9y2_001: 2
|
375
376
|
# => ...
|
@@ -436,7 +437,7 @@ end
|
|
436
437
|
|
437
438
|
# Create a new post - opaque_id is automatically generated
|
438
439
|
post = Post.create!(title: "Hello World", content: "This is my first post")
|
439
|
-
puts post.opaque_id # => "
|
440
|
+
puts post.opaque_id # => "izkpm55j334u8x9y2a"
|
440
441
|
|
441
442
|
# Create multiple posts
|
442
443
|
posts = Post.create!([
|
@@ -548,7 +549,7 @@ class Upload < ApplicationRecord
|
|
548
549
|
self.opaque_id_purge_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|']
|
549
550
|
end
|
550
551
|
|
551
|
-
# Generated filenames will look like: "
|
552
|
+
# Generated filenames will look like: "izkpm55j334u8x9y2a"
|
552
553
|
```
|
553
554
|
|
554
555
|
##### Session Token Configuration
|
@@ -611,7 +612,7 @@ end
|
|
611
612
|
|
612
613
|
```ruby
|
613
614
|
# Find by opaque ID (returns nil if not found)
|
614
|
-
user = User.find_by_opaque_id("
|
615
|
+
user = User.find_by_opaque_id("izkpm55j334u8x9y2a")
|
615
616
|
if user
|
616
617
|
puts "Found user: #{user.name}"
|
617
618
|
else
|
@@ -619,7 +620,7 @@ else
|
|
619
620
|
end
|
620
621
|
|
621
622
|
# Find by opaque ID (raises ActiveRecord::RecordNotFound if not found)
|
622
|
-
user = User.find_by_opaque_id!("
|
623
|
+
user = User.find_by_opaque_id!("izkpm55j334u8x9y2a")
|
623
624
|
puts "Found user: #{user.name}"
|
624
625
|
|
625
626
|
# Use in controllers for public-facing URLs
|
@@ -667,14 +668,14 @@ rails generate opaque_id:install users --column-name=public_id
|
|
667
668
|
|
668
669
|
OpaqueId provides comprehensive configuration options to customize ID generation behavior:
|
669
670
|
|
670
|
-
| Option | Type | Default
|
671
|
-
| -------------------------------- | --------------- |
|
672
|
-
| `opaque_id_column` | `Symbol` | `:opaque_id`
|
673
|
-
| `opaque_id_length` | `Integer` | `
|
674
|
-
| `opaque_id_alphabet` | `String` | `
|
675
|
-
| `opaque_id_require_letter_start` | `Boolean` | `false`
|
676
|
-
| `opaque_id_purge_chars` | `Array<String>` | `[]`
|
677
|
-
| `opaque_id_max_retry` | `Integer` | `3`
|
671
|
+
| Option | Type | Default | Description | Example Usage |
|
672
|
+
| -------------------------------- | --------------- | -------------------- | ----------------------------------------------- | ------------------------------------------------------- |
|
673
|
+
| `opaque_id_column` | `Symbol` | `:opaque_id` | Column name for storing the opaque ID | `self.opaque_id_column = :public_id` |
|
674
|
+
| `opaque_id_length` | `Integer` | `18` | Length of generated IDs | `self.opaque_id_length = 32` |
|
675
|
+
| `opaque_id_alphabet` | `String` | `SLUG_LIKE_ALPHABET` | Character set for ID generation | `self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET` |
|
676
|
+
| `opaque_id_require_letter_start` | `Boolean` | `false` | Require ID to start with a letter | `self.opaque_id_require_letter_start = true` |
|
677
|
+
| `opaque_id_purge_chars` | `Array<String>` | `[]` | Characters to remove from generated IDs | `self.opaque_id_purge_chars = ['0', 'O', 'I', 'l']` |
|
678
|
+
| `opaque_id_max_retry` | `Integer` | `3` | Maximum retry attempts for collision resolution | `self.opaque_id_max_retry = 10` |
|
678
679
|
|
679
680
|
### Configuration Details
|
680
681
|
|
@@ -691,7 +692,7 @@ OpaqueId provides comprehensive configuration options to customize ID generation
|
|
691
692
|
- **Performance**: Longer IDs are more secure but use more storage
|
692
693
|
- **Examples**:
|
693
694
|
- `6` → Short URLs: `"V1StGX"`
|
694
|
-
- `18` → Default: `"
|
695
|
+
- `18` → Default: `"izkpm55j334u8x9y2a"`
|
695
696
|
- `32` → API Keys: `"izkpm55j334u8x9y2abc1234def5678gh"`
|
696
697
|
|
697
698
|
#### `opaque_id_alphabet`
|
@@ -710,7 +711,7 @@ OpaqueId provides comprehensive configuration options to customize ID generation
|
|
710
711
|
- **Purpose**: Ensures IDs start with a letter for better readability
|
711
712
|
- **Use Cases**: When IDs are user-facing or need to be easily readable
|
712
713
|
- **Performance**: Slight overhead due to rejection sampling
|
713
|
-
- **Example**: `true` → `"
|
714
|
+
- **Example**: `true` → `"izkpm55j334u8x9y2a"`, `false` → `"zkpm55j334u8x9y2a"`
|
714
715
|
|
715
716
|
#### `opaque_id_purge_chars`
|
716
717
|
|
@@ -1667,6 +1668,27 @@ This software is provided "as is" without warranty of any kind, express or impli
|
|
1667
1668
|
|
1668
1669
|
Everyone interacting in the OpaqueId project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nyaggah/opaque_id/blob/main/CODE_OF_CONDUCT.md).
|
1669
1670
|
|
1671
|
+
## Performance & Benchmarks
|
1672
|
+
|
1673
|
+
You can run benchmarks to test OpaqueId's performance and uniqueness characteristics on your system.
|
1674
|
+
|
1675
|
+
**Quick Test:**
|
1676
|
+
|
1677
|
+
```bash
|
1678
|
+
# Test 10,000 ID generation
|
1679
|
+
ruby -e "require 'opaque_id'; start=Time.now; 10000.times{OpaqueId.generate}; puts \"Generated 10,000 IDs in #{(Time.now-start).round(4)}s\""
|
1680
|
+
|
1681
|
+
# Compare with SecureRandom (as mentioned in nanoid.rb issue #67)
|
1682
|
+
ruby -e "require 'opaque_id'; require 'securerandom'; puts 'OpaqueId: ' + OpaqueId.generate; puts 'SecureRandom: ' + SecureRandom.urlsafe_base64"
|
1683
|
+
```
|
1684
|
+
|
1685
|
+
**Expected Results:**
|
1686
|
+
|
1687
|
+
- **Performance**: 100,000+ IDs per second on modern hardware
|
1688
|
+
- **Uniqueness**: Zero collisions in practice (theoretical probability < 10^-16 for 1M IDs)
|
1689
|
+
|
1690
|
+
For comprehensive benchmarks including collision tests, alphabet distribution analysis, and performance comparisons, see the [Benchmarks Guide](docs/benchmarks.md).
|
1691
|
+
|
1670
1692
|
## Acknowledgements
|
1671
1693
|
|
1672
1694
|
OpaqueId is heavily inspired by [nanoid.rb](https://github.com/radeno/nanoid.rb), which is a Ruby implementation of the original [NanoID](https://github.com/ai/nanoid) project. The core algorithm and approach to secure ID generation draws from the excellent work done by the NanoID team.
|
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
@@ -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/usage.md
CHANGED
@@ -22,11 +22,11 @@ OpaqueId can be used independently of ActiveRecord for generating secure, random
|
|
22
22
|
```ruby
|
23
23
|
# Generate a default opaque ID (18 characters, slug-like)
|
24
24
|
id = OpaqueId.generate
|
25
|
-
# => "
|
25
|
+
# => "izkpm55j334u8x9y2a"
|
26
26
|
|
27
27
|
# Generate multiple IDs
|
28
28
|
ids = 5.times.map { OpaqueId.generate }
|
29
|
-
# => ["
|
29
|
+
# => ["izkpm55j334u8x9y2a", "k8jh2mn9pl3qr7st1v", ...]
|
30
30
|
```
|
31
31
|
|
32
32
|
### Custom Parameters
|
@@ -38,23 +38,27 @@ id = OpaqueId.generate(size: 10)
|
|
38
38
|
|
39
39
|
# Custom alphabet
|
40
40
|
id = OpaqueId.generate(alphabet: OpaqueId::STANDARD_ALPHABET)
|
41
|
-
# => "
|
41
|
+
# => "V1StGXR8_Z5jdHi6B-myT"
|
42
42
|
|
43
43
|
# Both custom length and alphabet
|
44
44
|
id = OpaqueId.generate(size: 15, alphabet: OpaqueId::STANDARD_ALPHABET)
|
45
|
-
# => "
|
45
|
+
# => "V1StGXR8_Z5jdHi6B"
|
46
46
|
```
|
47
47
|
|
48
48
|
### Built-in Alphabets
|
49
49
|
|
50
50
|
```ruby
|
51
|
-
#
|
51
|
+
# Slug-like alphabet (default) - 0-9, a-z
|
52
|
+
id = OpaqueId.generate(alphabet: OpaqueId::SLUG_LIKE_ALPHABET)
|
53
|
+
# => "izkpm55j334u8x9y2a"
|
54
|
+
|
55
|
+
# Alphanumeric alphabet - A-Z, a-z, 0-9
|
52
56
|
id = OpaqueId.generate(alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
|
53
|
-
# => "
|
57
|
+
# => "V1StGXR8Z5jdHi6BmyT"
|
54
58
|
|
55
59
|
# Standard alphabet - A-Z, a-z, 0-9, -, _
|
56
60
|
id = OpaqueId.generate(alphabet: OpaqueId::STANDARD_ALPHABET)
|
57
|
-
# => "
|
61
|
+
# => "V1StGXR8_Z5jdHi6B-myT"
|
58
62
|
```
|
59
63
|
|
60
64
|
### Custom Alphabets
|
@@ -94,7 +98,7 @@ end
|
|
94
98
|
# Create a new user - opaque_id is automatically generated
|
95
99
|
user = User.create!(name: "John Doe", email: "john@example.com")
|
96
100
|
puts user.opaque_id
|
97
|
-
# => "
|
101
|
+
# => "izkpm55j334u8x9y2a"
|
98
102
|
|
99
103
|
# The ID is generated before the record is saved
|
100
104
|
user = User.new(name: "Jane Smith")
|
@@ -103,17 +107,17 @@ puts user.opaque_id
|
|
103
107
|
|
104
108
|
user.save!
|
105
109
|
puts user.opaque_id
|
106
|
-
# => "
|
110
|
+
# => "k8jh2mn9pl3qr7st1va" (generated on save)
|
107
111
|
```
|
108
112
|
|
109
113
|
### Finder Methods
|
110
114
|
|
111
115
|
```ruby
|
112
116
|
# Find by opaque_id (returns nil if not found)
|
113
|
-
user = User.find_by_opaque_id("
|
117
|
+
user = User.find_by_opaque_id("izkpm55j334u8x9y2a")
|
114
118
|
|
115
119
|
# Find by opaque_id (raises exception if not found)
|
116
|
-
user = User.find_by_opaque_id!("
|
120
|
+
user = User.find_by_opaque_id!("izkpm55j334u8x9y2a")
|
117
121
|
# => ActiveRecord::RecordNotFound if not found
|
118
122
|
|
119
123
|
# Use in scopes
|
@@ -123,7 +127,7 @@ class User < ApplicationRecord
|
|
123
127
|
scope :by_opaque_id, ->(id) { where(opaque_id: id) }
|
124
128
|
end
|
125
129
|
|
126
|
-
users = User.by_opaque_id("
|
130
|
+
users = User.by_opaque_id("izkpm55j334u8x9y2a")
|
127
131
|
```
|
128
132
|
|
129
133
|
### Custom Column Names
|
@@ -139,9 +143,9 @@ end
|
|
139
143
|
# Now the methods use the custom column name
|
140
144
|
user = User.create!(name: "John Doe")
|
141
145
|
puts user.public_id
|
142
|
-
# => "
|
146
|
+
# => "izkpm55j334u8x9y2a"
|
143
147
|
|
144
|
-
user = User.find_by_public_id("
|
148
|
+
user = User.find_by_public_id("izkpm55j334u8x9y2a")
|
145
149
|
```
|
146
150
|
|
147
151
|
## Rails Generator
|
@@ -259,7 +263,7 @@ end
|
|
259
263
|
# Create a user
|
260
264
|
user = User.create!(name: "John Doe", email: "john@example.com")
|
261
265
|
puts user.opaque_id
|
262
|
-
# => "
|
266
|
+
# => "izkpm55j334u8x9y2a" (starts with letter)
|
263
267
|
|
264
268
|
# Use in user profiles
|
265
269
|
user_url(user.opaque_id)
|
@@ -273,7 +277,7 @@ user_url(user.opaque_id)
|
|
273
277
|
```ruby
|
274
278
|
# Generate multiple IDs at once
|
275
279
|
ids = 100.times.map { OpaqueId.generate }
|
276
|
-
# => ["
|
280
|
+
# => ["izkpm55j334u8x9y2a", "k8jh2mn9pl3qr7st1va", ...]
|
277
281
|
|
278
282
|
# Use in bulk operations
|
279
283
|
users_data = ids.map.with_index do |id, index|
|
@@ -15,8 +15,12 @@ module OpaqueId
|
|
15
15
|
class_option :column_name, type: :string, default: 'opaque_id',
|
16
16
|
desc: 'Name of the column to add'
|
17
17
|
|
18
|
+
class_option :limit, type: :numeric, default: 21,
|
19
|
+
desc: 'Column limit for the opaque_id string (default: 21)'
|
20
|
+
|
18
21
|
def create_migration_file
|
19
22
|
if model_name.present?
|
23
|
+
validate_limit_option
|
20
24
|
table_name = model_name.tableize
|
21
25
|
migration_template 'migration.rb.tt',
|
22
26
|
"db/migrate/add_opaque_id_to_#{table_name}.rb"
|
@@ -26,11 +30,20 @@ module OpaqueId
|
|
26
30
|
say 'Usage: rails generate opaque_id:install ModelName', :red
|
27
31
|
say 'Example: rails generate opaque_id:install User', :green
|
28
32
|
say 'Example: rails generate opaque_id:install Post --column-name=public_id', :green
|
33
|
+
say 'Example: rails generate opaque_id:install Post --limit=25', :green
|
29
34
|
end
|
30
35
|
end
|
31
36
|
|
32
37
|
private
|
33
38
|
|
39
|
+
def validate_limit_option
|
40
|
+
# Default OpaqueId length is 18, so limit should be at least that
|
41
|
+
return unless options[:limit] < 18
|
42
|
+
|
43
|
+
say "Warning: Column limit (#{options[:limit]}) is less than the default OpaqueId length (18).", :yellow
|
44
|
+
say 'Consider using --limit=21 or higher to avoid truncation issues.', :yellow
|
45
|
+
end
|
46
|
+
|
34
47
|
def add_include_to_model
|
35
48
|
model_path = "app/models/#{model_name.underscore}.rb"
|
36
49
|
|
@@ -1,6 +1,6 @@
|
|
1
|
-
class AddOpaqueIdTo<%=
|
1
|
+
class AddOpaqueIdTo<%= model_name.tableize.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
2
|
def change
|
3
|
-
add_column :<%=
|
4
|
-
add_index :<%=
|
3
|
+
add_column :<%= model_name.tableize %>, :<%= options[:column_name] %>, :string, limit: <%= options[:limit] %>
|
4
|
+
add_index :<%= model_name.tableize %>, :<%= options[:column_name] %>, unique: true
|
5
5
|
end
|
6
6
|
end
|
data/lib/opaque_id/model.rb
CHANGED
@@ -30,7 +30,7 @@ module OpaqueId
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def opaque_id_length
|
33
|
-
@opaque_id_length ||=
|
33
|
+
@opaque_id_length ||= 18
|
34
34
|
end
|
35
35
|
|
36
36
|
def opaque_id_length=(value)
|
@@ -38,7 +38,7 @@ module OpaqueId
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def opaque_id_alphabet
|
41
|
-
@opaque_id_alphabet ||= OpaqueId::
|
41
|
+
@opaque_id_alphabet ||= OpaqueId::SLUG_LIKE_ALPHABET
|
42
42
|
end
|
43
43
|
|
44
44
|
def opaque_id_alphabet=(value)
|
data/lib/opaque_id/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: opaque_id
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joe Nyaggah
|
@@ -140,6 +140,7 @@ files:
|
|
140
140
|
- docs/assets/css/custom.scss
|
141
141
|
- docs/assets/images/favicon.svg
|
142
142
|
- docs/assets/images/og-image.svg
|
143
|
+
- docs/benchmarks.md
|
143
144
|
- docs/configuration.md
|
144
145
|
- docs/development.md
|
145
146
|
- docs/getting-started.md
|