encoded_id-rails 1.0.0.rc1 → 1.0.0.rc7

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -18
  3. data/LICENSE.txt +1 -1
  4. data/README.md +81 -473
  5. data/context/encoded_id-rails.md +651 -0
  6. data/context/encoded_id.md +437 -0
  7. data/lib/encoded_id/rails/active_record_finders.rb +54 -0
  8. data/lib/encoded_id/rails/annotated_id.rb +14 -9
  9. data/lib/encoded_id/rails/annotated_id_parser.rb +9 -1
  10. data/lib/encoded_id/rails/coder.rb +55 -10
  11. data/lib/encoded_id/rails/composite_id_base.rb +39 -0
  12. data/lib/encoded_id/rails/configuration.rb +66 -10
  13. data/lib/encoded_id/rails/encoder_methods.rb +30 -7
  14. data/lib/encoded_id/rails/finder_methods.rb +11 -0
  15. data/lib/encoded_id/rails/model.rb +60 -7
  16. data/lib/encoded_id/rails/path_param.rb +8 -0
  17. data/lib/encoded_id/rails/persists.rb +55 -9
  18. data/lib/encoded_id/rails/query_methods.rb +21 -4
  19. data/lib/encoded_id/rails/railtie.rb +13 -0
  20. data/lib/encoded_id/rails/salt.rb +8 -0
  21. data/lib/encoded_id/rails/slugged_id.rb +14 -9
  22. data/lib/encoded_id/rails/slugged_id_parser.rb +9 -1
  23. data/lib/encoded_id/rails/slugged_path_param.rb +8 -0
  24. data/lib/encoded_id/rails.rb +11 -6
  25. data/lib/generators/encoded_id/rails/install_generator.rb +36 -2
  26. data/lib/generators/encoded_id/rails/templates/{encoded_id.rb → hashids_encoded_id.rb} +49 -5
  27. data/lib/generators/encoded_id/rails/templates/sqids_encoded_id.rb +116 -0
  28. metadata +16 -24
  29. data/.devcontainer/Dockerfile +0 -17
  30. data/.devcontainer/compose.yml +0 -10
  31. data/.devcontainer/devcontainer.json +0 -12
  32. data/.standard.yml +0 -3
  33. data/Appraisals +0 -9
  34. data/Gemfile +0 -24
  35. data/Rakefile +0 -20
  36. data/Steepfile +0 -4
  37. data/gemfiles/.bundle/config +0 -2
  38. data/gemfiles/rails_7.2.gemfile +0 -19
  39. data/gemfiles/rails_8.0.gemfile +0 -19
  40. data/lib/encoded_id/rails/version.rb +0 -7
  41. data/rbs_collection.yaml +0 -24
  42. data/sig/encoded_id/rails.rbs +0 -141
@@ -0,0 +1,651 @@
1
+ # EncodedId::Rails - Rails Integration Technical Documentation
2
+
3
+ ## Overview
4
+
5
+ `encoded_id-rails` provides seamless Rails integration for the `encoded_id` gem, enabling ActiveRecord models to use obfuscated IDs in URLs while maintaining standard Rails conventions. It offers multiple integration strategies from basic encoding to full ActiveRecord finder method overrides.
6
+
7
+ ## Key Features
8
+
9
+ - **ActiveRecord Integration**: Works seamlessly with Rails models
10
+ - **URL-Friendly IDs**: Automatic `to_param` overrides for encoded IDs in routes
11
+ - **Slugged IDs**: Human-readable slugs combined with encoded IDs (e.g., `john-doe--user_p5w9-z27j`)
12
+ - **Annotated IDs**: Model type prefixes for clarity (e.g., `user_p5w9-z27j`)
13
+ - **Finder Methods**: Find records using encoded IDs with familiar ActiveRecord syntax
14
+ - **Database Persistence**: Optional storage of encoded IDs for performance with automatic validations
15
+ - **Per-Model Configuration**: Different encoding strategies per model with inheritance support
16
+ - **ActiveRecord Finder Overrides**: Seamless integration with `find`, `find_by_id`, etc.
17
+ - **Record Duplication Safety**: Automatic encoded ID cache clearing on `dup`
18
+
19
+ ## Quick Module Reference
20
+
21
+ | Module | Purpose | Key Method |
22
+ |--------|---------|------------|
23
+ | `EncodedId::Rails::Model` | Core functionality | `#encoded_id` |
24
+ | `EncodedId::Rails::PathParam` | URLs use encoded IDs | `#to_param` |
25
+ | `EncodedId::Rails::SluggedPathParam` | URLs use slugged IDs | `#to_param` |
26
+ | `EncodedId::Rails::ActiveRecordFinders` | Transparent finder overrides | `find()` |
27
+ | `EncodedId::Rails::Persists` | Database persistence with validations | `set_normalized_encoded_id!` |
28
+
29
+ ## Installation & Setup
30
+
31
+ ```bash
32
+ # Add to Gemfile
33
+ gem 'encoded_id-rails'
34
+
35
+ # Install
36
+ bundle install
37
+
38
+ # Generate configuration (prompts for encoder choice: sqids or hashids)
39
+ rails generate encoded_id:rails:install
40
+
41
+ # Or specify encoder directly
42
+ rails generate encoded_id:rails:install --encoder=sqids
43
+ # or
44
+ rails generate encoded_id:rails:install --encoder=hashids
45
+ ```
46
+
47
+ The generator creates `config/initializers/encoded_id.rb` with encoder-specific configuration:
48
+ - **Sqids**: No salt required, ready to use
49
+ - **Hashids**: Requires salt configuration (generator includes commented template)
50
+
51
+ ## Core Modules
52
+
53
+ ### EncodedId::Rails::Model
54
+
55
+ The base module that provides core functionality.
56
+
57
+ ```ruby
58
+ class User < ApplicationRecord
59
+ include EncodedId::Rails::Model
60
+ end
61
+ ```
62
+
63
+ #### Instance Methods
64
+
65
+ ##### encoded_id
66
+ Returns the encoded ID with optional annotation prefix.
67
+
68
+ ```ruby
69
+ user = User.find(123)
70
+ user.encoded_id # => "user_p5w9-z27j"
71
+ ```
72
+
73
+ ##### slugged_encoded_id
74
+ Returns encoded ID with human-readable slug.
75
+
76
+ ```ruby
77
+ class User < ApplicationRecord
78
+ include EncodedId::Rails::Model
79
+
80
+ def name_for_encoded_id_slug
81
+ full_name.parameterize
82
+ end
83
+ end
84
+
85
+ user.slugged_encoded_id # => "john-doe--user_p5w9-z27j"
86
+ ```
87
+
88
+ ##### annotation_for_encoded_id
89
+ Override to customize the annotation prefix (defaults to `model_name.underscore`).
90
+
91
+ ```ruby
92
+ def annotation_for_encoded_id
93
+ "usr" # => "usr_p5w9-z27j"
94
+ end
95
+ ```
96
+
97
+ ##### clear_encoded_id_cache!
98
+ Manually clear memoized encoded ID values. Called automatically on `reload` and `dup`.
99
+
100
+ ```ruby
101
+ user.clear_encoded_id_cache!
102
+ ```
103
+
104
+ #### Class Methods
105
+
106
+ ##### find_by_encoded_id(encoded_id)
107
+ Find record by encoded ID (returns nil if not found).
108
+
109
+ ```ruby
110
+ User.find_by_encoded_id("user_p5w9-z27j") # With annotation
111
+ User.find_by_encoded_id("p5w9-z27j") # Just the hash
112
+ User.find_by_encoded_id("john-doe--user_p5w9-z27j") # Slugged
113
+ ```
114
+
115
+ ##### find_by_encoded_id!(encoded_id)
116
+ Same as above but raises `ActiveRecord::RecordNotFound` if not found.
117
+
118
+ ```ruby
119
+ User.find_by_encoded_id!("user_p5w9-z27j")
120
+ # Raises ActiveRecord::RecordNotFound if not found
121
+ ```
122
+
123
+ ##### find_all_by_encoded_id(encoded_id)
124
+ Find multiple records when encoded ID contains multiple IDs (returns nil if none found).
125
+
126
+ ```ruby
127
+ # If encoded ID represents [78, 45]
128
+ User.find_all_by_encoded_id("z2j7-0dmw")
129
+ # => [#<User id: 78>, #<User id: 45>]
130
+ ```
131
+
132
+ ##### find_all_by_encoded_id!(encoded_id)
133
+ Same as above but raises `ActiveRecord::RecordNotFound` if:
134
+ - No records found
135
+ - Number of records doesn't match number of decoded IDs
136
+
137
+ ```ruby
138
+ User.find_all_by_encoded_id!("z2j7-0dmw")
139
+ # Raises if records not found or count mismatch
140
+ ```
141
+
142
+ ##### where_encoded_id(*encoded_ids)
143
+ Returns ActiveRecord relation for chaining. Can take multiple IDs.
144
+
145
+ ```ruby
146
+ User.where_encoded_id("user_p5w9-z27j").where(active: true)
147
+ User.where_encoded_id("id1", "id2", "id3")
148
+ ```
149
+
150
+ ##### encode_encoded_id(id, **options)
151
+ Encode a specific ID using model's configuration (optionally override options).
152
+
153
+ ```ruby
154
+ User.encode_encoded_id(123) # => "p5w9-z27j"
155
+ User.encode_encoded_id(123, id_length: 12) # Override length
156
+ ```
157
+
158
+ ##### decode_encoded_id(encoded_id)
159
+ Decode an encoded ID using model's configuration.
160
+
161
+ ```ruby
162
+ User.decode_encoded_id("user_p5w9-z27j") # => [123]
163
+ ```
164
+
165
+ ### EncodedId::Rails::PathParam
166
+
167
+ Makes models use encoded IDs in URL helpers.
168
+
169
+ ```ruby
170
+ class User < ApplicationRecord
171
+ include EncodedId::Rails::Model
172
+ include EncodedId::Rails::PathParam
173
+ end
174
+
175
+ user.to_param # => "user_p5w9-z27j"
176
+
177
+ # In routes
178
+ link_to "View", user_path(user) # => "/users/user_p5w9-z27j"
179
+ ```
180
+
181
+ ### EncodedId::Rails::SluggedPathParam
182
+
183
+ Uses slugged encoded IDs in URLs.
184
+
185
+ ```ruby
186
+ class User < ApplicationRecord
187
+ include EncodedId::Rails::Model
188
+ include EncodedId::Rails::SluggedPathParam
189
+
190
+ def name_for_encoded_id_slug
191
+ full_name.parameterize
192
+ end
193
+ end
194
+
195
+ user.to_param # => "john-doe--user_p5w9-z27j"
196
+ ```
197
+
198
+ ### EncodedId::Rails::ActiveRecordFinders
199
+
200
+ Overrides standard ActiveRecord finders to handle encoded IDs transparently.
201
+
202
+ **IMPORTANT**: Only use with integer primary keys. Do NOT use with string-based primary keys (UUIDs).
203
+
204
+ ```ruby
205
+ class Product < ApplicationRecord
206
+ include EncodedId::Rails::Model
207
+ include EncodedId::Rails::ActiveRecordFinders
208
+ end
209
+
210
+ # Now these all work with encoded IDs
211
+ Product.find("product_p5w9-z27j")
212
+ Product.find_by_id("product_p5w9-z27j")
213
+ Product.where(id: "product_p5w9-z27j")
214
+
215
+ # Still works with regular IDs
216
+ Product.find(123)
217
+
218
+ # In controllers, no changes needed
219
+ def show
220
+ @product = Product.find(params[:id]) # Works with both
221
+ end
222
+ ```
223
+
224
+ ### EncodedId::Rails::Persists
225
+
226
+ Stores encoded IDs in database for performance with automatic validations.
227
+
228
+ ```bash
229
+ # Generate migration
230
+ rails generate encoded_id:rails:add_columns User
231
+
232
+ # Creates migration adding:
233
+ # - normalized_encoded_id (string) - for lookups without separators/annotations
234
+ # - prefixed_encoded_id (string) - with annotation prefix
235
+ ```
236
+
237
+ ```ruby
238
+ class User < ApplicationRecord
239
+ include EncodedId::Rails::Model
240
+ include EncodedId::Rails::Persists
241
+ end
242
+ ```
243
+
244
+ **Automatic Validations**:
245
+ - Uniqueness validation on `normalized_encoded_id`
246
+ - Uniqueness validation on `prefixed_encoded_id`
247
+ - Read-only enforcement after creation (prevents manual updates)
248
+
249
+ **Callbacks**:
250
+ - `after_create`: Automatically sets encoded IDs
251
+ - `before_save`: Updates encoded IDs if ID changed
252
+ - `after_commit`: Validates persisted values match computed values
253
+
254
+ **Instance Methods**:
255
+
256
+ ##### set_normalized_encoded_id!
257
+ Manually update persisted encoded IDs (uses `update_columns` to bypass callbacks).
258
+
259
+ ```ruby
260
+ user.set_normalized_encoded_id!
261
+ ```
262
+
263
+ ##### update_normalized_encoded_id!
264
+ Update persisted encoded IDs in-memory (will be saved with record).
265
+
266
+ ```ruby
267
+ user.update_normalized_encoded_id!
268
+ user.save
269
+ ```
270
+
271
+ **Database Lookups**:
272
+
273
+ ```ruby
274
+ # Fast lookups via direct DB query
275
+ User.where(normalized_encoded_id: "p5w9z27j").first
276
+ User.where(prefixed_encoded_id: "user_p5w9-z27j").first
277
+
278
+ # IMPORTANT: Add indexes for performance
279
+ # See migration section below
280
+ ```
281
+
282
+ **Record Duplication**:
283
+ When using `dup`, persisted encoded ID columns are automatically set to `nil` for the new record:
284
+
285
+ ```ruby
286
+ new_user = existing_user.dup
287
+ new_user.normalized_encoded_id # => nil (will be set on save)
288
+ new_user.prefixed_encoded_id # => nil
289
+ ```
290
+
291
+ ## Configuration
292
+
293
+ ### Global Configuration
294
+
295
+ ```ruby
296
+ # config/initializers/encoded_id.rb
297
+ EncodedId::Rails.configure do |config|
298
+ # Required for Hashids encoder
299
+ config.salt = "your-secret-salt" # Not required for Sqids
300
+
301
+ # Basic Options
302
+ config.id_length = 8 # Minimum length
303
+ config.character_group_size = 4 # Split every X chars
304
+ config.group_separator = "-" # Split character
305
+ config.alphabet = EncodedId::Alphabet.modified_crockford
306
+
307
+ # Annotation/Prefix Options
308
+ config.annotation_method_name = :annotation_for_encoded_id
309
+ config.annotated_id_separator = "_"
310
+
311
+ # Slug Options
312
+ config.slug_value_method_name = :name_for_encoded_id_slug
313
+ config.slugged_id_separator = "--"
314
+
315
+ # Encoder Selection
316
+ config.encoder = :sqids # Default: :sqids (or :hashids for backwards compatibility)
317
+ config.downcase_on_decode = false # Default: false (set to true for pre-v1 compatibility)
318
+
319
+ # Blocklist
320
+ config.blocklist = nil # EncodedId::Blocklist.minimal or custom
321
+
322
+ # Auto-include PathParam (makes all models use encoded IDs in URLs)
323
+ config.model_to_param_returns_encoded_id = false
324
+ end
325
+ ```
326
+
327
+ **Note**: As of v1.0.0:
328
+ - Default encoder is `:sqids` (no salt required)
329
+ - `downcase_on_decode` defaults to `false` (case-sensitive)
330
+ - For backwards compatibility with pre-v1: set `encoder: :hashids` and `downcase_on_decode: true`
331
+
332
+ ### Encoder-Specific Notes
333
+
334
+ **Sqids (default)**:
335
+ - No salt required
336
+ - Automatically avoids blocklisted words via iteration
337
+ - Faster decoding
338
+
339
+ **Hashids**:
340
+ - Requires salt (minimum 4 characters)
341
+ - Raises `EncodedId::BlocklistError` if blocklisted word appears
342
+ - Faster encoding, especially with blocklists
343
+
344
+ ### Per-Model Configuration
345
+
346
+ #### Using `encoded_id_config` (Recommended)
347
+
348
+ The cleanest way to configure encoding options per model:
349
+
350
+ ```ruby
351
+ class User < ApplicationRecord
352
+ include EncodedId::Rails::Model
353
+
354
+ # Configure encoder settings for this model
355
+ encoded_id_config encoder: :hashids, id_length: 12
356
+ end
357
+ ```
358
+
359
+ **Supports all configuration options**:
360
+
361
+ ```ruby
362
+ class Product < ApplicationRecord
363
+ include EncodedId::Rails::Model
364
+
365
+ encoded_id_config(
366
+ encoder: :sqids,
367
+ id_length: 12,
368
+ character_group_size: 3,
369
+ alphabet: EncodedId::Alphabet.new("0123456789ABCDEF"),
370
+ blocklist: EncodedId::Blocklist.minimal,
371
+ downcase_on_decode: true
372
+ )
373
+ end
374
+ ```
375
+
376
+ **Available Options**:
377
+ - `encoder` - `:sqids` or `:hashids`
378
+ - `id_length` - Minimum encoded ID length
379
+ - `character_group_size` - Character grouping (nil for no grouping)
380
+ - `alphabet` - Custom alphabet
381
+ - `blocklist` - Blocklist instance or array
382
+ - `downcase_on_decode` - Case-insensitive decoding
383
+ - `annotation_method_name` - Method to call for annotation
384
+ - `annotated_id_separator` - Separator for annotated IDs
385
+ - `slug_value_method_name` - Method to call for slug
386
+ - `slugged_id_separator` - Separator for slugged IDs
387
+
388
+ **Note**: For advanced blocklist control (modes like `:length_threshold`, `:always`, `:raise_if_likely`), use the base `EncodedId::ReversibleId` directly or configure via custom coder.
389
+
390
+ **Configuration Inheritance**: Child classes inherit their parent's configuration:
391
+
392
+ ```ruby
393
+ class BaseModel < ApplicationRecord
394
+ self.abstract_class = true
395
+ include EncodedId::Rails::Model
396
+
397
+ # All child models inherit these settings
398
+ encoded_id_config encoder: :hashids, id_length: 10
399
+ end
400
+
401
+ class User < BaseModel
402
+ # Inherits encoder: :hashids, id_length: 10
403
+ end
404
+
405
+ class Product < BaseModel
406
+ # Override parent settings
407
+ encoded_id_config encoder: :sqids
408
+ # Now uses encoder: :sqids but still id_length: 10
409
+ end
410
+ ```
411
+
412
+ #### Custom Salt Per Model
413
+
414
+ Override salt per model (Hashids only):
415
+
416
+ ```ruby
417
+ class User < ApplicationRecord
418
+ include EncodedId::Rails::Model
419
+
420
+ def self.encoded_id_salt
421
+ "user-specific-salt"
422
+ end
423
+ end
424
+ ```
425
+
426
+ ### Contextual Encoding
427
+
428
+ Different configurations for different use cases:
429
+
430
+ ```ruby
431
+ class User < ApplicationRecord
432
+ include EncodedId::Rails::Model
433
+
434
+ # Short ID for QR codes
435
+ def qr_encoded_id
436
+ self.class.encode_encoded_id(id,
437
+ id_length: 6,
438
+ character_group_size: nil
439
+ )
440
+ end
441
+
442
+ # API-friendly (no separators/annotations)
443
+ def api_encoded_id
444
+ self.class.encode_encoded_id(id,
445
+ character_group_size: nil,
446
+ annotation_method_name: nil
447
+ )
448
+ end
449
+ end
450
+ ```
451
+
452
+ ## Routes & Controllers
453
+
454
+ ### Basic Setup
455
+
456
+ ```ruby
457
+ # routes.rb
458
+ Rails.application.routes.draw do
459
+ resources :users, param: :encoded_id
460
+ end
461
+
462
+ # UsersController
463
+ class UsersController < ApplicationController
464
+ def show
465
+ @user = User.find_by_encoded_id!(params[:encoded_id])
466
+ end
467
+ end
468
+ ```
469
+
470
+ ### With ActiveRecordFinders
471
+
472
+ ```ruby
473
+ # routes.rb - standard :id param
474
+ resources :products
475
+
476
+ # ProductsController
477
+ class ProductsController < ApplicationController
478
+ def show
479
+ # Works with both regular and encoded IDs
480
+ @product = Product.find(params[:id])
481
+ end
482
+ end
483
+ ```
484
+
485
+ ### With Slugged IDs
486
+
487
+ ```ruby
488
+ # routes.rb
489
+ resources :articles
490
+
491
+ # ArticlesController
492
+ class ArticlesController < ApplicationController
493
+ def show
494
+ # Handles slugged IDs automatically
495
+ @article = Article.find_by_encoded_id!(params[:id])
496
+ end
497
+ end
498
+ ```
499
+
500
+ ## Common Patterns
501
+
502
+ ### Complete Integration Example
503
+
504
+ ```ruby
505
+ class Product < ApplicationRecord
506
+ include EncodedId::Rails::Model
507
+ include EncodedId::Rails::SluggedPathParam
508
+ include EncodedId::Rails::Persists
509
+ include EncodedId::Rails::ActiveRecordFinders
510
+
511
+ # Configure encoding options for this model
512
+ encoded_id_config(
513
+ blocklist: EncodedId::Blocklist.minimal,
514
+ id_length: 10
515
+ )
516
+
517
+ def name_for_encoded_id_slug
518
+ name.parameterize
519
+ end
520
+ end
521
+
522
+ # Usage
523
+ product = Product.create(name: "Cool Gadget")
524
+ product.encoded_id # => "product_k6jR8Myo23"
525
+ product.slugged_encoded_id # => "cool-gadget--product_k6jR8Myo23"
526
+
527
+ # All these work
528
+ Product.find("product_k6jR8Myo23")
529
+ Product.find("cool-gadget--product_k6jR8Myo23")
530
+ Product.find_by_encoded_id("k6jR8Myo23")
531
+ Product.where_encoded_id("product_k6jR8Myo23").active
532
+
533
+ # URLs automatically use slugged IDs
534
+ product_path(product) # => "/products/cool-gadget--product_k6jR8Myo23"
535
+ ```
536
+
537
+ ### Migration for Existing Data
538
+
539
+ When adding `Persists` module to existing models:
540
+
541
+ ```ruby
542
+ # 1. Generate and run migration
543
+ rails generate encoded_id:rails:add_columns User
544
+ # Edit migration to add indexes (see below)
545
+ rails db:migrate
546
+
547
+ # 2. Backfill existing records
548
+ User.find_each(batch_size: 1000) do |user|
549
+ user.set_normalized_encoded_id!
550
+ end
551
+
552
+ # Or via background job
553
+ class BackfillEncodedIdsJob < ApplicationJob
554
+ def perform(model_class, start_id, end_id)
555
+ model_class.where(id: start_id..end_id).find_each do |record|
556
+ record.set_normalized_encoded_id!
557
+ end
558
+ end
559
+ end
560
+ ```
561
+
562
+ **Enhanced Migration with Indexes** (recommended):
563
+
564
+ ```ruby
565
+ class AddEncodedIdColumnsToUsers < ActiveRecord::Migration[7.0]
566
+ def change
567
+ add_column :users, :normalized_encoded_id, :string
568
+ add_column :users, :prefixed_encoded_id, :string
569
+
570
+ # Add indexes for performance (critical for lookups)
571
+ add_index :users, :normalized_encoded_id, unique: true
572
+ add_index :users, :prefixed_encoded_id, unique: true
573
+ end
574
+ end
575
+ ```
576
+
577
+ ### Accessing Configuration
578
+
579
+ ```ruby
580
+ # Access global configuration
581
+ EncodedId::Rails.configuration.salt
582
+ EncodedId::Rails.configuration.encoder # => :sqids
583
+ EncodedId::Rails.configuration.id_length # => 8
584
+
585
+ # Access model-specific configuration
586
+ User.encoded_id_salt
587
+ User.encoded_id_coder.encoder
588
+ User.encoded_id_options # => Hash of configured options
589
+ ```
590
+
591
+ ## Performance Considerations
592
+
593
+ 1. **Persistence**: Use `EncodedId::Rails::Persists` for high-traffic lookups
594
+ 2. **Indexes**: ALWAYS add database indexes on `normalized_encoded_id` and `prefixed_encoded_id`
595
+ 3. **Caching**: Encoded IDs are deterministic and memoized per record - cache them if needed
596
+ 4. **Blocklists**: Large blocklists impact encoding performance, especially with Sqids (iterates to avoid words)
597
+ 5. **Blocklist Modes**:
598
+ - Sqids: Iteratively regenerates to avoid blocklisted words (may impact performance)
599
+ - Hashids: Raises exception if blocklisted word detected (requires retry logic)
600
+ - For advanced control, use base `EncodedId` configuration directly
601
+
602
+ ## Best Practices
603
+
604
+ 1. **Consistent Configuration**: Don't change salt/encoder after going to production
605
+ 2. **Model Naming**: Use clear annotation prefixes to identify model types
606
+ 3. **Error Handling**: Always use `find_by_encoded_id!` in controllers for proper 404s
607
+ 4. **URL Design**: Choose between encoded IDs vs slugged IDs based on UX needs
608
+ 5. **Testing**: Test with both regular IDs and encoded IDs in your specs
609
+ 6. **Indexes**: Always add database indexes when using `Persists` module
610
+ 7. **Validations**: Rely on automatic validations from `Persists` - don't manually update columns
611
+ 8. **Record Duplication**: `dup` automatically clears encoded IDs - persisted IDs set to nil for new records
612
+
613
+ ## Debugging
614
+
615
+ ```ruby
616
+ # Check configuration
617
+ user = User.first
618
+ user.class.encoded_id_salt
619
+ user.class.encoded_id_coder.encoder
620
+ user.class.encoded_id_options
621
+
622
+ # Test encoding/decoding
623
+ encoded = User.encode_encoded_id(123)
624
+ decoded = User.decode_encoded_id(encoded)
625
+
626
+ # Inspect persisted values (if using Persists)
627
+ user.normalized_encoded_id
628
+ user.prefixed_encoded_id
629
+
630
+ # Clear and regenerate encoded IDs
631
+ user.clear_encoded_id_cache!
632
+ user.encoded_id # Regenerates
633
+ ```
634
+
635
+ ## Security Considerations
636
+
637
+ - Encoded IDs are obfuscated, NOT encrypted
638
+ - Don't rely on them for authentication or authorization
639
+ - They help prevent enumeration attacks but aren't cryptographically secure
640
+ - Always validate decoded IDs before database operations
641
+ - Use `find_by_encoded_id!` to ensure proper error handling
642
+
643
+ ## Example Use Cases
644
+
645
+ 1. **Public-Facing IDs**: Hide sequential database IDs from users
646
+ 2. **SEO-Friendly URLs**: Combine slugs with encoded IDs (`cool-gadget--product_k6j8`)
647
+ 3. **API Design**: Provide opaque identifiers that don't leak information
648
+ 4. **Multi-Tenant Apps**: Use different salts per tenant for isolation
649
+ 5. **Legacy Migration**: Gradually move from numeric to encoded IDs
650
+ 6. **Referral Codes**: Encode user IDs into shareable links
651
+ 7. **Soft Launches**: Hide actual user/item counts from competitors