opaque_id 1.1.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 +7 -0
- data/.cursor/rules/create-prd.md +56 -0
- data/.cursor/rules/generate-tasks.md +60 -0
- data/.cursor/rules/process-task-list.md +47 -0
- data/.cursorignore +55 -0
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +31 -0
- data/CHANGELOG.md +169 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +1602 -0
- data/Rakefile +12 -0
- data/lib/generators/opaque_id/install_generator.rb +57 -0
- data/lib/generators/opaque_id/templates/migration.rb.tt +6 -0
- data/lib/opaque_id/model.rb +130 -0
- data/lib/opaque_id/version.rb +5 -0
- data/lib/opaque_id.rb +58 -0
- data/release-please-config.json +55 -0
- data/sig/opaque_id.rbs +4 -0
- data/tasks/0001-prd-opaque-id-gem.md +202 -0
- data/tasks/0002-prd-publishing-release-automation.md +206 -0
- data/tasks/references/opaque_gem_requirements.md +482 -0
- data/tasks/references/original_identifiable_concern_and_nanoid.md +110 -0
- data/tasks/tasks-0001-prd-opaque-id-gem.md +109 -0
- data/tasks/tasks-0002-prd-publishing-release-automation.md +177 -0
- metadata +168 -0
data/Rakefile
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/active_record'
|
5
|
+
|
6
|
+
module OpaqueId
|
7
|
+
module Generators
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
include ActiveRecord::Generators::Migration
|
10
|
+
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
12
|
+
|
13
|
+
argument :table_name, type: :string, default: nil, banner: 'table_name'
|
14
|
+
|
15
|
+
class_option :column_name, type: :string, default: 'opaque_id',
|
16
|
+
desc: 'Name of the column to add'
|
17
|
+
|
18
|
+
def create_migration_file
|
19
|
+
if table_name.present?
|
20
|
+
migration_template 'migration.rb.tt',
|
21
|
+
"db/migrate/add_opaque_id_to_#{table_name}.rb"
|
22
|
+
|
23
|
+
add_include_to_model
|
24
|
+
else
|
25
|
+
say 'Usage: rails generate opaque_id:install TABLE_NAME', :red
|
26
|
+
say 'Example: rails generate opaque_id:install posts', :green
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def add_include_to_model
|
33
|
+
model_path = "app/models/#{table_name.singularize}.rb"
|
34
|
+
|
35
|
+
if File.exist?(model_path)
|
36
|
+
# Read existing model file
|
37
|
+
content = File.read(model_path)
|
38
|
+
|
39
|
+
# Check if already included
|
40
|
+
if content.include?('include OpaqueId::Model')
|
41
|
+
say "OpaqueId::Model already included in #{model_path}", :yellow
|
42
|
+
else
|
43
|
+
# Add include statement
|
44
|
+
content.gsub!(/class #{table_name.classify} < ApplicationRecord/,
|
45
|
+
"class #{table_name.classify} < ApplicationRecord\n include OpaqueId::Model")
|
46
|
+
|
47
|
+
# Write back to file
|
48
|
+
File.write(model_path, content)
|
49
|
+
say "Updated #{model_path}", :green
|
50
|
+
end
|
51
|
+
else
|
52
|
+
say "Model file #{model_path} not found. Please add 'include OpaqueId::Model' manually.", :yellow
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
class AddOpaqueIdTo<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
|
+
def change
|
3
|
+
add_column :<%= table_name %>, :<%= options[:column_name] %>, :string
|
4
|
+
add_index :<%= table_name %>, :<%= options[:column_name] %>, unique: true
|
5
|
+
end
|
6
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module OpaqueId
|
6
|
+
module Model
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
before_create :set_opaque_id
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
def find_by_opaque_id(opaque_id)
|
15
|
+
where(opaque_id_column => opaque_id).first
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_by_opaque_id!(opaque_id)
|
19
|
+
find_by_opaque_id(opaque_id) || raise(ActiveRecord::RecordNotFound,
|
20
|
+
"Couldn't find #{name} with opaque_id=#{opaque_id}")
|
21
|
+
end
|
22
|
+
|
23
|
+
# Configuration options
|
24
|
+
def opaque_id_column
|
25
|
+
@opaque_id_column ||= :opaque_id
|
26
|
+
end
|
27
|
+
|
28
|
+
def opaque_id_column=(value)
|
29
|
+
@opaque_id_column = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def opaque_id_length
|
33
|
+
@opaque_id_length ||= 21
|
34
|
+
end
|
35
|
+
|
36
|
+
def opaque_id_length=(value)
|
37
|
+
@opaque_id_length = value
|
38
|
+
end
|
39
|
+
|
40
|
+
def opaque_id_alphabet
|
41
|
+
@opaque_id_alphabet ||= OpaqueId::ALPHANUMERIC_ALPHABET
|
42
|
+
end
|
43
|
+
|
44
|
+
def opaque_id_alphabet=(value)
|
45
|
+
@opaque_id_alphabet = value
|
46
|
+
end
|
47
|
+
|
48
|
+
def opaque_id_require_letter_start
|
49
|
+
@opaque_id_require_letter_start ||= false
|
50
|
+
end
|
51
|
+
|
52
|
+
def opaque_id_require_letter_start=(value)
|
53
|
+
@opaque_id_require_letter_start = value
|
54
|
+
end
|
55
|
+
|
56
|
+
def opaque_id_purge_chars
|
57
|
+
@opaque_id_purge_chars ||= []
|
58
|
+
end
|
59
|
+
|
60
|
+
def opaque_id_purge_chars=(value)
|
61
|
+
@opaque_id_purge_chars = value
|
62
|
+
end
|
63
|
+
|
64
|
+
def opaque_id_max_retry
|
65
|
+
@opaque_id_max_retry ||= 3
|
66
|
+
end
|
67
|
+
|
68
|
+
def opaque_id_max_retry=(value)
|
69
|
+
@opaque_id_max_retry = value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def set_opaque_id
|
76
|
+
return if send(opaque_id_column).present?
|
77
|
+
|
78
|
+
opaque_id_max_retry.times do
|
79
|
+
generated_id = generate_opaque_id
|
80
|
+
send("#{opaque_id_column}=", generated_id)
|
81
|
+
return unless self.class.where(opaque_id_column => generated_id).exists?
|
82
|
+
end
|
83
|
+
raise GenerationError, "Failed to generate a unique opaque_id after #{opaque_id_max_retry} attempts"
|
84
|
+
end
|
85
|
+
|
86
|
+
def generate_opaque_id
|
87
|
+
loop do
|
88
|
+
id = OpaqueId.generate(
|
89
|
+
size: opaque_id_length,
|
90
|
+
alphabet: opaque_id_alphabet
|
91
|
+
)
|
92
|
+
|
93
|
+
# Apply letter start requirement
|
94
|
+
next if opaque_id_require_letter_start && !id.match?(/\A[A-Za-z]/)
|
95
|
+
|
96
|
+
# Apply character purging
|
97
|
+
if opaque_id_purge_chars.any?
|
98
|
+
id = id.chars.reject { |char| opaque_id_purge_chars.include?(char) }.join
|
99
|
+
next if id.length < opaque_id_length
|
100
|
+
end
|
101
|
+
|
102
|
+
return id
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def opaque_id_column
|
107
|
+
self.class.opaque_id_column
|
108
|
+
end
|
109
|
+
|
110
|
+
def opaque_id_length
|
111
|
+
self.class.opaque_id_length
|
112
|
+
end
|
113
|
+
|
114
|
+
def opaque_id_alphabet
|
115
|
+
self.class.opaque_id_alphabet
|
116
|
+
end
|
117
|
+
|
118
|
+
def opaque_id_max_retry
|
119
|
+
self.class.opaque_id_max_retry
|
120
|
+
end
|
121
|
+
|
122
|
+
def opaque_id_require_letter_start
|
123
|
+
self.class.opaque_id_require_letter_start
|
124
|
+
end
|
125
|
+
|
126
|
+
def opaque_id_purge_chars
|
127
|
+
self.class.opaque_id_purge_chars
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/opaque_id.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
require_relative 'opaque_id/version'
|
5
|
+
require_relative 'opaque_id/model'
|
6
|
+
|
7
|
+
module OpaqueId
|
8
|
+
class Error < StandardError; end
|
9
|
+
class GenerationError < Error; end
|
10
|
+
class ConfigurationError < Error; end
|
11
|
+
|
12
|
+
# Standard URL-safe alphabet (64 characters)
|
13
|
+
STANDARD_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
|
14
|
+
|
15
|
+
# Alphanumeric alphabet (62 characters)
|
16
|
+
ALPHANUMERIC_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# Generate a cryptographically secure random ID
|
20
|
+
def generate(size: 21, alphabet: ALPHANUMERIC_ALPHABET)
|
21
|
+
raise ConfigurationError, 'Size must be positive' unless size.positive?
|
22
|
+
raise ConfigurationError, 'Alphabet cannot be empty' if alphabet.nil? || alphabet.empty?
|
23
|
+
|
24
|
+
alphabet_size = alphabet.size
|
25
|
+
|
26
|
+
# Handle edge case: single character alphabet
|
27
|
+
return alphabet * size if alphabet_size == 1
|
28
|
+
|
29
|
+
return generate_fast(size, alphabet) if alphabet_size == 64
|
30
|
+
|
31
|
+
generate_unbiased(size, alphabet, alphabet_size)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def generate_fast(size, alphabet)
|
37
|
+
bytes = SecureRandom.random_bytes(size).bytes
|
38
|
+
size.times.map { |i| alphabet[bytes[i] & 63] }.join
|
39
|
+
end
|
40
|
+
|
41
|
+
def generate_unbiased(size, alphabet, alphabet_size)
|
42
|
+
mask = (2 << Math.log2(alphabet_size - 1).floor) - 1
|
43
|
+
step = (1.6 * mask * size / alphabet_size).ceil
|
44
|
+
id = String.new(capacity: size)
|
45
|
+
|
46
|
+
loop do
|
47
|
+
bytes = SecureRandom.random_bytes(step).bytes
|
48
|
+
bytes.each do |byte|
|
49
|
+
masked_byte = byte & mask
|
50
|
+
if masked_byte < alphabet_size
|
51
|
+
id << alphabet[masked_byte]
|
52
|
+
return id if id.size == size
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
{
|
2
|
+
"release-type": "ruby",
|
3
|
+
"packages": {
|
4
|
+
".": {
|
5
|
+
"package-name": "opaque_id",
|
6
|
+
"version-file": "lib/opaque_id/version.rb"
|
7
|
+
}
|
8
|
+
},
|
9
|
+
"changelog-sections": [
|
10
|
+
{
|
11
|
+
"type": "feat",
|
12
|
+
"section": "Features"
|
13
|
+
},
|
14
|
+
{
|
15
|
+
"type": "fix",
|
16
|
+
"section": "Bug Fixes"
|
17
|
+
},
|
18
|
+
{
|
19
|
+
"type": "perf",
|
20
|
+
"section": "Performance Improvements"
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"type": "revert",
|
24
|
+
"section": "Reverts"
|
25
|
+
},
|
26
|
+
{
|
27
|
+
"type": "docs",
|
28
|
+
"section": "Documentation"
|
29
|
+
},
|
30
|
+
{
|
31
|
+
"type": "style",
|
32
|
+
"section": "Styles"
|
33
|
+
},
|
34
|
+
{
|
35
|
+
"type": "chore",
|
36
|
+
"section": "Miscellaneous Chores"
|
37
|
+
},
|
38
|
+
{
|
39
|
+
"type": "refactor",
|
40
|
+
"section": "Code Refactoring"
|
41
|
+
},
|
42
|
+
{
|
43
|
+
"type": "test",
|
44
|
+
"section": "Tests"
|
45
|
+
},
|
46
|
+
{
|
47
|
+
"type": "build",
|
48
|
+
"section": "Build System"
|
49
|
+
},
|
50
|
+
{
|
51
|
+
"type": "ci",
|
52
|
+
"section": "Continuous Integration"
|
53
|
+
}
|
54
|
+
]
|
55
|
+
}
|
data/sig/opaque_id.rbs
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
# Product Requirements Document: OpaqueId Ruby Gem
|
2
|
+
|
3
|
+
## Introduction/Overview
|
4
|
+
|
5
|
+
The OpaqueId gem is a Ruby library that generates cryptographically secure, collision-free opaque identifiers for ActiveRecord models. This gem replaces the existing `nanoid.rb` dependency by implementing the same functionality using Ruby's built-in `SecureRandom` methods, eliminating external dependencies while maintaining the same security and performance characteristics.
|
6
|
+
|
7
|
+
The primary problem this gem solves is preventing the exposure of incremental database IDs in public URLs and APIs, which can reveal business metrics, enable enumeration attacks, and expose internal system architecture. Instead, it provides opaque, non-sequential identifiers that are URL-friendly and cryptographically secure.
|
8
|
+
|
9
|
+
## Goals
|
10
|
+
|
11
|
+
1. **Replace nanoid.rb dependency** - Implement equivalent functionality using Ruby's built-in SecureRandom
|
12
|
+
2. **Maintain security standards** - Provide cryptographically secure ID generation with unbiased distribution
|
13
|
+
3. **Ensure performance** - Achieve 1M+ IDs/sec for 64-character alphabets, 180K+ IDs/sec for 36-character alphabets
|
14
|
+
4. **Simplify integration** - Provide seamless ActiveRecord integration with minimal configuration
|
15
|
+
5. **Enable wide adoption** - Create comprehensive documentation accessible to all Rails developers
|
16
|
+
6. **Ensure reliability** - Implement robust collision detection and retry logic
|
17
|
+
7. **Maintain compatibility** - Support Rails 8.0+ and Ruby 3.2+ environments
|
18
|
+
|
19
|
+
## User Stories
|
20
|
+
|
21
|
+
### Primary Users: Rails Developers
|
22
|
+
|
23
|
+
**As a Rails developer**, I want to generate opaque IDs for my models so that I can expose public identifiers without revealing database structure.
|
24
|
+
|
25
|
+
**As a Rails developer**, I want to easily integrate opaque ID generation into my existing models so that I don't have to manually implement ID generation logic.
|
26
|
+
|
27
|
+
**As a Rails developer**, I want to configure ID generation parameters (length, alphabet, column name) so that I can customize the behavior for different use cases.
|
28
|
+
|
29
|
+
**As a Rails developer**, I want to find records by their opaque ID so that I can build public-facing APIs and URLs.
|
30
|
+
|
31
|
+
**As a Rails developer**, I want the gem to handle collision detection automatically so that I don't have to worry about duplicate IDs.
|
32
|
+
|
33
|
+
**As a Rails developer**, I want comprehensive documentation and examples so that I can quickly understand and implement the gem in my project.
|
34
|
+
|
35
|
+
### Secondary Users: Security-Conscious Teams
|
36
|
+
|
37
|
+
**As a security-conscious developer**, I want cryptographically secure ID generation so that my public identifiers cannot be predicted or enumerated.
|
38
|
+
|
39
|
+
**As a security-conscious developer**, I want unbiased character distribution so that my IDs have maximum entropy and cannot be analyzed for patterns.
|
40
|
+
|
41
|
+
## Functional Requirements
|
42
|
+
|
43
|
+
### Core ID Generation
|
44
|
+
|
45
|
+
1. The system must generate cryptographically secure random IDs using Ruby's `SecureRandom`
|
46
|
+
2. The system must implement rejection sampling algorithm to ensure unbiased character distribution
|
47
|
+
3. The system must provide optimized fast path for 64-character alphabets using bitwise operations
|
48
|
+
4. The system must support custom alphabet configurations
|
49
|
+
5. The system must support custom ID length configurations
|
50
|
+
6. The system must provide two predefined alphabets: ALPHANUMERIC_ALPHABET (36 chars) and STANDARD_ALPHABET (64 chars)
|
51
|
+
|
52
|
+
### ActiveRecord Integration
|
53
|
+
|
54
|
+
7. The system must provide `OpaqueId::Model` concern for easy ActiveRecord integration
|
55
|
+
8. The system must automatically generate opaque IDs on model creation via `before_create` callback
|
56
|
+
9. The system must provide `find_by_opaque_id` and `find_by_opaque_id!` class methods
|
57
|
+
10. The system must support custom column names via `opaque_id_column` configuration
|
58
|
+
11. The system must implement collision detection with configurable retry attempts
|
59
|
+
12. The system must raise appropriate errors when collision resolution fails
|
60
|
+
|
61
|
+
### Rails Generator (Optional Convenience Tool)
|
62
|
+
|
63
|
+
13. The system must provide optional Rails generator `opaque_id:install` for creating migrations and updating models
|
64
|
+
14. The system must generate migration files that add opaque_id column with unique index
|
65
|
+
15. The system must automatically add `include OpaqueId::Model` to the corresponding model file
|
66
|
+
16. The system must support custom column names via generator options
|
67
|
+
17. The system must require explicit table name argument and show clear usage instructions when run without arguments
|
68
|
+
18. The system must work with any existing table (new or existing models)
|
69
|
+
19. The system must handle edge cases gracefully (missing model file, already included, different class names)
|
70
|
+
|
71
|
+
### Configuration Options
|
72
|
+
|
73
|
+
20. The system must support `opaque_id_column` configuration (default: `:opaque_id`)
|
74
|
+
21. The system must support `opaque_id_length` configuration (default: `18`)
|
75
|
+
22. The system must support `opaque_id_alphabet` configuration (default: `ALPHANUMERIC_ALPHABET`)
|
76
|
+
23. The system must support `opaque_id_require_letter_start` configuration (default: `true`)
|
77
|
+
24. The system must support `opaque_id_purge_chars` configuration (default: `nil`)
|
78
|
+
25. The system must support `opaque_id_max_retry` configuration (default: `1000`)
|
79
|
+
|
80
|
+
### Error Handling
|
81
|
+
|
82
|
+
26. The system must raise `OpaqueId::ConfigurationError` for invalid size or empty alphabet
|
83
|
+
27. The system must raise `OpaqueId::GenerationError` when collision resolution fails
|
84
|
+
28. The system must provide clear error messages for debugging
|
85
|
+
|
86
|
+
### Standalone Usage
|
87
|
+
|
88
|
+
29. The system must provide `OpaqueId.generate` method for standalone ID generation
|
89
|
+
30. The system must support all configuration options in standalone generation
|
90
|
+
31. The system must maintain the same security and performance characteristics in standalone mode
|
91
|
+
|
92
|
+
## Non-Goals (Out of Scope)
|
93
|
+
|
94
|
+
1. **Other ORM Support** - Will not support Mongoid, Sequel, or other ORMs in initial release
|
95
|
+
2. **Non-Rails Usage** - Will not provide standalone Ruby usage without ActiveRecord dependency
|
96
|
+
3. **Custom Algorithms** - Will not implement alternative ID generation algorithms beyond rejection sampling
|
97
|
+
4. **Database Migrations** - Will not provide automatic database migration for existing records
|
98
|
+
5. **ID Validation** - Will not provide built-in ID format validation (users can implement their own)
|
99
|
+
6. **Bulk Generation** - Will not provide optimized bulk ID generation methods
|
100
|
+
7. **ID Prefixes/Suffixes** - Will not support adding prefixes or suffixes to generated IDs
|
101
|
+
8. **Custom Random Sources** - Will not support custom random number generators beyond SecureRandom
|
102
|
+
9. **Interactive Generator Mode** - Will not provide interactive prompts for generator arguments
|
103
|
+
10. **Backward Compatibility** - Will not maintain compatibility with existing `public_id` implementations
|
104
|
+
|
105
|
+
## Design Considerations
|
106
|
+
|
107
|
+
### API Design
|
108
|
+
|
109
|
+
- Follow Rails conventions for ActiveRecord concerns and generators
|
110
|
+
- Use descriptive method names that clearly indicate functionality
|
111
|
+
- Provide both safe (`find_by_opaque_id`) and unsafe (`find_by_opaque_id!`) lookup methods
|
112
|
+
- Use class-level configuration options for easy customization
|
113
|
+
- Follow Devise-style generator pattern for seamless integration
|
114
|
+
|
115
|
+
### Performance Optimization
|
116
|
+
|
117
|
+
- Implement fast path for 64-character alphabets using bitwise operations (`byte & 63`)
|
118
|
+
- Use rejection sampling with optimal mask calculation for unbiased distribution
|
119
|
+
- Pre-allocate string capacity to avoid memory reallocation during generation
|
120
|
+
- Batch random byte generation to minimize SecureRandom calls
|
121
|
+
|
122
|
+
### Security Considerations
|
123
|
+
|
124
|
+
- Use only cryptographically secure random number generation
|
125
|
+
- Implement proper rejection sampling to avoid modulo bias
|
126
|
+
- Provide sufficient entropy through configurable alphabet sizes
|
127
|
+
- Ensure IDs cannot be predicted or enumerated
|
128
|
+
|
129
|
+
## Technical Considerations
|
130
|
+
|
131
|
+
### Dependencies
|
132
|
+
|
133
|
+
- **ActiveRecord**: >= 6.0 (targeting Rails 8.0+ compatibility)
|
134
|
+
- **ActiveSupport**: >= 6.0 (for concern functionality)
|
135
|
+
- **Ruby**: >= 3.2 (for modern Ruby features and performance)
|
136
|
+
|
137
|
+
### Testing Framework
|
138
|
+
|
139
|
+
- Use **Minitest** instead of RSpec for consistency with Rails conventions
|
140
|
+
- Implement comprehensive unit tests for all public methods
|
141
|
+
- Include statistical tests for character distribution uniformity
|
142
|
+
- Add performance benchmarks to ensure performance requirements are met
|
143
|
+
- Test edge cases including collision scenarios and error conditions
|
144
|
+
|
145
|
+
### File Structure
|
146
|
+
|
147
|
+
```
|
148
|
+
lib/
|
149
|
+
├── opaque_id.rb # Main module with generator
|
150
|
+
├── opaque_id/
|
151
|
+
│ ├── version.rb # Version constant
|
152
|
+
│ └── model.rb # ActiveRecord concern
|
153
|
+
└── generators/
|
154
|
+
└── opaque_id/
|
155
|
+
├── install_generator.rb # Migration generator
|
156
|
+
└── templates/
|
157
|
+
└── migration.rb.tt # Migration template
|
158
|
+
```
|
159
|
+
|
160
|
+
### Error Classes
|
161
|
+
|
162
|
+
- `OpaqueId::Error` - Base error class
|
163
|
+
- `OpaqueId::GenerationError` - ID generation failures
|
164
|
+
- `OpaqueId::ConfigurationError` - Invalid configuration
|
165
|
+
|
166
|
+
## Success Metrics
|
167
|
+
|
168
|
+
### Performance Metrics
|
169
|
+
|
170
|
+
- **Standard alphabet (64 chars)**: Achieve ~1.2M IDs/sec generation rate
|
171
|
+
- **Alphanumeric alphabet (36 chars)**: Achieve ~180K IDs/sec generation rate
|
172
|
+
- **Custom alphabet (20 chars)**: Achieve ~150K IDs/sec generation rate
|
173
|
+
|
174
|
+
### Quality Metrics
|
175
|
+
|
176
|
+
- **Test Coverage**: Achieve 95%+ code coverage
|
177
|
+
- **Documentation**: Provide comprehensive README with examples
|
178
|
+
- **Error Handling**: All error conditions properly tested and documented
|
179
|
+
|
180
|
+
### Adoption Metrics
|
181
|
+
|
182
|
+
- **Gem Downloads**: Target 1000+ downloads in first month
|
183
|
+
- **GitHub Stars**: Target 50+ stars within 6 months
|
184
|
+
- **Community Feedback**: Positive reception from Rails community
|
185
|
+
|
186
|
+
## Open Questions
|
187
|
+
|
188
|
+
1. **Version Strategy**: Should we follow semantic versioning strictly, or use a different versioning strategy for a utility gem?
|
189
|
+
|
190
|
+
2. **Backward Compatibility**: How should we handle potential breaking changes in future versions, especially regarding default configurations?
|
191
|
+
|
192
|
+
3. **Performance Testing**: Should we include automated performance regression testing in CI/CD, or rely on manual benchmarking?
|
193
|
+
|
194
|
+
4. **Documentation Hosting**: Should we create a dedicated documentation site, or rely on GitHub README and inline documentation?
|
195
|
+
|
196
|
+
5. **Community Contributions**: What level of community contribution should we expect, and how should we structure the project to encourage contributions?
|
197
|
+
|
198
|
+
6. **Integration Testing**: Should we test against multiple Rails versions in CI, or focus on the target Rails 8.0+ range?
|
199
|
+
|
200
|
+
7. **Security Auditing**: Should we implement any security auditing tools or processes for the random number generation?
|
201
|
+
|
202
|
+
8. **Migration Path**: How should we help users migrate from nanoid.rb to opaque_id, if at all?
|