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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 016b3d2bbd7ae3af1ba38973c0b590164d7be1a4bd60a8c0e0379cfca1c9dc0f
4
- data.tar.gz: 2924779f8769585995732ce79da51b8a1171c0b22fd33636c6476f77815adb44
3
+ metadata.gz: 4f3a94f08b1d9f3e21a513fe3f7ae6422c7288c2ccef38c78394209e835d264d
4
+ data.tar.gz: 114701a6d202fa5d41ac77da871f86ffc88182d54ffb58ea8ab2324bda177fee
5
5
  SHA512:
6
- metadata.gz: 82206f53378e08c302fbb9bd56caf94238e3b0780b6a271732d00d04f0e7e37136c1c6c941ef58fe6c6fffd154c7ce1b82c0b12b76240d23399007d34d81a0bd
7
- data.tar.gz: b5d588f50a7a2337b42fc4e021b9aef658cf3e9f92c64fe5c21c7bf901560d5575b48c05fe72edefa77112175357562b0497c4c3d2ba73dd23be2165b6eadd87
6
+ metadata.gz: 66f5027ec5599b164599a7c5f4d6bc6c7396787671ea7a2238bc95385482150bfb5b4e1341173264fc870af0b851d45c4faad6c9396519f96e11c00b5ff5978c
7
+ data.tar.gz: 50b3da266692f3b004d2c00f9a485d5bdfe0af51465dd4f87f5189d109cbfd3849c29a741ce4c3a4fb8565970594696e48467bf2e1fc8312b7356c7f1a87bd80
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.6.0"
2
+ ".": "1.7.0"
3
3
  }
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
- - [`ALPHANUMERIC_ALPHABET` (Default)](#alphanumeric_alphabet-default)
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 # => "izkpm55j334u8x9y2"
180
+ puts user.opaque_id # => "izkpm55j334u8x9y2a"
180
181
 
181
182
  # Find by opaque ID
182
- user = User.find_by_opaque_id("izkpm55j334u8x9y2")
183
- user = User.find_by_opaque_id!("izkpm55j334u8x9y2") # raises if not found
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
- # => "izkpm55j334u8x9y2" # Perfect for URLs and user selection
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
- # => "izkpm55j334u8x9y2"
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
- # => "izkpm55j334u8x9y2"
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
- # => "izkpm55j334u8x9y2"
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
- # => "izkpm55j334u8x9y2"
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 # => "izkpm55j334u8x9y2"
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: "izkpm55j334u8x9y2"
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("izkpm55j334u8x9y2")
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!("izkpm55j334u8x9y2")
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 | Description | Example Usage |
671
- | -------------------------------- | --------------- | ----------------------- | ----------------------------------------------- | ------------------------------------------------------- |
672
- | `opaque_id_column` | `Symbol` | `:opaque_id` | Column name for storing the opaque ID | `self.opaque_id_column = :public_id` |
673
- | `opaque_id_length` | `Integer` | `21` | Length of generated IDs | `self.opaque_id_length = 32` |
674
- | `opaque_id_alphabet` | `String` | `ALPHANUMERIC_ALPHABET` | Character set for ID generation | `self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET` |
675
- | `opaque_id_require_letter_start` | `Boolean` | `false` | Require ID to start with a letter | `self.opaque_id_require_letter_start = true` |
676
- | `opaque_id_purge_chars` | `Array<String>` | `[]` | Characters to remove from generated IDs | `self.opaque_id_purge_chars = ['0', 'O', 'I', 'l']` |
677
- | `opaque_id_max_retry` | `Integer` | `3` | Maximum retry attempts for collision resolution | `self.opaque_id_max_retry = 10` |
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: `"izkpm55j334u8x9y2"`
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` → `"izkpm55j334u8x9y2"`, `false` → `"zkpm55j334u8x9y2"`
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.
@@ -19,20 +19,20 @@ The main module for generating opaque IDs.
19
19
 
20
20
  ### Constants
21
21
 
22
- #### ALPHANUMERIC_ALPHABET
22
+ #### SLUG_LIKE_ALPHABET
23
23
 
24
24
  Default alphabet for ID generation.
25
25
 
26
26
  ```ruby
27
- OpaqueId::ALPHANUMERIC_ALPHABET
28
- # => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
27
+ OpaqueId::SLUG_LIKE_ALPHABET
28
+ # => "0123456789abcdefghijklmnopqrstuvwxyz"
29
29
  ```
30
30
 
31
31
  **Characteristics:**
32
32
 
33
- - **Length**: 62 characters
34
- - **Characters**: A-Z, a-z, 0-9
35
- - **Use case**: General purpose, URL-safe
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
@@ -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.
@@ -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: 21)
29
+ # Custom length (default: 18)
30
30
  self.opaque_id_length = 15
31
31
 
32
- # Custom alphabet (default: ALPHANUMERIC_ALPHABET)
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 | Description |
49
- | -------------------------------- | ------- | ----------------------- | --------------------------------------------- |
50
- | `opaque_id_column` | Symbol | `:opaque_id` | Column name for storing the opaque ID |
51
- | `opaque_id_length` | Integer | `21` | Length of generated IDs |
52
- | `opaque_id_alphabet` | String | `ALPHANUMERIC_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 |
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
- #### ALPHANUMERIC_ALPHABET (Default)
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: "V1StGXR8_Z5jdHi6B-myT"
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
- # => "V1StGXR8_Z5jdHi6B-myT"
256
+ # => "izkpm55j334u8x9y2a"
244
257
 
245
- user = User.find_by_public_id("V1StGXR8_Z5jdHi6B-myT")
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
- # => "V1StGXR8_Z5jdHi6B-myT" (starts with 'V')
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
- # => "V1StGXR8_Z5jdHi6B-myT" (no '0', 'O', 'l', 'I')
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 21, user.class.opaque_id_length
500
- assert_equal OpaqueId::ALPHANUMERIC_ALPHABET, user.class.opaque_id_alphabet
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 # => "izkpm55j334u8x9y2"
60
+ puts user.opaque_id # => "izkpm55j334u8x9y2a"
61
61
 
62
62
  # Find by opaque_id
63
- user = User.find_by_opaque_id("izkpm55j334u8x9y2")
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
- # => "izkpm55j334u8x9y2"
25
+ # => "izkpm55j334u8x9y2a"
26
26
 
27
27
  # Generate multiple IDs
28
28
  ids = 5.times.map { OpaqueId.generate }
29
- # => ["V1StGXR8_Z5jdHi6B-myT", "K8jH2mN9_pL3qR7sT1v", ...]
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
- # => "izkpm55j334u8x9y2"
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
- # => "izkpm55j334u8x9y"
45
+ # => "V1StGXR8_Z5jdHi6B"
46
46
  ```
47
47
 
48
48
  ### Built-in Alphabets
49
49
 
50
50
  ```ruby
51
- # Alphanumeric alphabet (default) - A-Z, a-z, 0-9
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
- # => "izkpm55j334u8x9y2"
57
+ # => "V1StGXR8Z5jdHi6BmyT"
54
58
 
55
59
  # Standard alphabet - A-Z, a-z, 0-9, -, _
56
60
  id = OpaqueId.generate(alphabet: OpaqueId::STANDARD_ALPHABET)
57
- # => "izkpm55j334u8x9y2"
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
- # => "izkpm55j334u8x9y2"
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
- # => "K8jH2mN9_pL3qR7sT1v" (generated on save)
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("izkpm55j334u8x9y2")
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!("izkpm55j334u8x9y2")
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("izkpm55j334u8x9y2")
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
- # => "izkpm55j334u8x9y2"
146
+ # => "izkpm55j334u8x9y2a"
143
147
 
144
- user = User.find_by_public_id("V1StGXR8_Z5jdHi6B-myT")
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
- # => "izkpm55j334u8x9y2" (starts with letter)
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
- # => ["V1StGXR8_Z5jdHi6B-myT", "K8jH2mN9_pL3qR7sT1v", ...]
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<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
1
+ class AddOpaqueIdTo<%= model_name.tableize.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
- add_column :<%= table_name %>, :<%= options[:column_name] %>, :string
4
- add_index :<%= table_name %>, :<%= options[:column_name] %>, unique: true
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
@@ -30,7 +30,7 @@ module OpaqueId
30
30
  end
31
31
 
32
32
  def opaque_id_length
33
- @opaque_id_length ||= 21
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::ALPHANUMERIC_ALPHABET
41
+ @opaque_id_alphabet ||= OpaqueId::SLUG_LIKE_ALPHABET
42
42
  end
43
43
 
44
44
  def opaque_id_alphabet=(value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpaqueId
4
- VERSION = '1.6.0'
4
+ VERSION = '1.7.0'
5
5
  end
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.6.0
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