encoded_id-rails 1.0.0.rc1 → 1.0.0.rc6
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 +4 -4
- data/CHANGELOG.md +77 -18
- data/LICENSE.txt +1 -1
- data/README.md +76 -479
- data/context/encoded_id-rails.md +433 -0
- data/context/encoded_id.md +283 -0
- data/lib/encoded_id/rails/active_record_finders.rb +52 -0
- data/lib/encoded_id/rails/annotated_id.rb +8 -0
- data/lib/encoded_id/rails/annotated_id_parser.rb +8 -1
- data/lib/encoded_id/rails/coder.rb +20 -2
- data/lib/encoded_id/rails/configuration.rb +44 -4
- data/lib/encoded_id/rails/encoder_methods.rb +9 -1
- data/lib/encoded_id/rails/finder_methods.rb +10 -0
- data/lib/encoded_id/rails/model.rb +25 -2
- data/lib/encoded_id/rails/path_param.rb +7 -0
- data/lib/encoded_id/rails/persists.rb +52 -8
- data/lib/encoded_id/rails/query_methods.rb +20 -4
- data/lib/encoded_id/rails/railtie.rb +13 -0
- data/lib/encoded_id/rails/salt.rb +7 -0
- data/lib/encoded_id/rails/slugged_id.rb +8 -0
- data/lib/encoded_id/rails/slugged_id_parser.rb +8 -1
- data/lib/encoded_id/rails/slugged_path_param.rb +7 -0
- data/lib/encoded_id/rails.rb +9 -6
- data/lib/generators/encoded_id/rails/templates/encoded_id.rb +22 -2
- metadata +13 -23
- data/.devcontainer/Dockerfile +0 -17
- data/.devcontainer/compose.yml +0 -10
- data/.devcontainer/devcontainer.json +0 -12
- data/.standard.yml +0 -3
- data/Appraisals +0 -9
- data/Gemfile +0 -24
- data/Rakefile +0 -20
- data/Steepfile +0 -4
- data/gemfiles/.bundle/config +0 -2
- data/gemfiles/rails_7.2.gemfile +0 -19
- data/gemfiles/rails_8.0.gemfile +0 -19
- data/lib/encoded_id/rails/version.rb +0 -7
- data/rbs_collection.yaml +0 -24
- data/sig/encoded_id/rails.rbs +0 -141
|
@@ -0,0 +1,433 @@
|
|
|
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
|
|
15
|
+
- **Per-Model Configuration**: Different encoding strategies per model
|
|
16
|
+
- **ActiveRecord Finder Overrides**: Seamless integration with `find`, `find_by_id`, etc.
|
|
17
|
+
|
|
18
|
+
## Installation & Setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Add to Gemfile
|
|
22
|
+
gem 'encoded_id-rails'
|
|
23
|
+
|
|
24
|
+
# Install
|
|
25
|
+
bundle install
|
|
26
|
+
|
|
27
|
+
# Generate configuration
|
|
28
|
+
rails generate encoded_id:rails:install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This creates `config/initializers/encoded_id.rb` with configuration options.
|
|
32
|
+
|
|
33
|
+
## Core Modules
|
|
34
|
+
|
|
35
|
+
### EncodedId::Rails::Model
|
|
36
|
+
|
|
37
|
+
The base module that provides core functionality.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
class User < ApplicationRecord
|
|
41
|
+
include EncodedId::Rails::Model
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### Instance Methods
|
|
46
|
+
|
|
47
|
+
##### encoded_id
|
|
48
|
+
Returns the encoded ID with optional annotation prefix.
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
user = User.find(123)
|
|
52
|
+
user.encoded_id # => "user_p5w9-z27j"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
##### slugged_encoded_id
|
|
56
|
+
Returns encoded ID with human-readable slug.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class User < ApplicationRecord
|
|
60
|
+
include EncodedId::Rails::Model
|
|
61
|
+
|
|
62
|
+
def name_for_encoded_id_slug
|
|
63
|
+
full_name.parameterize
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
user.slugged_encoded_id # => "john-doe--user_p5w9-z27j"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
##### annotation_for_encoded_id
|
|
71
|
+
Override to customize the annotation prefix.
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
def annotation_for_encoded_id
|
|
75
|
+
"usr" # => "usr_p5w9-z27j"
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### Class Methods
|
|
80
|
+
|
|
81
|
+
##### find_by_encoded_id(encoded_id)
|
|
82
|
+
Find record by encoded ID (returns nil if not found).
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
User.find_by_encoded_id("user_p5w9-z27j") # With annotation
|
|
86
|
+
User.find_by_encoded_id("p5w9-z27j") # Just the hash
|
|
87
|
+
User.find_by_encoded_id("john-doe--user_p5w9-z27j") # Slugged
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
##### find_by_encoded_id!(encoded_id)
|
|
91
|
+
Same as above but raises `ActiveRecord::RecordNotFound` if not found.
|
|
92
|
+
|
|
93
|
+
##### find_all_by_encoded_id(encoded_id)
|
|
94
|
+
Find multiple records when encoded ID contains multiple IDs.
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# If encoded ID represents [78, 45]
|
|
98
|
+
User.find_all_by_encoded_id("z2j7-0dmw")
|
|
99
|
+
# => [#<User id: 78>, #<User id: 45>]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
##### where_encoded_id(encoded_id)
|
|
103
|
+
Returns ActiveRecord relation for chaining.
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
User.where_encoded_id("user_p5w9-z27j").where(active: true)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
##### encode_encoded_id(id)
|
|
110
|
+
Encode a specific ID using model's configuration.
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
User.encode_encoded_id(123) # => "p5w9-z27j"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### EncodedId::Rails::PathParam
|
|
117
|
+
|
|
118
|
+
Makes models use encoded IDs in URL helpers.
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class User < ApplicationRecord
|
|
122
|
+
include EncodedId::Rails::Model
|
|
123
|
+
include EncodedId::Rails::PathParam
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
user.to_param # => "user_p5w9-z27j"
|
|
127
|
+
|
|
128
|
+
# In routes
|
|
129
|
+
link_to "View", user_path(user) # => "/users/user_p5w9-z27j"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### EncodedId::Rails::SluggedPathParam
|
|
133
|
+
|
|
134
|
+
Uses slugged encoded IDs in URLs.
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
class User < ApplicationRecord
|
|
138
|
+
include EncodedId::Rails::Model
|
|
139
|
+
include EncodedId::Rails::SluggedPathParam
|
|
140
|
+
|
|
141
|
+
def name_for_encoded_id_slug
|
|
142
|
+
full_name.parameterize
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
user.to_param # => "john-doe--user_p5w9-z27j"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### EncodedId::Rails::ActiveRecordFinders
|
|
150
|
+
|
|
151
|
+
Overrides standard ActiveRecord finders to handle encoded IDs transparently.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
class Product < ApplicationRecord
|
|
155
|
+
include EncodedId::Rails::Model
|
|
156
|
+
include EncodedId::Rails::ActiveRecordFinders
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Now these all work with encoded IDs
|
|
160
|
+
Product.find("product_p5w9-z27j")
|
|
161
|
+
Product.find_by_id("product_p5w9-z27j")
|
|
162
|
+
Product.where(id: "product_p5w9-z27j")
|
|
163
|
+
|
|
164
|
+
# Still works with regular IDs
|
|
165
|
+
Product.find(123)
|
|
166
|
+
|
|
167
|
+
# In controllers, no changes needed
|
|
168
|
+
def show
|
|
169
|
+
@product = Product.find(params[:id]) # Works with both
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Warning**: Do NOT use with string-based primary keys (UUIDs).
|
|
174
|
+
|
|
175
|
+
### EncodedId::Rails::Persists
|
|
176
|
+
|
|
177
|
+
Stores encoded IDs in database for performance.
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Generate migration
|
|
181
|
+
rails generate encoded_id:rails:add_columns User
|
|
182
|
+
|
|
183
|
+
# Adds columns:
|
|
184
|
+
# - normalized_encoded_id (string)
|
|
185
|
+
# - prefixed_encoded_id (string)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
class User < ApplicationRecord
|
|
190
|
+
include EncodedId::Rails::Model
|
|
191
|
+
include EncodedId::Rails::Persists
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Fast lookups via direct DB query
|
|
195
|
+
User.where(normalized_encoded_id: "p5w9z27j").first
|
|
196
|
+
|
|
197
|
+
# Add index for performance
|
|
198
|
+
add_index :users, :normalized_encoded_id, unique: true
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Configuration
|
|
202
|
+
|
|
203
|
+
### Global Configuration
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
# config/initializers/encoded_id.rb
|
|
207
|
+
EncodedId::Rails.configure do |config|
|
|
208
|
+
# Required
|
|
209
|
+
config.salt = "your-secret-salt"
|
|
210
|
+
|
|
211
|
+
# Optional
|
|
212
|
+
config.id_length = 8 # Minimum length
|
|
213
|
+
config.character_group_size = 4 # Split every X chars
|
|
214
|
+
config.group_separator = "-" # Split character
|
|
215
|
+
config.alphabet = EncodedId::Alphabet.modified_crockford
|
|
216
|
+
config.annotation_method_name = :annotation_for_encoded_id
|
|
217
|
+
config.annotated_id_separator = "_"
|
|
218
|
+
config.slug_value_method_name = :name_for_encoded_id_slug
|
|
219
|
+
config.slugged_id_separator = "--"
|
|
220
|
+
config.model_to_param_returns_encoded_id = false
|
|
221
|
+
config.encoder = :hashids # or :sqids
|
|
222
|
+
config.blocklist = nil
|
|
223
|
+
config.hex_digit_encoding_group_size = 4
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Per-Model Configuration
|
|
228
|
+
|
|
229
|
+
Override salt per model:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
class User < ApplicationRecord
|
|
233
|
+
include EncodedId::Rails::Model
|
|
234
|
+
|
|
235
|
+
def self.encoded_id_salt
|
|
236
|
+
"user-specific-salt"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Advanced Model Configuration
|
|
242
|
+
|
|
243
|
+
Full control via `encoded_id_coder`:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
class Product < ApplicationRecord
|
|
247
|
+
include EncodedId::Rails::Model
|
|
248
|
+
|
|
249
|
+
def self.encoded_id_coder(options = {})
|
|
250
|
+
super(options.merge(
|
|
251
|
+
encoder: :sqids,
|
|
252
|
+
id_length: 12,
|
|
253
|
+
character_group_size: 3,
|
|
254
|
+
group_separator: ".",
|
|
255
|
+
alphabet: EncodedId::Alphabet.new("0123456789ABCDEF"),
|
|
256
|
+
blocklist: ["BAD", "FAKE"]
|
|
257
|
+
))
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Contextual Encoding
|
|
263
|
+
|
|
264
|
+
Different configurations for different use cases:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
class User < ApplicationRecord
|
|
268
|
+
include EncodedId::Rails::Model
|
|
269
|
+
|
|
270
|
+
# Short ID for QR codes
|
|
271
|
+
def qr_encoded_id
|
|
272
|
+
self.class.encode_encoded_id(id,
|
|
273
|
+
id_length: 6,
|
|
274
|
+
character_group_size: nil
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# API-friendly (no separators/annotations)
|
|
279
|
+
def api_encoded_id
|
|
280
|
+
self.class.encode_encoded_id(id,
|
|
281
|
+
character_group_size: nil,
|
|
282
|
+
annotation_method_name: nil
|
|
283
|
+
)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Routes & Controllers
|
|
289
|
+
|
|
290
|
+
### Basic Setup
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# routes.rb
|
|
294
|
+
Rails.application.routes.draw do
|
|
295
|
+
resources :users, param: :encoded_id
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# UsersController
|
|
299
|
+
class UsersController < ApplicationController
|
|
300
|
+
def show
|
|
301
|
+
@user = User.find_by_encoded_id!(params[:encoded_id])
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### With ActiveRecordFinders
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
# routes.rb - standard :id param
|
|
310
|
+
resources :products
|
|
311
|
+
|
|
312
|
+
# ProductsController
|
|
313
|
+
class ProductsController < ApplicationController
|
|
314
|
+
def show
|
|
315
|
+
# Works with both regular and encoded IDs
|
|
316
|
+
@product = Product.find(params[:id])
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Common Patterns
|
|
322
|
+
|
|
323
|
+
### Complete Integration Example
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
class Product < ApplicationRecord
|
|
327
|
+
include EncodedId::Rails::Model
|
|
328
|
+
include EncodedId::Rails::SluggedPathParam
|
|
329
|
+
include EncodedId::Rails::Persists
|
|
330
|
+
include EncodedId::Rails::ActiveRecordFinders
|
|
331
|
+
|
|
332
|
+
def name_for_encoded_id_slug
|
|
333
|
+
name.parameterize
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def self.encoded_id_coder(options = {})
|
|
337
|
+
super(options.merge(
|
|
338
|
+
encoder: :sqids,
|
|
339
|
+
blocklist: ["offensive", "words"],
|
|
340
|
+
id_length: 10
|
|
341
|
+
))
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Usage
|
|
346
|
+
product = Product.create(name: "Cool Gadget")
|
|
347
|
+
product.encoded_id # => "product_k6jR8Myo23"
|
|
348
|
+
product.slugged_encoded_id # => "cool-gadget--product_k6jR8Myo23"
|
|
349
|
+
|
|
350
|
+
# All these work
|
|
351
|
+
Product.find("product_k6jR8Myo23")
|
|
352
|
+
Product.find("cool-gadget--product_k6jR8Myo23")
|
|
353
|
+
Product.find_by_encoded_id("k6jR8Myo23")
|
|
354
|
+
Product.where_encoded_id("product_k6jR8Myo23").active
|
|
355
|
+
|
|
356
|
+
# URLs automatically use slugged IDs
|
|
357
|
+
product_path(product) # => "/products/cool-gadget--product_k6jR8Myo23"
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Migration for Existing Data
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# For persisted encoded IDs
|
|
364
|
+
User.find_each(batch_size: 1000) do |user|
|
|
365
|
+
user.set_normalized_encoded_id!
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Or via background job
|
|
369
|
+
class BackfillEncodedIdsJob < ApplicationJob
|
|
370
|
+
def perform(model_class, start_id, end_id)
|
|
371
|
+
model_class.where(id: start_id..end_id).find_each do |record|
|
|
372
|
+
record.set_normalized_encoded_id!
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Performance Considerations
|
|
379
|
+
|
|
380
|
+
1. **Persistence**: Use `EncodedId::Rails::Persists` for high-traffic lookups
|
|
381
|
+
2. **Indexes**: Add database indexes on `normalized_encoded_id`
|
|
382
|
+
3. **Caching**: Encoded IDs are deterministic - cache them if needed
|
|
383
|
+
4. **Blocklists**: Large blocklists impact performance, especially with Sqids
|
|
384
|
+
|
|
385
|
+
## Best Practices
|
|
386
|
+
|
|
387
|
+
1. **Consistent Configuration**: Don't change salt/encoder after going to production
|
|
388
|
+
2. **Model Naming**: Use clear annotation prefixes to identify model types
|
|
389
|
+
3. **Error Handling**: Always use `find_by_encoded_id!` in controllers for proper 404s
|
|
390
|
+
4. **URL Design**: Choose between encoded IDs vs slugged IDs based on UX needs
|
|
391
|
+
5. **Testing**: Test with both regular IDs and encoded IDs in your specs
|
|
392
|
+
|
|
393
|
+
## Troubleshooting
|
|
394
|
+
|
|
395
|
+
### Load Order Issues
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
# In initializer if seeing load errors
|
|
399
|
+
require 'encoded_id'
|
|
400
|
+
require 'encoded_id/rails'
|
|
401
|
+
|
|
402
|
+
# Or in ApplicationRecord
|
|
403
|
+
require 'encoded_id/rails/model'
|
|
404
|
+
require 'encoded_id/rails/path_param'
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Debugging
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
# Check configuration
|
|
411
|
+
user = User.first
|
|
412
|
+
user.class.encoded_id_salt
|
|
413
|
+
user.class.encoded_id_coder.encoder
|
|
414
|
+
|
|
415
|
+
# Test encoding/decoding
|
|
416
|
+
encoded = User.encode_encoded_id(123)
|
|
417
|
+
decoded = User.decode_encoded_id(encoded)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Security Considerations
|
|
421
|
+
|
|
422
|
+
- Encoded IDs are obfuscated, NOT encrypted
|
|
423
|
+
- Don't rely on them for authentication or authorization
|
|
424
|
+
- They help prevent enumeration attacks but aren't cryptographically secure
|
|
425
|
+
- Always validate decoded IDs before database operations
|
|
426
|
+
|
|
427
|
+
## Example Use Cases
|
|
428
|
+
|
|
429
|
+
1. **Public-Facing IDs**: Hide sequential database IDs from users
|
|
430
|
+
2. **SEO-Friendly URLs**: Combine slugs with encoded IDs for best of both worlds
|
|
431
|
+
3. **API Design**: Provide opaque identifiers that don't leak information
|
|
432
|
+
4. **Multi-Tenant Apps**: Use different salts per tenant for isolation
|
|
433
|
+
5. **Legacy Migration**: Gradually move from numeric to encoded IDs
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# EncodedId Ruby Gem - Technical Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`encoded_id` is a Ruby gem that provides reversible obfuscation of numerical and hexadecimal IDs into human-readable strings suitable for use in URLs. It offers a secure way to hide sequential database IDs from users while maintaining the ability to decode them back to their original values.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
- **Reversible Encoding**: Unlike UUIDs, encoded IDs can be decoded back to their original numeric values
|
|
10
|
+
- **Multiple ID Support**: Encode multiple numeric IDs in a single string
|
|
11
|
+
- **Algorithm Choice**: Supports both HashIds and Sqids encoding algorithms
|
|
12
|
+
- **Human-Readable Format**: Character grouping and configurable separators for better readability
|
|
13
|
+
- **Character Mapping**: Handles easily confused characters (0/O, 1/I/l) through equivalence mapping
|
|
14
|
+
- **Performance Optimized**: Uses an optimized HashIds implementation for better performance
|
|
15
|
+
- **Profanity Protection**: Built-in blocklist support to prevent offensive words in generated IDs
|
|
16
|
+
- **Customizable**: Configurable alphabets, lengths, and formatting options
|
|
17
|
+
|
|
18
|
+
## Core API
|
|
19
|
+
|
|
20
|
+
### EncodedId::ReversibleId
|
|
21
|
+
|
|
22
|
+
The main class for encoding and decoding IDs.
|
|
23
|
+
|
|
24
|
+
#### Constructor
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
EncodedId::ReversibleId.new(
|
|
28
|
+
salt:, # Required: String salt (min 4 chars)
|
|
29
|
+
length: 8, # Minimum length of encoded string
|
|
30
|
+
split_at: 4, # Split encoded string every X characters
|
|
31
|
+
split_with: "-", # Character to split with
|
|
32
|
+
alphabet: EncodedId::Alphabet.modified_crockford,
|
|
33
|
+
hex_digit_encoding_group_size: 4,
|
|
34
|
+
max_length: 128, # Maximum length limit
|
|
35
|
+
max_inputs_per_id: 32, # Maximum IDs to encode together
|
|
36
|
+
encoder: :hashids, # :hashids or :sqids
|
|
37
|
+
blocklist: nil # Words to prevent in IDs
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
#### Key Methods
|
|
42
|
+
|
|
43
|
+
##### encode(values)
|
|
44
|
+
Encodes one or more integer IDs into an obfuscated string.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
coder = EncodedId::ReversibleId.new(salt: "my-salt")
|
|
48
|
+
|
|
49
|
+
# Single ID
|
|
50
|
+
coder.encode(123) # => "p5w9-z27j"
|
|
51
|
+
|
|
52
|
+
# Multiple IDs
|
|
53
|
+
coder.encode([78, 45]) # => "z2j7-0dmw"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
##### decode(encoded_id, downcase: true)
|
|
57
|
+
Decodes an encoded string back to original IDs.
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
coder.decode("p5w9-z27j") # => [123]
|
|
61
|
+
coder.decode("z2j7-0dmw") # => [78, 45]
|
|
62
|
+
|
|
63
|
+
# Handles confused characters
|
|
64
|
+
coder.decode("p5w9-z27J") # => [123]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
##### encode_hex(hex_strings) (Experimental)
|
|
68
|
+
Encodes hexadecimal strings (like UUIDs).
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# Encode UUID
|
|
72
|
+
coder.encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
|
|
73
|
+
# => "5jjy-c8d9-hxp2-qsve-rgh9-rxnt-7nb5-tve7-bf84-vr"
|
|
74
|
+
|
|
75
|
+
# With larger group size for shorter output
|
|
76
|
+
coder = EncodedId::ReversibleId.new(
|
|
77
|
+
salt: "my-salt",
|
|
78
|
+
hex_digit_encoding_group_size: 32
|
|
79
|
+
)
|
|
80
|
+
coder.encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
|
|
81
|
+
# => "vr7m-qra8-m5y6-dkgj-5rqr-q44e-gp4a-52"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
##### decode_hex(encoded_id, downcase: true) (Experimental)
|
|
85
|
+
Decodes back to hexadecimal strings.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
coder.decode_hex("w72a-y0az") # => ["10f8c"]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### EncodedId::Alphabet
|
|
92
|
+
|
|
93
|
+
Class for creating custom alphabets.
|
|
94
|
+
|
|
95
|
+
#### Predefined Alphabets
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# Default: modified Crockford Base32
|
|
99
|
+
# Characters: "0123456789abcdefghjkmnpqrstuvwxyz"
|
|
100
|
+
# Excludes: i, l, o, u (easily confused)
|
|
101
|
+
# Equivalences: {"o"=>"0", "i"=>"j", "l"=>"1", ...}
|
|
102
|
+
EncodedId::Alphabet.modified_crockford
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Custom Alphabets
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
# Simple custom alphabet
|
|
109
|
+
alphabet = EncodedId::Alphabet.new("0123456789abcdef")
|
|
110
|
+
|
|
111
|
+
# With character equivalences
|
|
112
|
+
alphabet = EncodedId::Alphabet.new(
|
|
113
|
+
"0123456789ABCDEF",
|
|
114
|
+
{"a"=>"A", "b"=>"B", "c"=>"C", "d"=>"D", "e"=>"E", "f"=>"F"}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Greek alphabet example
|
|
118
|
+
alphabet = EncodedId::Alphabet.new("αβγδεζηθικλμνξοπρστυφχψω")
|
|
119
|
+
coder = EncodedId::ReversibleId.new(salt: "my-salt", alphabet: alphabet)
|
|
120
|
+
coder.encode(123) # => "θεαψ-ζκυο"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Configuration Options
|
|
124
|
+
|
|
125
|
+
### Basic Options
|
|
126
|
+
|
|
127
|
+
- **salt**: Required secret salt (minimum 4 characters). Changing the salt changes all encoded IDs
|
|
128
|
+
- **length**: Minimum length of encoded string (default: 8)
|
|
129
|
+
- **max_length**: Maximum allowed length (default: 128) to prevent DoS attacks
|
|
130
|
+
- **max_inputs_per_id**: Maximum IDs encodable together (default: 32)
|
|
131
|
+
|
|
132
|
+
### Encoder Selection
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
# Default HashIds encoder
|
|
136
|
+
coder = EncodedId::ReversibleId.new(salt: "my-salt")
|
|
137
|
+
|
|
138
|
+
# Sqids encoder (requires 'sqids' gem)
|
|
139
|
+
coder = EncodedId::ReversibleId.new(salt: "my-salt", encoder: :sqids)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Important**: HashIds and Sqids produce different encodings and are not compatible.
|
|
143
|
+
|
|
144
|
+
### Formatting Options
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# Custom splitting
|
|
148
|
+
coder = EncodedId::ReversibleId.new(
|
|
149
|
+
salt: "my-salt",
|
|
150
|
+
split_at: 3, # Group every 3 chars
|
|
151
|
+
split_with: "." # Use dots
|
|
152
|
+
)
|
|
153
|
+
coder.encode(123) # => "p5w.9z2.7j"
|
|
154
|
+
|
|
155
|
+
# No splitting
|
|
156
|
+
coder = EncodedId::ReversibleId.new(
|
|
157
|
+
salt: "my-salt",
|
|
158
|
+
split_at: nil
|
|
159
|
+
)
|
|
160
|
+
coder.encode(123) # => "p5w9z27j"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Blocklist Configuration
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
# Prevent specific words
|
|
167
|
+
coder = EncodedId::ReversibleId.new(
|
|
168
|
+
salt: "my-salt",
|
|
169
|
+
blocklist: ["bad", "offensive", "words"]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Behavior differs by encoder:
|
|
173
|
+
# - HashIds: Raises error if blocklisted word appears
|
|
174
|
+
# - Sqids: Automatically avoids generating blocklisted words
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Exception Handling
|
|
178
|
+
|
|
179
|
+
| Exception | Description |
|
|
180
|
+
|-----------|-------------|
|
|
181
|
+
| `EncodedId::InvalidConfigurationError` | Invalid configuration parameters |
|
|
182
|
+
| `EncodedId::InvalidAlphabetError` | Invalid alphabet (< 16 unique chars) |
|
|
183
|
+
| `EncodedId::EncodedIdFormatError` | Invalid encoded ID format |
|
|
184
|
+
| `EncodedId::EncodedIdLengthError` | Encoded ID exceeds max_length |
|
|
185
|
+
| `EncodedId::InvalidInputError` | Invalid input (negative integers, too many inputs) |
|
|
186
|
+
| `EncodedId::SaltError` | Invalid salt (too short) |
|
|
187
|
+
|
|
188
|
+
## Usage Examples
|
|
189
|
+
|
|
190
|
+
### Basic Usage
|
|
191
|
+
```ruby
|
|
192
|
+
# Initialize
|
|
193
|
+
coder = EncodedId::ReversibleId.new(salt: "my-secret-salt")
|
|
194
|
+
|
|
195
|
+
# Encode/decode cycle
|
|
196
|
+
encoded = coder.encode(123) # => "p5w9-z27j"
|
|
197
|
+
decoded = coder.decode(encoded) # => [123]
|
|
198
|
+
original_id = decoded.first # => 123
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Multiple IDs
|
|
202
|
+
```ruby
|
|
203
|
+
# Encode multiple IDs in one string
|
|
204
|
+
encoded = coder.encode([78, 45, 92]) # => "z2j7-0dmw-kf8p"
|
|
205
|
+
decoded = coder.decode(encoded) # => [78, 45, 92]
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Custom Configuration
|
|
209
|
+
```ruby
|
|
210
|
+
# Highly customized instance
|
|
211
|
+
coder = EncodedId::ReversibleId.new(
|
|
212
|
+
salt: "my-app-salt",
|
|
213
|
+
encoder: :sqids,
|
|
214
|
+
length: 12,
|
|
215
|
+
split_at: 3,
|
|
216
|
+
split_with: ".",
|
|
217
|
+
alphabet: EncodedId::Alphabet.new("0123456789ABCDEF"),
|
|
218
|
+
blocklist: ["BAD", "FAKE"]
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Hex Encoding (UUIDs)
|
|
223
|
+
```ruby
|
|
224
|
+
# For encoding UUIDs efficiently
|
|
225
|
+
coder = EncodedId::ReversibleId.new(
|
|
226
|
+
salt: "my-salt",
|
|
227
|
+
hex_digit_encoding_group_size: 32
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
uuid = "550e8400-e29b-41d4-a716-446655440000"
|
|
231
|
+
encoded = coder.encode_hex(uuid)
|
|
232
|
+
decoded = coder.decode_hex(encoded).first # => original UUID
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Performance Considerations
|
|
236
|
+
|
|
237
|
+
1. **Algorithm Choice**:
|
|
238
|
+
- HashIds: Faster encoding, especially with blocklists
|
|
239
|
+
- Sqids: Faster decoding
|
|
240
|
+
|
|
241
|
+
2. **Blocklist Impact**: Large blocklists can slow down encoding, especially with Sqids
|
|
242
|
+
|
|
243
|
+
3. **Length vs Performance**: Longer minimum lengths may require more computation
|
|
244
|
+
|
|
245
|
+
4. **Memory Usage**: The gem uses optimized implementations to minimize memory allocation
|
|
246
|
+
|
|
247
|
+
## Security Notes
|
|
248
|
+
|
|
249
|
+
**Important**: Encoded IDs are NOT cryptographically secure. They provide obfuscation, not encryption. Do not rely on them for security purposes. They can potentially be reversed through brute-force attacks if the salt is compromised.
|
|
250
|
+
|
|
251
|
+
Use encoded IDs for:
|
|
252
|
+
- Hiding sequential database IDs
|
|
253
|
+
- Creating user-friendly URLs
|
|
254
|
+
- Preventing ID enumeration attacks
|
|
255
|
+
|
|
256
|
+
Do NOT use for:
|
|
257
|
+
- Secure tokens
|
|
258
|
+
- Authentication
|
|
259
|
+
- Sensitive data protection
|
|
260
|
+
|
|
261
|
+
## Installation
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
# Gemfile
|
|
265
|
+
gem 'encoded_id'
|
|
266
|
+
|
|
267
|
+
# Or install directly
|
|
268
|
+
gem install encoded_id
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
For Sqids support:
|
|
272
|
+
```ruby
|
|
273
|
+
gem 'encoded_id'
|
|
274
|
+
gem 'sqids'
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Best Practices
|
|
278
|
+
|
|
279
|
+
1. **Salt Management**: Use a strong, unique salt and store it securely (e.g., environment variables)
|
|
280
|
+
2. **Consistent Configuration**: Once in production, don't change salt or encoder
|
|
281
|
+
3. **Error Handling**: Always handle potential exceptions when decoding user input
|
|
282
|
+
4. **Length Limits**: Set appropriate max_length to prevent DoS attacks
|
|
283
|
+
5. **Validation**: Validate decoded IDs before using them in database queries
|