opaque_id 1.4.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,191 +0,0 @@
1
- # Product Requirements Document: OpaqueId Documentation Site
2
-
3
- ## Introduction/Overview
4
-
5
- Create a professional documentation website for the OpaqueId Ruby gem using [Just the Docs](https://just-the-docs.com) Jekyll theme. The site will provide comprehensive, developer-focused documentation that mirrors the README content but with improved navigation, search capabilities, and a clean, minimal design similar to the Just the Docs site itself.
6
-
7
- **Problem Solved:** The current README is comprehensive but long (1600+ lines), making it difficult to navigate and find specific information quickly. A dedicated documentation site will improve developer experience and make the gem more accessible.
8
-
9
- **Goal:** Create a professional, searchable documentation site that makes OpaqueId easy to understand and implement for developers.
10
-
11
- ## Goals
12
-
13
- 1. **Improve Developer Experience**: Provide easy navigation and search for finding specific documentation
14
- 2. **Professional Presentation**: Create a polished, modern documentation site that reflects the quality of the gem
15
- 3. **Maintain Content Quality**: Preserve all valuable information from the README while improving organization
16
- 4. **Enable Auto-Deployment**: Set up GitHub Actions for automatic deployment on successful pushes
17
- 5. **Ensure Accessibility**: Make documentation accessible and mobile-friendly
18
- 6. **Minimize Maintenance**: Keep the site simple and maintainable
19
-
20
- ## User Stories
21
-
22
- - **As a developer** evaluating OpaqueId, I want to quickly understand what the gem does and how to install it
23
- - **As a developer** implementing OpaqueId, I want to find specific configuration options and usage examples
24
- - **As a developer** troubleshooting issues, I want to search for specific error messages or solutions
25
- - **As a developer** learning advanced features, I want to explore performance benchmarks and security considerations
26
- - **As a developer** contributing to the project, I want to understand the development setup and guidelines
27
-
28
- ## Functional Requirements
29
-
30
- ### 1. Site Structure & Navigation
31
-
32
- 1.1. Create a `/docs` directory with Jekyll site structure
33
- 1.2. Implement sidebar navigation based on README Table of Contents
34
- 1.3. Use minimal, flat navigation structure (no collapsible sections)
35
- 1.4. Include search functionality using Just the Docs built-in search
36
- 1.5. Add "Getting Started" quick start guide as a separate page
37
-
38
- ### 2. Content Organization
39
-
40
- 2.1. Convert README sections into individual documentation pages
41
- 2.2. Create logical page hierarchy: Home → Getting Started → Installation → Usage → Configuration → etc.
42
- 2.3. Maintain all existing content from README
43
- 2.4. Add code syntax highlighting for Ruby examples
44
- 2.5. Include proper cross-references between pages
45
-
46
- ### 3. Design & Styling
47
-
48
- 3.1. Use Just the Docs theme with default light/dark mode switching
49
- 3.2. Apply minimal design similar to just-the-docs.com
50
- 3.3. Include OpaqueId branding and gem badges
51
- 3.4. Ensure responsive design for mobile devices
52
- 3.5. Use consistent typography and spacing
53
-
54
- ### 4. GitHub Pages Integration
55
-
56
- 4.1. Configure site for `https://nyaggah.github.io/opaque_id`
57
- 4.2. Set up GitHub Actions workflow for automatic deployment
58
- 4.3. Deploy on successful pushes to main branch
59
- 4.4. Include proper Jekyll configuration for GitHub Pages
60
-
61
- ### 5. Content Pages
62
-
63
- 5.1. **Home**: Overview, badges, quick introduction
64
- 5.2. **Getting Started**: Step-by-step installation and basic usage
65
- 5.3. **Installation**: Detailed installation instructions and requirements
66
- 5.4. **Usage**: Standalone and ActiveRecord integration examples
67
- 5.5. **Configuration**: All configuration options and examples
68
- 5.6. **Alphabets**: Built-in alphabets and custom alphabet guide
69
- 5.7. **Algorithms**: Technical details about fast path and unbiased algorithms
70
- 5.8. **Performance**: Benchmarks and optimization tips
71
- 5.9. **Security**: Security considerations and best practices
72
- 5.10. **Use Cases**: Real-world examples and applications
73
- 5.11. **Development**: Contributing guidelines and development setup
74
- 5.12. **API Reference**: Complete method documentation
75
-
76
- ### 6. Technical Implementation
77
-
78
- 6.1. Use Jekyll with Just the Docs theme
79
- 6.2. Configure `_config.yml` for GitHub Pages
80
- 6.3. Set up proper front matter for all pages
81
- 6.4. Include necessary Jekyll plugins for GitHub Pages
82
- 6.5. Optimize for fast loading and SEO
83
-
84
- ## Non-Goals (Out of Scope)
85
-
86
- - **Live Code Examples**: No interactive demos or live code execution
87
- - **Version-Specific Documentation**: Single version documentation only
88
- - **Automatic README Sync**: Manual content maintenance
89
- - **Custom Domain**: Using default GitHub.io domain
90
- - **Breadcrumb Navigation**: Sidebar navigation only
91
- - **User Authentication**: Public documentation only
92
- - **Comments/Feedback System**: Static documentation only
93
-
94
- ## Design Considerations
95
-
96
- ### Visual Design
97
-
98
- - **Theme**: [Just the Docs](https://just-the-docs.com) Jekyll theme
99
- - **Color Scheme**: Default light/dark mode switching
100
- - **Layout**: Clean, minimal design with focus on content
101
- - **Typography**: Clear, readable fonts with proper hierarchy
102
- - **Navigation**: Left sidebar with flat structure
103
-
104
- ### Content Structure
105
-
106
- - **Homepage**: Hero section with badges, overview, and quick start
107
- - **Sidebar**: Logical grouping of documentation sections
108
- - **Search**: Full-text search across all documentation
109
- - **Code Examples**: Syntax-highlighted Ruby code blocks
110
- - **Cross-References**: Links between related sections
111
-
112
- ## Technical Considerations
113
-
114
- ### Jekyll Configuration
115
-
116
- - Use gem-based approach with `just-the-docs` gem
117
- - Configure for GitHub Pages compatibility
118
- - Include necessary plugins: jekyll-feed, jekyll-sitemap, jekyll-seo-tag
119
- - Set up proper permalinks and URL structure
120
-
121
- ### GitHub Actions Workflow
122
-
123
- - Trigger on pushes to main branch
124
- - Build Jekyll site
125
- - Deploy to GitHub Pages
126
- - Include proper error handling and notifications
127
-
128
- ### File Structure
129
-
130
- ```
131
- /docs/
132
- ├── _config.yml
133
- ├── Gemfile
134
- ├── index.md (Home)
135
- ├── getting-started.md
136
- ├── installation.md
137
- ├── usage.md
138
- ├── configuration.md
139
- ├── alphabets.md
140
- ├── algorithms.md
141
- ├── performance.md
142
- ├── security.md
143
- ├── use-cases.md
144
- ├── development.md
145
- ├── api-reference.md
146
- └── assets/
147
- └── images/
148
- ```
149
-
150
- ## Success Metrics
151
-
152
- 1. **Developer Adoption**: Increased gem downloads and usage
153
- 2. **User Engagement**: Time spent on documentation pages
154
- 3. **Search Usage**: Frequency of search functionality usage
155
- 4. **Page Performance**: Fast loading times (< 3 seconds)
156
- 5. **Mobile Usage**: Successful mobile documentation access
157
- 6. **SEO Performance**: High search engine rankings for OpaqueId-related queries
158
-
159
- ## Open Questions
160
-
161
- 1. Should we include a changelog page or link to GitHub releases?
162
- 2. Do we want to include a "Troubleshooting" section for common issues?
163
- 3. Should we add a "Migration Guide" for users switching from nanoid.rb?
164
- 4. Do we want to include performance comparison charts or keep them as tables?
165
- 5. Should we add a "Community" section with links to discussions and support?
166
-
167
- ## Implementation Priority
168
-
169
- ### Phase 1: Core Setup
170
-
171
- - Set up Jekyll site structure
172
- - Configure GitHub Pages deployment
173
- - Create basic page structure
174
-
175
- ### Phase 2: Content Migration
176
-
177
- - Convert README content to individual pages
178
- - Set up navigation and cross-references
179
- - Add code syntax highlighting
180
-
181
- ### Phase 3: Enhancement
182
-
183
- - Add search functionality
184
- - Optimize performance and SEO
185
- - Polish design and user experience
186
-
187
- ### Phase 4: Launch
188
-
189
- - Deploy to GitHub Pages
190
- - Test all functionality
191
- - Announce to community
@@ -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
- ```