opaque_id 1.4.0 → 1.6.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.
@@ -1,482 +0,0 @@
1
- # OpaqueId Gem - Complete File Structure
2
-
3
- ## File Structure
4
-
5
- ```
6
- opaque_id/
7
- ├── lib/
8
- │ ├── opaque_id.rb # Main module with generator
9
- │ ├── opaque_id/
10
- │ │ ├── version.rb # Version constant
11
- │ │ └── model.rb # ActiveRecord concern
12
- │ └── generators/
13
- │ └── opaque_id/
14
- │ ├── install_generator.rb # Migration generator
15
- │ └── templates/
16
- │ └── migration.rb.tt # Migration template
17
- ├── spec/
18
- │ ├── spec_helper.rb
19
- │ ├── opaque_id_spec.rb
20
- │ └── opaque_id/
21
- │ └── model_spec.rb
22
- ├── opaque_id.gemspec
23
- ├── Gemfile
24
- ├── Rakefile
25
- ├── README.md
26
- ├── LICENSE.txt
27
- └── CHANGELOG.md
28
- ```
29
-
30
- ## opaque_id.gemspec
31
-
32
- ```ruby
33
- # frozen_string_literal: true
34
-
35
- require_relative "lib/opaque_id/version"
36
-
37
- Gem::Specification.new do |spec|
38
- spec.name = "opaque_id"
39
- spec.version = OpaqueId::VERSION
40
- spec.authors = ["Your Name"]
41
- spec.email = ["your.email@example.com"]
42
-
43
- spec.summary = "Generate cryptographically secure, collision-free opaque IDs for ActiveRecord models"
44
- spec.description = <<~DESC
45
- OpaqueId provides a simple way to generate unique, URL-friendly identifiers for your ActiveRecord models.
46
- Uses rejection sampling for unbiased random generation, ensuring perfect uniformity across the alphabet.
47
- Prevents exposing incremental database IDs in URLs and APIs.
48
- DESC
49
- spec.homepage = "https://github.com/yourusername/opaque_id"
50
- spec.license = "MIT"
51
- spec.required_ruby_version = ">= 2.7.0"
52
-
53
- spec.metadata["homepage_uri"] = spec.homepage
54
- spec.metadata["source_code_uri"] = "https://github.com/yourusername/opaque_id"
55
- spec.metadata["changelog_uri"] = "https://github.com/yourusername/opaque_id/blob/main/CHANGELOG.md"
56
-
57
- spec.files = Dir.chdir(__dir__) do
58
- `git ls-files -z`.split("\x0").reject do |f|
59
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
60
- end
61
- end
62
-
63
- spec.bindir = "exe"
64
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
65
- spec.require_paths = ["lib"]
66
-
67
- # Dependencies
68
- spec.add_dependency "activerecord", ">= 6.0"
69
- spec.add_dependency "activesupport", ">= 6.0"
70
-
71
- # Development dependencies
72
- spec.add_development_dependency "rake", "~> 13.0"
73
- spec.add_development_dependency "rspec", "~> 3.0"
74
- spec.add_development_dependency "sqlite3", "~> 1.4"
75
- spec.add_development_dependency "rubocop", "~> 1.21"
76
- end
77
- ```
78
-
79
- ## Gemfile
80
-
81
- ```ruby
82
- # frozen_string_literal: true
83
-
84
- source "https://rubygems.org"
85
-
86
- gemspec
87
-
88
- gem "rake", "~> 13.0"
89
- gem "rspec", "~> 3.0"
90
- gem "sqlite3", "~> 1.4"
91
- gem "rubocop", "~> 1.21"
92
- ```
93
-
94
- ## Rakefile
95
-
96
- ```ruby
97
- # frozen_string_literal: true
98
-
99
- require "bundler/gem_tasks"
100
- require "rspec/core/rake_task"
101
- require "rubocop/rake_task"
102
-
103
- RSpec::Core::RakeTask.new(:spec)
104
- RuboCop::RakeTask.new
105
-
106
- task default: %i[spec rubocop]
107
- ```
108
-
109
- ## lib/opaque_id.rb (Entry Point)
110
-
111
- ```ruby
112
- # frozen_string_literal: true
113
-
114
- require "securerandom"
115
- require_relative "opaque_id/version"
116
- require_relative "opaque_id/model"
117
-
118
- module OpaqueId
119
- class Error < StandardError; end
120
- class GenerationError < Error; end
121
- class ConfigurationError < Error; end
122
-
123
- # Standard URL-safe alphabet (64 characters)
124
- STANDARD_ALPHABET = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
125
-
126
- # Lowercase alphanumeric (36 characters)
127
- ALPHANUMERIC_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
128
-
129
- class << self
130
- # Generate a cryptographically secure random ID
131
- def generate(size: 21, alphabet: ALPHANUMERIC_ALPHABET)
132
- raise ConfigurationError, "Size must be positive" unless size.positive?
133
- raise ConfigurationError, "Alphabet cannot be empty" if alphabet.empty?
134
-
135
- alphabet_size = alphabet.size
136
- return generate_fast(size, alphabet) if alphabet_size == 64
137
-
138
- generate_unbiased(size, alphabet, alphabet_size)
139
- end
140
-
141
- private
142
-
143
- def generate_fast(size, alphabet)
144
- bytes = SecureRandom.random_bytes(size).bytes
145
- size.times.map { |i| alphabet[bytes[i] & 63] }.join
146
- end
147
-
148
- def generate_unbiased(size, alphabet, alphabet_size)
149
- mask = (2 << Math.log2(alphabet_size - 1).floor) - 1
150
- step = (1.6 * mask * size / alphabet_size).ceil
151
- id = String.new(capacity: size)
152
-
153
- loop do
154
- bytes = SecureRandom.random_bytes(step).bytes
155
- bytes.each do |byte|
156
- masked_byte = byte & mask
157
- if masked_byte < alphabet_size
158
- id << alphabet[masked_byte]
159
- return id if id.size == size
160
- end
161
- end
162
- end
163
- end
164
- end
165
- end
166
- ```
167
-
168
- ## lib/generators/opaque_id/install_generator.rb
169
-
170
- ```ruby
171
- # frozen_string_literal: true
172
-
173
- require "rails/generators"
174
- require "rails/generators/active_record"
175
-
176
- module OpaqueId
177
- module Generators
178
- class InstallGenerator < Rails::Generators::Base
179
- include ActiveRecord::Generators::Migration
180
-
181
- source_root File.expand_path("templates", __dir__)
182
-
183
- argument :table_name, type: :string, default: nil, banner: "table_name"
184
-
185
- class_option :column_name, type: :string, default: "opaque_id",
186
- desc: "Name of the column to add"
187
-
188
- def create_migration_file
189
- if table_name.present?
190
- migration_template "migration.rb.tt",
191
- "db/migrate/add_opaque_id_to_#{table_name}.rb"
192
- else
193
- say "Usage: rails generate opaque_id:install TABLE_NAME", :red
194
- say "Example: rails generate opaque_id:install posts", :green
195
- end
196
- end
197
- end
198
- end
199
- end
200
- ```
201
-
202
- ## lib/generators/opaque_id/templates/migration.rb.tt
203
-
204
- ```ruby
205
- class AddOpaqueIdTo<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
206
- def change
207
- add_column :<%= table_name %>, :<%= options[:column_name] %>, :string
208
- add_index :<%= table_name %>, :<%= options[:column_name] %>, unique: true
209
- end
210
- end
211
- ```
212
-
213
- ## README.md
214
-
215
- ````markdown
216
- # OpaqueId
217
-
218
- Generate cryptographically secure, collision-free opaque IDs for your ActiveRecord models. Perfect for exposing public identifiers in URLs and APIs without revealing your database's internal structure.
219
-
220
- ## Features
221
-
222
- - 🔒 **Cryptographically secure** - Uses Ruby's `SecureRandom` for entropy
223
- - 🎯 **Unbiased generation** - Implements rejection sampling for perfect uniformity
224
- - ⚡ **Performance optimized** - Fast path for 64-character alphabets
225
- - 🎨 **Highly configurable** - Customize alphabet, length, and behavior per model
226
- - ✅ **HTML-valid by default** - IDs start with letters for use as HTML element IDs
227
- - 🔄 **Collision detection** - Automatic retry logic with configurable attempts
228
- - 🚀 **Zero dependencies** - Only requires ActiveSupport/ActiveRecord
229
-
230
- ## Installation
231
-
232
- Add to your Gemfile:
233
-
234
- ```ruby
235
- gem 'opaque_id'
236
- ```
237
- ````
238
-
239
- Then run:
240
-
241
- ```bash
242
- bundle install
243
- rails generate opaque_id:install posts
244
- rails db:migrate
245
- ```
246
-
247
- ## Usage
248
-
249
- ### Basic Usage
250
-
251
- ```ruby
252
- class Post < ApplicationRecord
253
- include OpaqueId::Model
254
- end
255
-
256
- post = Post.create(title: "Hello World")
257
- post.opaque_id #=> "k3x9m2n8p5q7r4t6"
258
- ```
259
-
260
- ### Custom Configuration
261
-
262
- ```ruby
263
- class Invoice < ApplicationRecord
264
- include OpaqueId::Model
265
-
266
- self.opaque_id_column = :public_id # Custom column name
267
- self.opaque_id_length = 24 # Longer IDs
268
- self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET # 64-char alphabet
269
- self.opaque_id_require_letter_start = false # Allow starting with numbers
270
- self.opaque_id_purge_chars = %w[0 1 5 o O i I l] # Exclude confusing chars
271
- self.opaque_id_max_retry = 2000 # More collision attempts
272
- end
273
- ```
274
-
275
- ### Standalone Generation
276
-
277
- ```ruby
278
- # Generate a random ID
279
- OpaqueId.generate #=> "k3x9m2n8p5q7r4t6"
280
-
281
- # Custom size
282
- OpaqueId.generate(size: 32) #=> "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
283
-
284
- # Custom alphabet
285
- OpaqueId.generate(
286
- size: 21,
287
- alphabet: OpaqueId::STANDARD_ALPHABET
288
- ) #=> "V-x3_Kp9Mq2Nn8Rt6Wz4"
289
- ```
290
-
291
- ### Finding Records
292
-
293
- ```ruby
294
- Post.find_by_opaque_id("k3x9m2n8p5q7r4t6")
295
- Post.find_by_opaque_id!("k3x9m2n8p5q7r4t6") # Raises if not found
296
- ```
297
-
298
- ## Configuration Options
299
-
300
- | Option | Default | Description |
301
- | -------------------------------- | ------------ | ---------------------------- |
302
- | `opaque_id_column` | `:opaque_id` | Database column name |
303
- | `opaque_id_length` | `18` | Length of generated IDs |
304
- | `opaque_id_alphabet` | `0-9a-z` | Character set for IDs |
305
- | `opaque_id_require_letter_start` | `true` | Ensure HTML validity |
306
- | `opaque_id_purge_chars` | `nil` | Characters to exclude |
307
- | `opaque_id_max_retry` | `1000` | Max collision retry attempts |
308
-
309
- ## Alphabets
310
-
311
- ### ALPHANUMERIC_ALPHABET (default)
312
-
313
- - **Characters**: `0123456789abcdefghijklmnopqrstuvwxyz`
314
- - **Size**: 36 characters
315
- - **Use case**: User-facing IDs, URLs
316
-
317
- ### STANDARD_ALPHABET
318
-
319
- - **Characters**: `_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`
320
- - **Size**: 64 characters
321
- - **Use case**: Maximum entropy, API keys
322
-
323
- ## Algorithm
324
-
325
- OpaqueId uses **rejection sampling** to ensure perfectly uniform distribution:
326
-
327
- 1. Calculate optimal bit mask based on alphabet size
328
- 2. Generate random bytes using `SecureRandom`
329
- 3. Apply mask and check if value is within alphabet range
330
- 4. Accept valid values, reject others (no modulo bias)
331
-
332
- For 64-character alphabets, uses optimized bitwise operations (`byte & 63`).
333
-
334
- ## Performance
335
-
336
- Benchmarks on Ruby 3.3 (1M iterations):
337
-
338
- ```
339
- Standard alphabet (64 chars): ~1.2M IDs/sec
340
- Alphanumeric (36 chars): ~180K IDs/sec
341
- Custom alphabet (20 chars): ~150K IDs/sec
342
- ```
343
-
344
- ## Why OpaqueId?
345
-
346
- ### The Problem
347
-
348
- ```ruby
349
- # ❌ Exposes database structure
350
- GET /api/posts/1
351
- GET /api/posts/2 # Easy to enumerate
352
-
353
- # ❌ Reveals business metrics
354
- GET /api/invoices/10523 # "They have 10,523 invoices"
355
- ```
356
-
357
- ### The Solution
358
-
359
- ```ruby
360
- # ✅ Opaque, non-sequential IDs
361
- GET /api/posts/k3x9m2n8p5q7r4
362
- GET /api/posts/t6v8w1x4y7z9a2
363
-
364
- # ✅ No information leakage
365
- GET /api/invoices/m3n8p5q7r4t6v8
366
- ```
367
-
368
- ## License
369
-
370
- MIT License - see LICENSE.txt
371
-
372
- ## Contributing
373
-
374
- 1. Fork it
375
- 2. Create your feature branch (`git checkout -b my-new-feature`)
376
- 3. Commit your changes (`git commit -am 'Add some feature'`)
377
- 4. Push to the branch (`git push origin my-new-feature`)
378
- 5. Create new Pull Request
379
-
380
- ````
381
-
382
- ## spec/spec_helper.rb
383
-
384
- ```ruby
385
- # frozen_string_literal: true
386
-
387
- require "opaque_id"
388
- require "active_record"
389
-
390
- ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
391
-
392
- ActiveRecord::Schema.define do
393
- create_table :test_models, force: true do |t|
394
- t.string :opaque_id
395
- t.timestamps
396
- end
397
-
398
- add_index :test_models, :opaque_id, unique: true
399
- end
400
-
401
- class TestModel < ActiveRecord::Base
402
- include OpaqueId::Model
403
- end
404
-
405
- RSpec.configure do |config|
406
- config.expect_with :rspec do |expectations|
407
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
408
- end
409
-
410
- config.mock_with :rspec do |mocks|
411
- mocks.verify_partial_doubles = true
412
- end
413
-
414
- config.shared_context_metadata_behavior = :apply_to_host_groups
415
- end
416
- ````
417
-
418
- ## spec/opaque_id_spec.rb
419
-
420
- ```ruby
421
- # frozen_string_literal: true
422
-
423
- require "spec_helper"
424
-
425
- RSpec.describe OpaqueId do
426
- describe ".generate" do
427
- it "generates IDs of default length" do
428
- id = described_class.generate
429
- expect(id.length).to eq(21)
430
- end
431
-
432
- it "generates IDs of custom length" do
433
- id = described_class.generate(size: 32)
434
- expect(id.length).to eq(32)
435
- end
436
-
437
- it "uses only characters from the alphabet" do
438
- alphabet = "abc123"
439
- id = described_class.generate(size: 100, alphabet: alphabet)
440
- expect(id.chars.all? { |c| alphabet.include?(c) }).to be true
441
- end
442
-
443
- it "generates unique IDs" do
444
- ids = 10_000.times.map { described_class.generate }
445
- expect(ids.uniq.length).to eq(10_000)
446
- end
447
-
448
- it "raises error for invalid size" do
449
- expect { described_class.generate(size: 0) }.to raise_error(OpaqueId::ConfigurationError)
450
- expect { described_class.generate(size: -1) }.to raise_error(OpaqueId::ConfigurationError)
451
- end
452
-
453
- it "raises error for empty alphabet" do
454
- expect { described_class.generate(alphabet: "") }.to raise_error(OpaqueId::ConfigurationError)
455
- end
456
-
457
- context "with 64-character alphabet" do
458
- it "uses fast path" do
459
- id = described_class.generate(size: 21, alphabet: OpaqueId::STANDARD_ALPHABET)
460
- expect(id.length).to eq(21)
461
- expect(id.chars.all? { |c| OpaqueId::STANDARD_ALPHABET.include?(c) }).to be true
462
- end
463
- end
464
- end
465
-
466
- describe "statistical uniformity" do
467
- it "distributes characters evenly" do
468
- alphabet = "0123456789"
469
- samples = 10_000.times.map { described_class.generate(size: 1, alphabet: alphabet) }
470
-
471
- frequency = samples.tally
472
- expected = samples.length / alphabet.length
473
-
474
- # Chi-square test: all frequencies should be within 20% of expected
475
- frequency.each_value do |count|
476
- deviation = (count - expected).abs.to_f / expected
477
- expect(deviation).to be < 0.2
478
- end
479
- end
480
- end
481
- end
482
- ```
@@ -1,110 +0,0 @@
1
- This is the original functionality we are replicating and enhancing. typically implemented as a concern in a Model via `include Identifiable`
2
-
3
- ```ruby
4
- require "nanoid"
5
-
6
- # generate a public_id (nanoid) when creating new records
7
- # unique URL-friendly ID that'll prevent exposing incremental db ids where
8
- # we otherwise can't or don't want to use friendly_ids e.g. in URLs and APIs
9
- #
10
- # https://planetscale.com/blog/why-we-chose-nanoids-for-planetscales-api#generating-nanoids-in-rails
11
- # https://maful.web.id/posts/how-i-use-nano-id-in-rails/
12
- # https://zelark.github.io/nano-id-cc/
13
- module Identifiable
14
- extend ActiveSupport::Concern
15
-
16
- included do
17
- before_create :set_public_id
18
- end
19
-
20
- PUBLIC_ID_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
21
- PUBLIC_ID_LENGTH = 18
22
- MAX_RETRY = 1000
23
-
24
- PUBLIC_ID_REGEX = /[#{PUBLIC_ID_ALPHABET}]{#{PUBLIC_ID_LENGTH}}\z/
25
-
26
- class_methods do
27
- def generate_nanoid(alphabet: PUBLIC_ID_ALPHABET, size: PUBLIC_ID_LENGTH)
28
- Nanoid.generate(size:, alphabet:)
29
- end
30
- end
31
-
32
- def set_public_id
33
- return if public_id.present?
34
-
35
- MAX_RETRY.times do
36
- self.public_id = generate_public_id
37
- return unless self.class.where(public_id:).exists?
38
- end
39
- raise "Failed to generate a unique public id after #{MAX_RETRY} attempts"
40
- end
41
-
42
- def generate_public_id
43
- self.class.generate_nanoid(alphabet: PUBLIC_ID_ALPHABET)
44
- end
45
- end
46
- ```
47
-
48
- The original nanoid.rb is below
49
-
50
- ```ruby
51
- require 'securerandom'
52
-
53
- module Nanoid
54
- SAFE_ALPHABET = '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.freeze
55
-
56
- def self.generate(size: 21, alphabet: SAFE_ALPHABET, non_secure: false)
57
- return non_secure_generate(size: size, alphabet: alphabet) if non_secure
58
-
59
- return simple_generate(size: size) if alphabet == SAFE_ALPHABET
60
-
61
- complex_generate(size: size, alphabet: alphabet)
62
- end
63
-
64
- private
65
-
66
- def self.simple_generate(size:)
67
- bytes = random_bytes(size)
68
-
69
- (0...size).reduce('') do |acc, i|
70
- acc << SAFE_ALPHABET[bytes[i] & 63]
71
- end
72
- end
73
-
74
- def self.complex_generate(size:, alphabet:)
75
- alphabet_size = alphabet.size
76
- mask = (2 << Math.log(alphabet_size - 1) / Math.log(2)) - 1
77
- step = (1.6 * mask * size / alphabet_size).ceil
78
-
79
- id = ''
80
-
81
- loop do
82
- bytes = random_bytes(size)
83
- (0...step).each do |idx|
84
- byte = bytes[idx] & mask
85
- character = byte && alphabet[byte]
86
- if character
87
- id << character
88
- return id if id.size == size
89
- end
90
- end
91
- end
92
- end
93
-
94
- def self.non_secure_generate(size:, alphabet:)
95
- alphabet_size = alphabet.size
96
-
97
- id = ''
98
-
99
- size.times do
100
- id << alphabet[(Random.rand * alphabet_size).floor]
101
- end
102
-
103
- id
104
- end
105
-
106
- def self.random_bytes(size)
107
- SecureRandom.random_bytes(size).bytes
108
- end
109
- end
110
- ```
@@ -1,109 +0,0 @@
1
- # Task List: OpaqueId Ruby Gem Implementation
2
-
3
- Based on PRD: `0001-prd-opaque-id-gem.md`
4
-
5
- ## Relevant Files
6
-
7
- - `lib/opaque_id.rb` - Main module with core ID generation functionality and error classes
8
- - `lib/opaque_id/version.rb` - Version constant (already exists)
9
- - `lib/opaque_id/model.rb` - ActiveRecord concern for model integration
10
- - `lib/generators/opaque_id/install_generator.rb` - Rails generator for migrations and model updates
11
- - `lib/generators/opaque_id/templates/migration.rb.tt` - Migration template for generator
12
- - `opaque_id.gemspec` - Gem specification and dependencies (updated for Rails 8.0+)
13
- - `Gemfile` - Development dependencies
14
- - `Rakefile` - Rake tasks for testing and linting
15
- - `.rubocop.yml` - RuboCop configuration for code quality
16
- - `test/test_helper.rb` - Test setup with Rails configuration and deprecation fixes
17
- - `test/test_opaque_id.rb` - Unit tests for main module
18
- - `test/opaque_id_test.rb` - Comprehensive tests for core functionality
19
- - `test/opaque_id/model_test.rb` - Unit tests for ActiveRecord concern
20
- - `test/opaque_id/generators/install_generator_test.rb` - Unit tests for generator
21
- - `README.md` - Documentation and usage examples
22
- - `CHANGELOG.md` - Version history and changes
23
-
24
- ### Notes
25
-
26
- - Unit tests should be placed in the `test/` directory following Minitest conventions
27
- - Use `ruby -Ilib:test test/test_opaque_id.rb` to run specific test files
28
- - Use `rake test` to run all tests (once Rakefile is configured)
29
- - The existing `test/test_helper.rb` already sets up Minitest and loads the gem
30
-
31
- ## Tasks
32
-
33
- - [x] 1.0 Implement Core ID Generation Module
34
-
35
- - [x] 1.1 Create error classes (Error, GenerationError, ConfigurationError)
36
- - [x] 1.2 Implement alphabet constants (STANDARD_ALPHABET, ALPHANUMERIC_ALPHABET)
37
- - [x] 1.3 Implement generate_fast method for 64-character alphabets using bitwise operations
38
- - [x] 1.4 Implement generate_unbiased method with rejection sampling algorithm
39
- - [x] 1.5 Implement main generate method with parameter validation
40
- - [x] 1.6 Add proper error handling for invalid size and empty alphabet
41
- - [x] 1.7 Add require statements for SecureRandom and version
42
-
43
- - [x] 2.0 Create ActiveRecord Model Integration
44
-
45
- - [x] 2.1 Create OpaqueId::Model concern with ActiveSupport::Concern
46
- - [x] 2.2 Implement before_create callback for automatic ID generation
47
- - [x] 2.3 Add find_by_opaque_id and find_by_opaque_id! class methods
48
- - [x] 2.4 Implement collision detection with configurable retry attempts
49
- - [x] 2.5 Add configuration options (column, length, alphabet, require_letter_start, purge_chars, max_retry)
50
- - [x] 2.6 Implement set_opaque_id private method with collision handling
51
- - [x] 2.7 Add generate_opaque_id private method using main module
52
- - [x] 2.8 Handle edge cases (already has ID, collision resolution failure)
53
-
54
- - [x] 3.0 Build Rails Generator for Easy Setup
55
-
56
- - [x] 3.1 Create lib/generators/opaque_id directory structure
57
- - [x] 3.2 Implement InstallGenerator class with Rails::Generators::Base
58
- - [x] 3.3 Add table_name argument and column_name option handling
59
- - [x] 3.4 Create migration template (migration.rb.tt) with add_column and add_index
60
- - [x] 3.5 Implement create_migration_file method with error handling
61
- - [x] 3.6 Add model file modification to include OpaqueId::Model
62
- - [x] 3.7 Handle edge cases (missing model file, already included, different class names)
63
- - [x] 3.8 Add proper console output and error messages
64
-
65
- - [x] 4.0 Update Gem Configuration and Dependencies
66
-
67
- - [x] 4.1 Update gemspec with proper summary and description
68
- - [x] 4.2 Add ActiveRecord and ActiveSupport dependencies (>= 8.0)
69
- - [x] 4.3 Add development dependencies (rake, sqlite3, rubocop, rails)
70
- - [x] 4.4 Update metadata (homepage, source_code_uri, changelog_uri)
71
- - [x] 4.5 Set required_ruby_version to >= 3.2
72
- - [x] 4.6 Configure file inclusion and exclusion patterns
73
- - [x] 4.7 Update Gemfile to match gemspec dependencies
74
- - [x] 4.8 Update Rakefile with test and rubocop tasks
75
-
76
- - [x] 5.0 Create Comprehensive Test Suite
77
-
78
- - [x] 5.1 Create test/opaque_id directory structure
79
- - [x] 5.2 Implement test_opaque_id.rb with core module tests
80
- - [x] 5.3 Add tests for generate method (default length, custom length, alphabet validation)
81
- - [x] 5.4 Add tests for error conditions (invalid size, empty alphabet)
82
- - [x] 5.5 Add statistical uniformity tests for character distribution
83
- - [x] 5.6 Add performance benchmark tests for different alphabet sizes
84
- - [x] 5.7 Create test/opaque_id/model_test.rb for ActiveRecord concern
85
- - [x] 5.8 Test model integration (automatic generation, find methods, configuration)
86
- - [x] 5.9 Test collision detection and retry logic
87
- - [x] 5.10 Create test/opaque_id/generators/install_generator_test.rb
88
- - [x] 5.11 Test generator behavior (migration creation, model modification, edge cases)
89
- - [x] 5.12 Add test database setup and cleanup
90
-
91
- - [x] 5.13 Configure RuboCop for code quality
92
-
93
- - [x] 5.13.1 Create .rubocop.yml configuration file
94
- - [x] 5.13.2 Set appropriate rules for gem development
95
- - [x] 5.13.3 Auto-correct all RuboCop offenses
96
- - [x] 5.13.4 Fix Rails 8.1 deprecation warning in test helper
97
-
98
- - [ ] 6.0 Write Documentation and Examples
99
- - [x] 6.1 Update README.md with comprehensive feature list
100
- - [x] 6.2 Add installation instructions and gem usage
101
- - [x] 6.3 Create basic usage examples with code snippets
102
- - [x] 6.4 Add custom configuration examples
103
- - [x] 6.5 Document standalone generation usage
104
- - [x] 6.6 Add configuration options table with defaults
105
- - [x] 6.7 Document alphabet options (ALPHANUMERIC_ALPHABET, STANDARD_ALPHABET)
106
- - [x] 6.8 Add algorithm explanation and performance benchmarks
107
- - [x] 6.9 Create security considerations and use case examples
108
- - [x] 6.10 Add contributing guidelines and license information
109
- - [x] 6.11 Update CHANGELOG.md with version history