encoded_id 1.0.0.rc6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5135a9e76cfd6a49cd14921bfa277deb83eb05b9e753efdb23e4145b3f5f7c7
4
- data.tar.gz: ed8a43d5e654d9fd947d7b9b0a37aacc18c3b713c9303c9d6dbbf22b890eb965
3
+ metadata.gz: ccfa7987d3a108bfce0268467b8ae40793226e98f01fa2c331bc1f642026f47b
4
+ data.tar.gz: b4cb2429a8031f0e576f242550397b4683e302a184d57f0c85b870da502e181e
5
5
  SHA512:
6
- metadata.gz: 91e58ac5b6df75e7da94f8ea4868967e93f6b8c76ec94cb6fc821a60ab33dfe704b19ad43d41916f8896d5db982f6d610e48335f8e9d03129f0422a7b0f5cd60
7
- data.tar.gz: f427c6e2ca883e522e80a934053a5b5486dc75be3a6babf5e55f878d85fe8e31a0a1c1af98b4bbda8a5f310be18ac5ea2b2ec8edf322b54584d78b2126c92c15
6
+ metadata.gz: db4ae0c6d8477ea3ab5ada1bcbd959b18420c1aa1be54ec747898d298f16e6dd6a2090ff4a18008fe1711d3641c158bf9a8c1da23a33663be7fbc17a68079409
7
+ data.tar.gz: 4e731df7f992cf6935012cabffc7c6d4923de5db46059c123780d0f6a1dfbe329844ddf074580c67ec1a16101afb9c315175049045bcf5375e6666ae2707fa0b
data/CHANGELOG.md CHANGED
@@ -1,14 +1,39 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [2.0.0.alpha1] - unreleased
3
+ - nothing yet
4
+
5
+ ## [1.0.0] - 2025-11-21
6
+
7
+ - First stable release!
8
+
9
+ **Important!!: `:sqids` are not compatible with `:hashids`, DO NOT CHANGE FROM ONE TO THE OTHER AFTER GOING LIVE.**
10
+
11
+ ### Fixed (Rails integration)
12
+
13
+ - Ensure finder methods correctly override their ActiveRecord counterparts
14
+ - Warn when ActiveRecord integration used in model that doesn't use `id` as primary key
15
+
16
+ ## [1.0.0.rc7] - 2025-11-19
4
17
 
5
18
  ### Breaking changes
6
19
 
7
- - `ReversibleId` now no longer downcases the encodedid input string by default on decode, ie the `decode` option `downcase` is now `false`. In a future release the `downcase` option will be removed.
8
- - The default encoding engine is now `:sqids` to reflect the official "deprecated" status of `Hashid`s (see https://sqids.org/faq#why-hashids)
20
+ - `ReversibleId` now no longer downcases the encodedid input string by default on decode, ie the `decode` option `downcase` is now `false`. This can be configured via the Rails configuration `downcase_on_decode` option. In a future release the `downcase` option will be removed.
21
+ - The default encoding engine is now `:sqids` to reflect the official "deprecated" status of `Hashid`s (see https://sqids.org/faq#why-hashids)
9
22
  - Ruby < 3.2 support dropped. The minimum supported Ruby version is now 3.2.0
23
+ - `Encoders` classes no longer expose `encode_hex` and `decode_hex` as they worked differently to the similarly named methods on ReversibleId
24
+ - Rails generator now needs you to specify which encoding algorithm you are using
10
25
 
11
- **Important!!: `:sqids` are not compatible with `:hashids`, DO NOT CHANGE FROM ONE TO THE OTHER AFTER GOING LIVE.**
26
+ ### Added (Rails integration)
27
+
28
+ - `encoded_id_options` class method to override `encoded_id` configuration on a model by model basis
29
+ - Blocklist application "modes". User can decide if the blocklist checks should be applied in situations
30
+ where very long encoded IDs are likely and hence block word collisions are undesirably likely.
31
+
32
+ ### Changed
33
+
34
+ - Blocklists are now pre-filtered to the encoder alphabet to avoid testing words which cannot be created by that alphabet anyway.
35
+ - Updates to inline RBS type signatures
36
+ - Documentation updates
12
37
 
13
38
  ## [1.0.0.rc6] - 2025-11-17
14
39
 
data/README.md CHANGED
@@ -7,28 +7,58 @@
7
7
 
8
8
  `encoded_id-rails` is a Rails integration that provides additional features for using `encoded_id` with ActiveRecord models.
9
9
 
10
+ It's one of the most, if not _the_ most, feature complete gem of its kind!
11
+
10
12
  👉 **Full documentation available at [encoded-id.onrender.com](https://encoded-id.onrender.com)**
11
13
 
14
+ ## Key Features
15
+
16
+ * 🔄 **Reversible** - Encoded IDs can be decoded back to the original values
17
+ * 👥 **Multiple IDs** - Encode multiple numeric IDs in one string
18
+ * 🚀 **Choose your encoding** - Supports `Sqids` (default) and `Hashids`, or use your own custom encoder
19
+ * 👓 **Human-readable** - Character grouping & character mappings of easily confused characters for better readability
20
+ * 🔡 **Custom alphabets** - Use your preferred character set, or a provided default
21
+ * 🚗 **Performance** - Uses an optimized `Hashids` encoder (compared to `hashids` gem) for better performance and less memory usage, and have pushed performance improvements to `Sqids` as well
22
+ * 🤬 **Blocklist filtering** - Built-in word blocklist support with configurable modes and optional default lists
23
+
24
+ ### Rails Integration Features
25
+
26
+ * 🏷️ **ActiveRecord integration** - Use with ActiveRecord models
27
+ * 🔑 **Per-model configuration** - Use custom salt and encoding settings per model
28
+ * 💅 **Slugged IDs** - URL-friendly slugs like `my-product--p5w9-z27j`
29
+ * 🔖 **Annotated IDs** - Model type indicators like `user_p5w9-z27j` (default behavior)
30
+ * 🔍 **Finder methods** - `find_by_encoded_id`, `find_by_encoded_id!`, `find_all_by_encoded_id`, `where_encoded_id`
31
+ * 🛣️ **URL params** - `to_param` automatically uses encoded IDs
32
+ * 🔒 **Safe defaults** - Limits on encoded ID lengths to prevent CPU and memory-intensive encode/decodes when used in URLs
33
+ * 💾 **Persistence** - Optional database persistence for efficient lookups
34
+
35
+
12
36
  ## Quick Example
13
37
 
14
38
  ```ruby
15
- coder = ::EncodedId::ReversibleId.new(salt: "my-salt")
39
+ # Using Hashids encoder (requires salt)
40
+ coder = ::EncodedId::ReversibleId.hashid(salt: "my-salt")
16
41
  coder.encode(123)
17
- # => "p5w9-z27j"
42
+ # => "m3pm-8anj"
18
43
 
19
44
  # The encoded strings are reversible
20
- coder.decode("p5w9-z27j")
45
+ coder.decode("m3pm-8anj")
21
46
  # => [123]
22
47
 
23
48
  # Supports encoding multiple IDs at once
24
49
  coder.encode([78, 45])
25
- # => "z2j7-0dmw"
50
+ # => "ny9y-sd7p"
51
+
52
+ # Using Sqids encoder (default, no salt required)
53
+ sqids_coder = ::EncodedId::ReversibleId.sqids
54
+ sqids_coder.encode(123)
55
+ # => (encoded value varies)
26
56
 
27
57
  # Can also be used with ActiveRecord models
28
58
  class User < ApplicationRecord
29
59
  include EncodedId::Rails::Model
30
-
31
- # Optional slug for the encoded ID
60
+
61
+ # Optional: define method to provide slug for encoded ID
32
62
  def name_for_encoded_id_slug
33
63
  full_name
34
64
  end
@@ -40,28 +70,6 @@ user.encoded_id # => "user_p5w9-z27j"
40
70
  user.slugged_encoded_id # => "bob-smith--user_p5w9-z27j"
41
71
  ```
42
72
 
43
- ## Key Features
44
-
45
- * 🔄 **Reversible** - Encoded IDs can be decoded back to the original values
46
- * 👥 **Multiple IDs** - Encode multiple numeric IDs in one string
47
- * 🚀 **Choose your encoding** - Supports `Hashids` and `Sqids` out of the box, or use your own custom encoder
48
- * 👓 **Human-readable** - Character grouping & character mappings of easily confused characters for better readability
49
- * 🔡 **Custom alphabets** - Use your preferred character set, or a provided default
50
- * 🚗 **Performance** - Uses an optimized `Hashids` encoder (compared to `hashids` gem) for better performance and less memory usage, and have pushed performance improvements to `Sqids` as well
51
- * 🤬 **Profanity blocking** - Built-in word blocklist support and optional default lists
52
-
53
- ### Rails Integration Features
54
-
55
- * 🏷️ **ActiveRecord integration** - Use with ActiveRecord models
56
- * 🔑 **Per-model salt** - Use a custom salt for encoding per model
57
- * 💅 **Slugged IDs** - URL-friendly slugs like `my-product--p5w9-z27j`
58
- * 🔖 **Annotated IDs** - Model type indicators like `user_p5w9-z27j`
59
- * 🔍 **Finder methods** - Find records using encoded IDs
60
- * 🛣️ **URL params** - `to_param` with encoded IDs
61
- * 🔒 **Safe defaults**: Limits on encoded ID lengths to prevent CPU and memory-intensive encode/decodes eg when used in URLs
62
- * 💾 **Persistence** - Optional database persistence for efficient lookups
63
-
64
-
65
73
  ## Standalone Gem
66
74
 
67
75
 
@@ -91,6 +99,10 @@ See the [Rails Integration](https://encoded-id.onrender.com/docs/encoded_id_rail
91
99
 
92
100
  **Encoded IDs are not secure**. They are meant to provide obfuscation, not encryption. Do not use them as a security mechanism.
93
101
 
102
+ As of version 1.0.0, `Sqids` is the default encoder. `Hashids` is still supported but is officially deprecated by the Hashids project in favor of Sqids.
103
+
104
+ Read more about the security implications: [Hashids expose salt value](https://www.sjoerdlangkemper.nl/2023/11/25/hashids-expose-salt-value/) (note: this specifically applies to Hashids encoder)
105
+
94
106
  ## Compare to Alternate Gems
95
107
 
96
108
  - [prefixed_ids](https://github.com/excid3/prefixed_ids)
@@ -105,11 +117,10 @@ For a detailed comparison, see the [Compared to Other Gems](https://encoded-id.o
105
117
 
106
118
  Visit [encoded-id.onrender.com](https://encoded-id.onrender.com) for comprehensive documentation including:
107
119
 
108
- - [EncodedId Core API](https://encoded-id.onrender.com/docs/encoded_id/api)
109
- - [Rails Integration API](https://encoded-id.onrender.com/docs/encoded_id_rails/api)
110
- - [Configuration Options](https://encoded-id.onrender.com/docs/encoded_id/configuration)
111
- - [Examples](https://encoded-id.onrender.com/docs/encoded_id/examples)
112
- - [Advanced Topics](https://encoded-id.onrender.com/docs/advanced-topics)
120
+ - [EncodedId Core Guide](https://encoded-id.onrender.com/docs/encoded_id/) - Installation, configuration, examples, and advanced topics
121
+ - [EncodedId Rails Guide](https://encoded-id.onrender.com/docs/encoded_id_rails/) - Rails integration, configuration, and examples
122
+ - [EncodedId Core API Reference](https://encoded-id.onrender.com/docs/encoded_id/api)
123
+ - [Rails Integration API Reference](https://encoded-id.onrender.com/docs/encoded_id_rails/api)
113
124
 
114
125
  ## Development
115
126
 
@@ -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
@@ -3,6 +3,7 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  module EncodedId
6
+ # Represents a character set (alphabet) used for encoding IDs, with optional character equivalences.
6
7
  class Alphabet
7
8
  MIN_UNIQUE_CHARACTERS = 16
8
9
 
@@ -94,6 +95,7 @@ module EncodedId
94
95
  unique_characters.size >= MIN_UNIQUE_CHARACTERS
95
96
  end
96
97
 
98
+ # Validates equivalences ensure: keys map to values in the alphabet, and keys are not already in the alphabet
97
99
  # @rbs (Hash[String, String]? equivalences) -> bool
98
100
  def valid_equivalences?(equivalences)
99
101
  return true if equivalences.nil?