encoded_id-rails 1.0.0.rc6 → 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.
@@ -14,6 +14,25 @@
14
14
  - **Performance Optimized**: Uses an optimized HashIds implementation for better performance
15
15
  - **Profanity Protection**: Built-in blocklist support to prevent offensive words in generated IDs
16
16
  - **Customizable**: Configurable alphabets, lengths, and formatting options
17
+ - **Blocklist Modes**: Three modes for controlling blocklist checking performance
18
+
19
+ ## Quick Reference
20
+
21
+ ```ruby
22
+ # Sqids encoder (default, no salt required)
23
+ coder = EncodedId::ReversibleId.sqids(min_length: 10)
24
+ id = coder.encode(123) # => "p5w9-z27j-k8"
25
+ nums = coder.decode(id) # => [123]
26
+
27
+ # Hashids encoder (requires salt)
28
+ coder = EncodedId::ReversibleId.hashid(salt: "my-salt", min_length: 8)
29
+ id = coder.encode([78, 45]) # => "z2j7-0dmw"
30
+ nums = coder.decode(id) # => [78, 45]
31
+
32
+ # UUID encoding (experimental)
33
+ hex_id = coder.encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
34
+ uuid = coder.decode_hex(hex_id).first
35
+ ```
17
36
 
18
37
  ## Core API
19
38
 
@@ -21,30 +40,65 @@
21
40
 
22
41
  The main class for encoding and decoding IDs.
23
42
 
24
- #### Constructor
43
+ #### Factory Methods (Recommended)
44
+
45
+ Factory methods provide the cleanest way to create encoders:
25
46
 
26
47
  ```ruby
27
- EncodedId::ReversibleId.new(
28
- salt:, # Required: String salt (min 4 chars)
29
- length: 8, # Minimum length of encoded string
48
+ # Sqids encoder (default, no salt required)
49
+ coder = EncodedId::ReversibleId.sqids(
50
+ min_length: 10,
51
+ blocklist: ["bad", "words"]
52
+ )
53
+
54
+ # Hashids encoder (requires salt)
55
+ coder = EncodedId::ReversibleId.hashid(
56
+ salt: "my-salt",
57
+ min_length: 8,
58
+ blocklist: ["bad", "words"]
59
+ )
60
+ ```
61
+
62
+ Both factory methods accept all configuration options described below.
63
+
64
+ #### Constructor (Alternative)
65
+
66
+ You can also use the constructor with explicit configuration objects:
67
+
68
+ ```ruby
69
+ # Using Sqids configuration
70
+ config = EncodedId::Encoders::SqidsConfiguration.new(
71
+ min_length: 8, # Minimum length of encoded string
30
72
  split_at: 4, # Split encoded string every X characters
31
73
  split_with: "-", # Character to split with
32
74
  alphabet: EncodedId::Alphabet.modified_crockford,
33
75
  hex_digit_encoding_group_size: 4,
34
76
  max_length: 128, # Maximum length limit
35
77
  max_inputs_per_id: 32, # Maximum IDs to encode together
36
- encoder: :hashids, # :hashids or :sqids
37
- blocklist: nil # Words to prevent in IDs
78
+ blocklist: nil, # Words to prevent in IDs
79
+ blocklist_mode: :length_threshold, # :always, :length_threshold, or :raise_if_likely
80
+ blocklist_max_length: 32 # Max length for :length_threshold mode
81
+ )
82
+ coder = EncodedId::ReversibleId.new(config)
83
+
84
+ # Using Hashids configuration (requires salt)
85
+ config = EncodedId::Encoders::HashidConfiguration.new(
86
+ salt: "my-salt", # Required for Hashids (min 4 chars)
87
+ min_length: 8,
88
+ # ... other options same as above
38
89
  )
90
+ coder = EncodedId::ReversibleId.new(config)
39
91
  ```
40
92
 
93
+ **Note**: As of v1.0.0, the default encoder is `:sqids`. For backwards compatibility with pre-v1 versions, use `ReversibleId.hashid()`.
94
+
41
95
  #### Key Methods
42
96
 
43
97
  ##### encode(values)
44
98
  Encodes one or more integer IDs into an obfuscated string.
45
99
 
46
100
  ```ruby
47
- coder = EncodedId::ReversibleId.new(salt: "my-salt")
101
+ coder = EncodedId::ReversibleId.sqids
48
102
 
49
103
  # Single ID
50
104
  coder.encode(123) # => "p5w9-z27j"
@@ -53,17 +107,22 @@ coder.encode(123) # => "p5w9-z27j"
53
107
  coder.encode([78, 45]) # => "z2j7-0dmw"
54
108
  ```
55
109
 
56
- ##### decode(encoded_id, downcase: true)
110
+ ##### decode(encoded_id, downcase: false)
57
111
  Decodes an encoded string back to original IDs.
58
112
 
59
113
  ```ruby
60
114
  coder.decode("p5w9-z27j") # => [123]
61
115
  coder.decode("z2j7-0dmw") # => [78, 45]
62
116
 
63
- # Handles confused characters
64
- coder.decode("p5w9-z27J") # => [123]
117
+ # Case-sensitive by default (v1.0.0+)
118
+ coder.decode("p5w9-z27J") # => [] (case doesn't match)
119
+
120
+ # For case-insensitive matching (pre-v1 behavior)
121
+ coder.decode("p5w9-z27J", downcase: true) # => [123]
65
122
  ```
66
123
 
124
+ **Note**: As of v1.0.0, decoding is case-sensitive by default (`downcase: false`). Set `downcase: true` for backwards compatibility.
125
+
67
126
  ##### encode_hex(hex_strings) (Experimental)
68
127
  Encodes hexadecimal strings (like UUIDs).
69
128
 
@@ -73,19 +132,19 @@ coder.encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
73
132
  # => "5jjy-c8d9-hxp2-qsve-rgh9-rxnt-7nb5-tve7-bf84-vr"
74
133
 
75
134
  # With larger group size for shorter output
76
- coder = EncodedId::ReversibleId.new(
77
- salt: "my-salt",
78
- hex_digit_encoding_group_size: 32
79
- )
135
+ coder = EncodedId::ReversibleId.sqids(hex_digit_encoding_group_size: 32)
80
136
  coder.encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
81
137
  # => "vr7m-qra8-m5y6-dkgj-5rqr-q44e-gp4a-52"
82
138
  ```
83
139
 
84
- ##### decode_hex(encoded_id, downcase: true) (Experimental)
140
+ ##### decode_hex(encoded_id, downcase: false) (Experimental)
85
141
  Decodes back to hexadecimal strings.
86
142
 
87
143
  ```ruby
88
144
  coder.decode_hex("w72a-y0az") # => ["10f8c"]
145
+
146
+ # For case-insensitive decoding (pre-v1 behavior)
147
+ coder.decode_hex("W72A-Y0AZ", downcase: true) # => ["10f8c"]
89
148
  ```
90
149
 
91
150
  ### EncodedId::Alphabet
@@ -116,62 +175,127 @@ alphabet = EncodedId::Alphabet.new(
116
175
 
117
176
  # Greek alphabet example
118
177
  alphabet = EncodedId::Alphabet.new("αβγδεζηθικλμνξοπρστυφχψω")
119
- coder = EncodedId::ReversibleId.new(salt: "my-salt", alphabet: alphabet)
178
+ coder = EncodedId::ReversibleId.sqids(alphabet: alphabet)
120
179
  coder.encode(123) # => "θεαψ-ζκυο"
121
180
  ```
122
181
 
182
+ ### EncodedId::Blocklist
183
+
184
+ Class for managing profanity/word blocklists.
185
+
186
+ #### Predefined Blocklists
187
+
188
+ ```ruby
189
+ # Empty blocklist (no filtering)
190
+ EncodedId::Blocklist.empty
191
+
192
+ # Minimal blocklist (~50 common profane words)
193
+ EncodedId::Blocklist.minimal
194
+
195
+ # Full Sqids default blocklist (comprehensive)
196
+ EncodedId::Blocklist.sqids_blocklist
197
+
198
+ # Use in configuration
199
+ coder = EncodedId::ReversibleId.sqids(
200
+ blocklist: EncodedId::Blocklist.minimal
201
+ )
202
+ ```
203
+
204
+ #### Custom Blocklists
205
+
206
+ ```ruby
207
+ # From array
208
+ blocklist = EncodedId::Blocklist.new(["bad", "offensive", "words"])
209
+
210
+ # Merge blocklists
211
+ combined = EncodedId::Blocklist.minimal.merge(
212
+ EncodedId::Blocklist.new(["custom", "words"])
213
+ )
214
+
215
+ # Filter for specific alphabet (automatic with configuration)
216
+ filtered = blocklist.filter_for_alphabet(EncodedId::Alphabet.modified_crockford)
217
+ ```
218
+
219
+ **Note**: Blocklists are automatically filtered to only include words possible with your configured alphabet. This optimization improves performance.
220
+
123
221
  ## Configuration Options
124
222
 
125
223
  ### Basic Options
126
224
 
127
- - **salt**: Required secret salt (minimum 4 characters). Changing the salt changes all encoded IDs
128
- - **length**: Minimum length of encoded string (default: 8)
225
+ - **min_length**: Minimum length of encoded string (default: 8)
129
226
  - **max_length**: Maximum allowed length (default: 128) to prevent DoS attacks
130
227
  - **max_inputs_per_id**: Maximum IDs encodable together (default: 32)
228
+ - **hex_digit_encoding_group_size**: Group size for hex encoding (default: 4)
131
229
 
132
230
  ### Encoder Selection
133
231
 
134
232
  ```ruby
135
- # Default HashIds encoder
136
- coder = EncodedId::ReversibleId.new(salt: "my-salt")
233
+ # Sqids encoder (default, no salt required)
234
+ coder = EncodedId::ReversibleId.sqids
137
235
 
138
- # Sqids encoder (requires 'sqids' gem)
139
- coder = EncodedId::ReversibleId.new(salt: "my-salt", encoder: :sqids)
236
+ # Hashids encoder (requires salt - minimum 4 characters)
237
+ coder = EncodedId::ReversibleId.hashid(salt: "my-salt-minimum-4-chars")
140
238
  ```
141
239
 
142
- **Important**: HashIds and Sqids produce different encodings and are not compatible.
240
+ **Important**:
241
+ - As of v1.0.0, `:sqids` is the default encoder
242
+ - **Sqids**: No salt required, automatically avoids blocklisted words via iteration
243
+ - **Hashids**: Salt required (min 4 chars), raises exception if blocklisted word appears
244
+ - HashIds and Sqids produce different encodings and are **not compatible**
245
+ - Do NOT change encoders after going to production with existing encoded IDs
143
246
 
144
- ### Formatting Options
247
+ ### Blocklist Configuration
248
+
249
+ #### Blocklist Modes
250
+
251
+ Control how blocklist checking behaves to balance performance and safety:
145
252
 
146
253
  ```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
254
+ # :length_threshold (default) - Check blocklist only until encoded length reaches blocklist_max_length
255
+ # Best for most use cases - prevents performance issues with very long IDs
256
+ coder = EncodedId::ReversibleId.sqids(
257
+ blocklist: EncodedId::Blocklist.minimal,
258
+ blocklist_mode: :length_threshold,
259
+ blocklist_max_length: 32 # Stop checking after 32 characters
152
260
  )
153
- coder.encode(123) # => "p5w.9z2.7j"
154
261
 
155
- # No splitting
156
- coder = EncodedId::ReversibleId.new(
262
+ # :always - Always check blocklist regardless of encoded length
263
+ # Can be slow for long IDs or large blocklists
264
+ coder = EncodedId::ReversibleId.hashid(
157
265
  salt: "my-salt",
158
- split_at: nil
266
+ blocklist: ["bad", "words"],
267
+ blocklist_mode: :always
159
268
  )
160
- coder.encode(123) # => "p5w9z27j"
269
+
270
+ # :raise_if_likely - Raise error at configuration time if settings likely cause blocklist collisions
271
+ # Prevents configurations that would cause performance issues
272
+ coder = EncodedId::ReversibleId.sqids(
273
+ min_length: 8,
274
+ blocklist: ["bad", "words"],
275
+ blocklist_mode: :raise_if_likely
276
+ )
277
+ # Raises InvalidConfigurationError if min_length > blocklist_max_length
161
278
  ```
162
279
 
163
- ### Blocklist Configuration
280
+ **Blocklist Behavior by Encoder**:
281
+ - **Sqids**: Iteratively regenerates to avoid blocklisted words (may impact encoding performance)
282
+ - **Hashids**: Raises `EncodedId::BlocklistError` if a blocklisted word appears
283
+
284
+ **Recommendation**: Use `:length_threshold` mode (default) for best balance of performance and safety.
285
+
286
+ ### Formatting Options
164
287
 
165
288
  ```ruby
166
- # Prevent specific words
167
- coder = EncodedId::ReversibleId.new(
168
- salt: "my-salt",
169
- blocklist: ["bad", "offensive", "words"]
289
+ # Custom splitting
290
+ coder = EncodedId::ReversibleId.sqids(
291
+ split_at: 3, # Group every 3 chars
292
+ split_with: "." # Use dots
170
293
  )
294
+ coder.encode(123) # => "p5w.9z2.7j"
171
295
 
172
- # Behavior differs by encoder:
173
- # - HashIds: Raises error if blocklisted word appears
174
- # - Sqids: Automatically avoids generating blocklisted words
296
+ # No splitting
297
+ coder = EncodedId::ReversibleId.sqids(split_at: nil)
298
+ coder.encode(123) # => "p5w9z27j"
175
299
  ```
176
300
 
177
301
  ## Exception Handling
@@ -183,14 +307,15 @@ coder = EncodedId::ReversibleId.new(
183
307
  | `EncodedId::EncodedIdFormatError` | Invalid encoded ID format |
184
308
  | `EncodedId::EncodedIdLengthError` | Encoded ID exceeds max_length |
185
309
  | `EncodedId::InvalidInputError` | Invalid input (negative integers, too many inputs) |
186
- | `EncodedId::SaltError` | Invalid salt (too short) |
310
+ | `EncodedId::SaltError` | Invalid salt (too short, only for Hashids) |
311
+ | `EncodedId::BlocklistError` | Generated ID contains blocklisted word (Hashids only) |
187
312
 
188
313
  ## Usage Examples
189
314
 
190
315
  ### Basic Usage
191
316
  ```ruby
192
- # Initialize
193
- coder = EncodedId::ReversibleId.new(salt: "my-secret-salt")
317
+ # Initialize with Sqids (no salt needed)
318
+ coder = EncodedId::ReversibleId.sqids
194
319
 
195
320
  # Encode/decode cycle
196
321
  encoded = coder.encode(123) # => "p5w9-z27j"
@@ -205,44 +330,79 @@ encoded = coder.encode([78, 45, 92]) # => "z2j7-0dmw-kf8p"
205
330
  decoded = coder.decode(encoded) # => [78, 45, 92]
206
331
  ```
207
332
 
208
- ### Custom Configuration
333
+ ### With Hashids and Blocklist
209
334
  ```ruby
210
- # Highly customized instance
211
- coder = EncodedId::ReversibleId.new(
335
+ coder = EncodedId::ReversibleId.hashid(
212
336
  salt: "my-app-salt",
213
- encoder: :sqids,
214
- length: 12,
337
+ min_length: 12,
338
+ blocklist: EncodedId::Blocklist.minimal,
339
+ blocklist_mode: :length_threshold
340
+ )
341
+
342
+ encoded = coder.encode(123)
343
+ # Raises BlocklistError if result contains blocklisted word
344
+ ```
345
+
346
+ ### Custom Configuration
347
+ ```ruby
348
+ # Highly customized Sqids instance
349
+ coder = EncodedId::ReversibleId.sqids(
350
+ min_length: 12,
215
351
  split_at: 3,
216
352
  split_with: ".",
217
353
  alphabet: EncodedId::Alphabet.new("0123456789ABCDEF"),
218
- blocklist: ["BAD", "FAKE"]
354
+ blocklist: ["BAD", "FAKE"],
355
+ blocklist_mode: :length_threshold,
356
+ blocklist_max_length: 32
219
357
  )
220
358
  ```
221
359
 
222
360
  ### Hex Encoding (UUIDs)
223
361
  ```ruby
224
362
  # For encoding UUIDs efficiently
225
- coder = EncodedId::ReversibleId.new(
226
- salt: "my-salt",
227
- hex_digit_encoding_group_size: 32
228
- )
363
+ coder = EncodedId::ReversibleId.sqids(hex_digit_encoding_group_size: 32)
229
364
 
230
365
  uuid = "550e8400-e29b-41d4-a716-446655440000"
231
366
  encoded = coder.encode_hex(uuid)
232
- decoded = coder.decode_hex(encoded).first # => original UUID
367
+ decoded = coder.decode_hex(encoded).first # => original UUID (without hyphens)
233
368
  ```
234
369
 
235
370
  ## Performance Considerations
236
371
 
237
- 1. **Algorithm Choice**:
372
+ 1. **Algorithm Choice**:
238
373
  - HashIds: Faster encoding, especially with blocklists
239
- - Sqids: Faster decoding
374
+ - Sqids: Faster decoding, automatically avoids blocklisted words
375
+
376
+ 2. **Blocklist Impact**:
377
+ - Large blocklists slow encoding, especially with Sqids (which iterates to avoid words)
378
+ - Hashids may raise exceptions requiring retry logic
379
+ - Use `blocklist_mode: :length_threshold` for best performance
380
+ - `:always` mode can significantly impact encoding speed for long IDs
381
+ - Blocklists are automatically filtered for your alphabet, improving performance
382
+
383
+ 3. **Blocklist Mode Performance**:
384
+ - `:length_threshold` (default): Only checks blocklist for IDs ≤ `blocklist_max_length` (default: 32)
385
+ - `:always`: Checks all IDs regardless of length (can be slow)
386
+ - `:raise_if_likely`: Validates configuration at initialization to prevent performance issues
387
+
388
+ 4. **Length vs Performance**: Longer minimum lengths may require more computation
389
+
390
+ 5. **Memory Usage**: The gem uses optimized implementations to minimize memory allocation
240
391
 
241
- 2. **Blocklist Impact**: Large blocklists can slow down encoding, especially with Sqids
392
+ ## Version Compatibility
242
393
 
243
- 3. **Length vs Performance**: Longer minimum lengths may require more computation
394
+ **v1.0.0 Breaking Changes:**
244
395
 
245
- 4. **Memory Usage**: The gem uses optimized implementations to minimize memory allocation
396
+ 1. **Default encoder**: Changed from `:hashids` to `:sqids`
397
+ 2. **Case sensitivity**: `decode` is now case-sensitive by default (`downcase: false`)
398
+ - Pre-v1: `decode("ABC")` and `decode("abc")` were equivalent
399
+ - v1.0.0+: These produce different results unless `downcase: true`
400
+ 3. **Salt requirement**: Sqids (default) doesn't require salt; Hashids still requires salt
401
+ 4. **Migration**: For backwards compatibility with pre-v1:
402
+ ```ruby
403
+ coder = EncodedId::ReversibleId.hashid(salt: "your-salt")
404
+ decoded = coder.decode(id, downcase: true)
405
+ ```
246
406
 
247
407
  ## Security Notes
248
408
 
@@ -252,32 +412,26 @@ Use encoded IDs for:
252
412
  - Hiding sequential database IDs
253
413
  - Creating user-friendly URLs
254
414
  - Preventing ID enumeration attacks
415
+ - Obscuring business metrics (user counts, order volumes)
255
416
 
256
417
  Do NOT use for:
257
418
  - Secure tokens
258
419
  - Authentication
259
420
  - Sensitive data protection
421
+ - Cryptographic purposes
260
422
 
261
423
  ## Installation
262
424
 
263
425
  ```ruby
264
426
  # Gemfile
265
427
  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
428
  ```
276
429
 
277
430
  ## Best Practices
278
431
 
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
432
+ 1. **Consistent Configuration**: Once in production, don't change salt, encoder, or alphabet
433
+ 2. **Error Handling**: Always handle potential exceptions when decoding user input
434
+ 3. **Length Limits**: Set appropriate max_length to prevent DoS attacks
435
+ 4. **Validation**: Validate decoded IDs before using them in database queries
436
+ 5. **Blocklist Mode**: Use `:length_threshold` (default) for production - best performance/safety balance
437
+ 6. **Factory Methods**: Prefer `ReversibleId.sqids()` and `ReversibleId.hashid()` over constructor
@@ -20,12 +20,14 @@ module EncodedId
20
20
  end
21
21
  end
22
22
 
23
+ # Class methods for overriding ActiveRecord's finder methods to decode encoded IDs.
23
24
  module ClassMethods
24
25
  # @rbs (*untyped args) -> untyped
25
26
  def find(*args)
26
- return super unless args.size == 1 && args.first.is_a?(String)
27
+ first_arg = args.first
28
+ return super unless args.size == 1 && first_arg.is_a?(String)
27
29
 
28
- decoded_ids = decode_encoded_id(args.first)
30
+ decoded_ids = decode_encoded_id(first_arg)
29
31
 
30
32
  if decoded_ids.blank?
31
33
  raise ::ActiveRecord::RecordNotFound
@@ -2,28 +2,25 @@
2
2
 
3
3
  # rbs_inline: enabled
4
4
 
5
- require "cgi"
6
-
7
5
  module EncodedId
8
6
  module Rails
9
- class AnnotatedId
10
- # @rbs @annotation: String
11
- # @rbs @id_part: String
12
- # @rbs @separator: String
13
-
7
+ # Represents an encoded ID with an annotation prefix (e.g., "user_ABC123").
8
+ class AnnotatedId < CompositeIdBase
14
9
  # @rbs (annotation: String, id_part: String, ?separator: String) -> void
15
10
  def initialize(annotation:, id_part:, separator: "_")
16
- @annotation = annotation
17
- @id_part = id_part
18
- @separator = separator
11
+ super(first_part: annotation, id_part: id_part, separator: separator)
19
12
  end
20
13
 
21
14
  # @rbs return: String
22
15
  def annotated_id
23
- unless @id_part.present? && @annotation.present?
24
- raise ::StandardError, "The model does not provide a valid ID and/or annotation"
25
- end
26
- "#{@annotation.to_s.parameterize}#{CGI.escape(@separator)}#{@id_part}"
16
+ build_composite_id
17
+ end
18
+
19
+ private
20
+
21
+ # @rbs return: String
22
+ def invalid_id_error_message
23
+ "The model does not return a valid ID and/or annotation"
27
24
  end
28
25
  end
29
26
  end
@@ -4,6 +4,7 @@
4
4
 
5
5
  module EncodedId
6
6
  module Rails
7
+ # Parses an annotated ID into its annotation and ID components.
7
8
  class AnnotatedIdParser
8
9
  # @rbs @annotation: String?
9
10
  # @rbs @id: String
@@ -4,24 +4,32 @@
4
4
 
5
5
  module EncodedId
6
6
  module Rails
7
+ # Encodes and decodes IDs using the configured encoder and settings.
7
8
  class Coder
8
- # @rbs @salt: String
9
+ # @rbs @salt: String?
9
10
  # @rbs @id_length: Integer
10
11
  # @rbs @character_group_size: Integer
11
12
  # @rbs @separator: String
12
13
  # @rbs @alphabet: ::EncodedId::Alphabet
13
- # @rbs @encoder: (Symbol | ::EncodedId::Encoders::Base)
14
+ # @rbs @encoder: Symbol
14
15
  # @rbs @blocklist: ::EncodedId::Blocklist?
16
+ # @rbs @blocklist_mode: Symbol
17
+ # @rbs @blocklist_max_length: Integer
18
+ # @rbs @downcase_on_decode: bool
15
19
 
16
- # @rbs (salt: String, id_length: Integer, character_group_size: Integer, separator: String, alphabet: ::EncodedId::Alphabet, ?encoder: Symbol?, ?blocklist: ::EncodedId::Blocklist?) -> void
17
- def initialize(salt:, id_length:, character_group_size:, separator:, alphabet:, encoder: nil, blocklist: nil)
20
+ # @rbs (salt: String?, id_length: Integer, character_group_size: Integer, separator: String, alphabet: ::EncodedId::Alphabet, ?encoder: Symbol?, ?blocklist: ::EncodedId::Blocklist?, ?blocklist_mode: Symbol?, ?blocklist_max_length: Integer?, ?downcase_on_decode: bool?) -> void
21
+ def initialize(salt:, id_length:, character_group_size:, separator:, alphabet:, encoder: nil, blocklist: nil, blocklist_mode: nil, blocklist_max_length: nil, downcase_on_decode: nil)
18
22
  @salt = salt
19
23
  @id_length = id_length
20
24
  @character_group_size = character_group_size
21
25
  @separator = separator
22
26
  @alphabet = alphabet
23
- @encoder = encoder || EncodedId::Rails.configuration.encoder
24
- @blocklist = blocklist || EncodedId::Rails.configuration.blocklist
27
+ config = EncodedId::Rails.configuration
28
+ @encoder = encoder || config.encoder
29
+ @blocklist = blocklist || config.blocklist
30
+ @blocklist_mode = blocklist_mode || config.blocklist_mode
31
+ @blocklist_max_length = blocklist_max_length || config.blocklist_max_length
32
+ @downcase_on_decode = downcase_on_decode.nil? ? config.downcase_on_decode : downcase_on_decode
25
33
  end
26
34
 
27
35
  # @rbs (Integer | Array[Integer] id) -> String
@@ -29,26 +37,45 @@ module EncodedId
29
37
  coder.encode(id)
30
38
  end
31
39
 
32
- # @rbs (String encoded_id) -> Array[Integer]?
40
+ # @rbs (String encoded_id) -> Array[Integer]
33
41
  def decode(encoded_id)
34
- coder.decode(encoded_id)
42
+ coder.decode(encoded_id, downcase: @downcase_on_decode)
35
43
  rescue EncodedId::EncodedIdFormatError, EncodedId::InvalidInputError
36
- nil
44
+ []
37
45
  end
38
46
 
39
47
  private
40
48
 
41
49
  # @rbs return: ::EncodedId::ReversibleId
42
50
  def coder
43
- ::EncodedId::ReversibleId.new(
44
- salt: @salt,
45
- length: @id_length,
46
- split_at: @character_group_size,
47
- split_with: @separator,
48
- alphabet: @alphabet,
49
- encoder: @encoder,
50
- blocklist: @blocklist
51
- )
51
+ # Build the appropriate configuration based on encoder type
52
+ config = case @encoder
53
+ when :hashids
54
+ ::EncodedId::Encoders::HashidConfiguration.new(
55
+ salt: @salt || raise(ArgumentError, "Salt is required for hashids encoder"),
56
+ min_length: @id_length,
57
+ split_at: @character_group_size,
58
+ split_with: @separator,
59
+ alphabet: @alphabet,
60
+ blocklist: @blocklist,
61
+ blocklist_mode: @blocklist_mode,
62
+ blocklist_max_length: @blocklist_max_length
63
+ )
64
+ when :sqids
65
+ ::EncodedId::Encoders::SqidsConfiguration.new(
66
+ min_length: @id_length,
67
+ split_at: @character_group_size,
68
+ split_with: @separator,
69
+ alphabet: @alphabet,
70
+ blocklist: @blocklist,
71
+ blocklist_mode: @blocklist_mode,
72
+ blocklist_max_length: @blocklist_max_length
73
+ )
74
+ else
75
+ raise ArgumentError, "Unknown encoder type: #{@encoder}"
76
+ end
77
+
78
+ ::EncodedId::ReversibleId.new(config)
52
79
  end
53
80
  end
54
81
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ require "cgi"
6
+
7
+ module EncodedId
8
+ module Rails
9
+ # Base class for composite IDs (slugged and annotated)
10
+ class CompositeIdBase
11
+ # @rbs @first_part: String
12
+ # @rbs @id_part: String
13
+ # @rbs @separator: String
14
+
15
+ # @rbs (first_part: String, id_part: String, separator: String) -> void
16
+ def initialize(first_part:, id_part:, separator:)
17
+ @first_part = first_part
18
+ @id_part = id_part
19
+ @separator = separator
20
+ end
21
+
22
+ private
23
+
24
+ # @rbs return: String
25
+ def build_composite_id
26
+ unless @id_part.present? && @first_part.present?
27
+ raise ::StandardError, invalid_id_error_message
28
+ end
29
+ "#{@first_part.to_s.parameterize}#{CGI.escape(@separator)}#{@id_part}"
30
+ end
31
+
32
+ # Default error message. Subclasses can override for more specific messages.
33
+ # @rbs return: String
34
+ def invalid_id_error_message
35
+ "The model does not return a valid ID and/or prefix"
36
+ end
37
+ end
38
+ end
39
+ end