encoded_id-rails 0.6.2 → 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6c56ae6571c9793eb8b20d4cb2d9bfbbc3e1ebf1c313b774c42640ea68d6584
4
- data.tar.gz: 58b5fbf9eb0e79fc152f23a2055f7c9ec2131dd5000342f0fe3d2e40f796f712
3
+ metadata.gz: 0a92cfd619fa254d0996950214c14aa115d6c62327e4ec2a6715dc9d7d898ebf
4
+ data.tar.gz: 27b448535392de1e1e21c37b82c324e4b5aecdc5225d4c7b2757a1ce8d5d18a9
5
5
  SHA512:
6
- metadata.gz: cf5668fbf5100a23b4b4019392ac2f1a0325af3288427c96909183fa3e3a2111689d3f349d187b88ecd456e02a4796256c8d3f81e288dd65464e5ba623e83a48
7
- data.tar.gz: b6191f0ee41f5732e0706b20b44ce74627d1ff9a5683447b57abe8ffe0ea351d9bee39df4e1b4f755654722547b814d572689c2d1cdf73b22a697962a5eac2d0
6
+ metadata.gz: 1ad6f6555bbdb19650f16220dedc81c733f847931a6eea27e28e69c9050f6542d28b8295a6ef3395c95bfe5480c22cc7e1ea224dc687616634ad34da58a71ddf
7
+ data.tar.gz: 53d42d543a9c86fd2ab3842bad841e7aff95cdc57059bbade5529183df3fa3ee426525d1100174d22821644c5f3915b0bef4e4c252ea56d4c08352bc662aa00f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.0] - (in beta)
4
+
5
+ ### Breaking changes
6
+
7
+ - `#encoded_id` now defaults to returning an 'annotated' ID, one in which a prefix is added to the encoded ID to indicate
8
+ the 'type' of the record the ID represents. This can be disabled. IDs generated by older versions of this gem will
9
+ decode correctly. But not that IDs generated by this version onwards will not decode correctly by older versions of this
10
+ gem so make sure to disable annotation if you need to support older versions of this gem.
11
+ - `#name_for_encoded_id_slug` no longer provides a default implementation, it is up to the user to define this method,
12
+ or configure the gem to use a different method name.
13
+ - `#slugged_encoded_id` no longer takes a `with:` parameter. To specify the name of the method to call to generate the
14
+ slug, use the `slug_value_method_name` configuration option.
15
+
16
+ ### Added
17
+
18
+ - `#encoded_id_hash` has been added to return only the encoded ID without an annotation prefix. If annotation is disabled,
19
+ this method is basically an alias to `#encoded_id`.
20
+ - `.find_all_by_encoded_id` has been added to return all records matching the given encoded ID. This will return all
21
+ matching records whose IDs are encoded in the encoded_id. Missing records are ignored.
22
+ - `.find_all_by_encoded_id!` like `.find_all_by_encoded_id` but raises an `ActiveRecord::RecordNotFound` exception if
23
+ *any* of the records are not found.
24
+
25
+
3
26
  ## [0.6.2] - 2023-02-09
4
27
 
5
28
  - Fix `encoded_id` memoization clearing when record is duplicated
data/Gemfile CHANGED
@@ -10,9 +10,9 @@ group :development, :test do
10
10
 
11
11
  gem "minitest", "~> 5.0"
12
12
 
13
- gem "standard", "~> 1.3"
13
+ gem "standard", "~> 1.30"
14
14
 
15
- gem "steep", "~> 1.2"
15
+ gem "steep", "~> 1.5"
16
16
 
17
17
  gem "sqlite3", "~> 1.5"
18
18
  end
data/README.md CHANGED
@@ -1,30 +1,19 @@
1
1
  # EncodedId::Rails (`encoded_id-rails`)
2
2
 
3
- [EncodedId](https://github.com/stevegeek/encoded_id) for Rails and `ActiveRecord` models.
3
+ `EncodedId::Rails` lets you turn numeric or hex **IDs into reversible and human friendly obfuscated strings**. The gem brings [EncodedId](https://github.com/stevegeek/encoded_id) to Rails and `ActiveRecord` models.
4
4
 
5
- EncodedID lets you turn numeric or hex IDs into reversible and human friendly obfuscated strings.
5
+ You can use it in routes for example, to go from something like `/users/725` to `/users/bob-smith--usr_p5w9-z27j` with miminal effort.
6
6
 
7
- ```ruby
8
- class User < ApplicationRecord
9
- include EncodedId::Model
10
-
11
- def name_for_encoded_id_slug
12
- full_name
13
- end
14
- end
15
-
16
- user = User.find_by_encoded_id("p5w9-z27j") # => #<User id: 78>
17
- user.encoded_id # => "p5w9-z27j"
18
- user.slugged_encoded_id # => "bob-smith--p5w9-z27j"
19
- ```
7
+ ## Features
20
8
 
21
- # Features
9
+ Under the hood it uses hashIds, but it offers more features.
22
10
 
23
- - encoded IDs are reversible (see [`encoded_id`](https://github.com/stevegeek/encoded_id))
24
- - supports slugged IDs (eg `my-cool-product-name--p5w9-z27j`) that are URL friendly (assuming your alphabet is too)
25
- - encoded string can be split into groups of letters to improve human-readability (eg `abcd-efgh`)
26
- - supports multiple IDs encoded in one encoded string (eg imagine the encoded ID `7aq60zqw` might decode to two IDs `[78, 45]`)
27
- - supports custom alphabets for the encoded string (at least 16 characters needed)
11
+ - 🔄 encoded IDs are reversible (see [`encoded_id`](https://github.com/stevegeek/encoded_id))
12
+ - 💅 supports slugged IDs (eg `my-cool-product-name--p5w9-z27j`) that are URL friendly (assuming your alphabet is too)
13
+ - 🔖 supports annotated IDs to help identify the model the encoded ID belongs to (eg for a `User` the encoded ID might be `user_p5w9-z27j`)
14
+ - 👓 encoded string can be split into groups of letters to improve human-readability (eg `abcd-efgh`)
15
+ - 👥 supports multiple IDs encoded in one encoded string (eg imagine the encoded ID `7aq60zqw` might decode to two IDs `[78, 45]`)
16
+ - 🔡 supports custom alphabets for the encoded string (at least 16 characters needed)
28
17
  - by default uses a variation of the Crockford reduced character set (https://www.crockford.com/base32.html)
29
18
  - easily confused characters (eg i and j, 0 and O, 1 and I etc) are mapped to counterpart characters, to
30
19
  help avoid common readability mistakes when reading/sharing
@@ -36,24 +25,55 @@ The gem provides:
36
25
  or query by encoded IDs
37
26
  - sensible defaults to allow you to get started out of the box
38
27
 
39
- ### Coming in future (?)
28
+ ```ruby
29
+ class User < ApplicationRecord
30
+ include EncodedId::Model
31
+
32
+ # An optional slug for the encoded ID string. This is prepended to the encoded ID string, and is solely
33
+ # to make the ID human friendly, or useful in URLs. It is not required for finding records by encoded ID.
34
+ def name_for_encoded_id_slug
35
+ full_name
36
+ end
37
+
38
+ # An optional prefix on the encoded ID string to help identify the model it belongs to.
39
+ # Default is to use model's parameterized name, but can be overridden, or disabled.
40
+ # Note it is not required for finding records by encoded ID.
41
+ def annotation_for_encoded_id
42
+ "usr"
43
+ end
44
+ end
45
+
46
+ # You can find by the encoded ID
47
+ user = User.find_by_encoded_id("p5w9-z27j") # => #<User id: 78>
48
+ user.encoded_id # => "usr_p5w9-z27j"
49
+ user.slugged_encoded_id # => "bob-smith--usr_p5w9-z27j"
40
50
 
41
- - support for UUIDs for IDs (which will be encoded as an array of integers)
51
+ # You can find by a slugged & annotated encoded ID
52
+ user == User.find_by_encoded_id("bob-smith--usr_p5w9-z27j") # => true
42
53
 
43
- # Why this gem?
54
+ # Encoded IDs can encode multiple IDs at the same time
55
+ users = User.find_all_by_encoded_id("7aq60zqw") # => [#<User id: 78>, #<User id: 45>]
56
+ ```
57
+
58
+ ## Why this gem?
44
59
 
45
60
  With this gem you can easily obfuscate your IDs in your URLs, and still be able to find records by using
46
61
  the encoded IDs. The encoded IDs are meant to be somewhat human friendly, to make communication easier
47
62
  when sharing encoded IDs with other people.
48
63
 
49
64
  * Hashids are reversible, no need to persist the generated Id
50
- * we don't override any AR methods. `encoded_id`s are intentionally not interchangeable with normal record `id`s
65
+ * we don't override any AR methods. `encoded_id`s are intentionally **not interchangeable** with normal record `id`s
51
66
  (ie you can't use `.find` to find by encoded ID or record ID, you must be explicit)
52
67
  * we support slugged IDs (eg `my-amazing-product--p5w9-z27j`)
53
68
  * we support multiple model IDs encoded in one `EncodedId` (eg `7aq6-0zqw` might decode to `[78, 45]`)
54
69
  * the gem is configurable
55
70
  * encoded IDs can be stable across environments, or not (you can set the salt to different values per environment)
56
71
 
72
+
73
+ ## Coming in future (?)
74
+
75
+ - support for UUIDs for IDs (which will be encoded as an array of integers)
76
+
57
77
  ## Installation
58
78
 
59
79
  Install the gem and add to the application's Gemfile by executing:
@@ -135,6 +155,8 @@ user = User.find_by_encoded_id("p5w9-z27j") # => #<User id: 78>
135
155
  user.encoded_id # => "p5w9-z27j"
136
156
  ```
137
157
 
158
+ Note when an encoded ID string contains multiple IDs, this method will return the record for the first ID.
159
+
138
160
  ### `.find_by_encoded_id!`
139
161
 
140
162
  Like `.find!` but accepts an encoded ID string instead of an ID. Raises `ActiveRecord::RecordNotFound` if no record is found.
@@ -146,6 +168,18 @@ user = User.find_by_encoded_id!("p5w9-z27j") # => #<User id: 78>
146
168
  user = User.find_by_encoded_id!("encoded-id-that-is-not-found") # => ActiveRecord::RecordNotFound
147
169
  ```
148
170
 
171
+ Note when an encoded ID string contains multiple IDs, this method will return the record for the first ID.
172
+
173
+ ### `.find_all_by_encoded_id`
174
+
175
+ Like `.find_by_encoded_id` but when an encoded ID string contains multiple IDs,
176
+ this method will return an array of records.
177
+
178
+ ### `.find_all_by_encoded_id!`
179
+
180
+ Like `.find_by_encoded_id!` but when an encoded ID string contains multiple IDs,
181
+ this method will return an array of records.
182
+
149
183
  ### `.where_encoded_id`
150
184
 
151
185
  A helper for creating relations. Decodes the encoded ID string before passing it to `.where`.
@@ -198,44 +232,90 @@ end
198
232
  User.encoded_id_salt # => "my-user-model-salt"
199
233
  ```
200
234
 
235
+ ### `#encoded_id_hash`
236
+
237
+ Returns only the encoded 'hashId' part of the encoded ID for the record:
238
+
239
+ ```ruby
240
+ user = User.create(name: "Bob Smith")
241
+ user.encoded_id # => "p5w9-z27j"
242
+ ```
243
+
244
+
201
245
  ### `#encoded_id`
202
246
 
203
- Use the `encoded_id` instance method to get the encoded ID for the record:
247
+ Returns the encoded ID for the record, with an annotation (if it is enabled):
204
248
 
205
249
  ```ruby
206
250
  user = User.create(name: "Bob Smith")
251
+ user.encoded_id # => "user_p5w9-z27j"
252
+ ```
253
+
254
+ By default, the annotation comes from the underscored model name. However, you can change this by either:
255
+
256
+ - overriding `#annotation_for_encoded_id` on the model
257
+ - overriding `#annotation_for_encoded_id` on all models via your `ApplicationRecord`
258
+ - change the method called to get the annotation via setting the `annotation_method_name` config options in your initializer
259
+ - disable the annotation via setting the `annotation_method_name` config options in your initializer to `nil`
260
+
261
+
262
+ Examples:
263
+
264
+ ```ruby
265
+ EncodedId::Rails.configuration.annotation_method_name = :name
266
+ user.encoded_id # => "bob_smith_p5w9-z27j"
267
+ ```
268
+
269
+ ```ruby
270
+ EncodedId::Rails.configuration.annotation_method_name = nil
207
271
  user.encoded_id # => "p5w9-z27j"
208
272
  ```
209
273
 
274
+ ```ruby
275
+ class User < ApplicationRecord
276
+ include EncodedId::Model
277
+
278
+ def annotation_for_encoded_id
279
+ "foo"
280
+ end
281
+ end
282
+
283
+ user = User.create(name: "Bob Smith")
284
+ user.encoded_id # => "foo_p5w9-z27j"
285
+ ```
286
+
287
+ Note that you can also configure the annotation separator via the `annotated_id_separator` config option in your initializer,
288
+ but it must be set to a string that only contains character that are not part of the alphabet used to encode the ID.
289
+
290
+ ```ruby
291
+ EncodedId::Rails.configuration.annotated_id_separator = "^^"
292
+ user.encoded_id # => "foo^^p5w9-z27j"
293
+ ```
210
294
 
211
295
  ### `#slugged_encoded_id`
212
296
 
213
297
  Use the `slugged_encoded_id` instance method to get the slugged version of the encoded ID for the record.
214
- Calls `#name_for_encoded_id_slug` on the record to get the slug part of the encoded ID:
215
298
 
216
299
  ```ruby
217
300
  user = User.create(name: "Bob Smith")
218
301
  user.slugged_encoded_id # => "bob-smith--p5w9-z27j"
219
302
  ```
220
303
 
221
- ### `#name_for_encoded_id_slug`
222
-
223
- Use `#name_for_encoded_id_slug` to specify what will be used to create the slug part of the encoded ID.
224
- By default it calls `#name` on the instance, or if the instance does not respond to
225
- `name` (or the value returned is blank) then uses the Model name.
304
+ Calls `#name_for_encoded_id_slug` on the record to get the slug part of the encoded ID.
305
+ By default, `#name_for_encoded_id_slug` raises, and must be overridden, or configured via the `slug_value_method_name` config option in your initializer:
226
306
 
227
307
  ```ruby
228
308
  class User < ApplicationRecord
229
309
  include EncodedId::Model
230
310
 
231
- # If User has an attribute `name`, that will be used for the slug,
232
- # otherwise `user` will be used as determined by the class name.
311
+ # Assuming user has a name attribute
312
+ def name_for_encoded_id_slug
313
+ name
314
+ end
233
315
  end
234
316
 
235
317
  user = User.create(name: "Bob Smith")
236
318
  user.slugged_encoded_id # => "bob-smith--p5w9-z27j"
237
- user2 = User.create(name: "")
238
- user2.slugged_encoded_id # => "user--i74r-bn28"
239
319
  ```
240
320
 
241
321
  You can optionally override this method to define your own slug:
@@ -253,6 +333,21 @@ user = User.create(superhero_name: "Super Dev")
253
333
  user.slugged_encoded_id # => "super-dev--37nw-8nh7"
254
334
  ```
255
335
 
336
+ Configure the method called by setting the `slug_value_method_name` config option in your initializer:
337
+
338
+ ```ruby
339
+ EncodedId::Rails.configuration.slug_value_method_name = :name
340
+ user.slugged_encoded_id # => "bob-smith--p5w9-z27j"
341
+ ```
342
+
343
+ Note that you can also configure the slug separator via the `slugged_id_separator` config option in your initializer,
344
+ but it must be set to a string that only contains character that are not part of the alphabet used to encode the ID.
345
+
346
+ ```ruby
347
+ EncodedId::Rails.configuration.annotated_id_separator = "***"
348
+ user.encoded_id # => "bob-smith***p5w9-z27j"
349
+ ```
350
+
256
351
  ## To use on all models
257
352
 
258
353
  Simply add the mixin to your `ApplicationRecord`:
@@ -268,6 +363,34 @@ end
268
363
 
269
364
  However, I recommend you only use it on the models that need it.
270
365
 
366
+ ## Example usage for a route and controller
367
+
368
+ ```ruby
369
+ # Route
370
+ resources :users, param: :encoded_id, only: [:show]
371
+ ```
372
+
373
+ ```ruby
374
+ # Model
375
+ class User < ApplicationRecord
376
+ include EncodedId::Model
377
+ include EncodedId::PathParam
378
+ end
379
+ ```
380
+
381
+ ```ruby
382
+ # Controller
383
+ class UsersController < ApplicationController
384
+ def show
385
+ @user = User.find_by_encoded_id!(params[:encoded_id])
386
+ end
387
+ end
388
+ ```
389
+
390
+ ```erb
391
+ <%= link_to "User", user_path %>
392
+ ```
393
+
271
394
  ## Development
272
395
 
273
396
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Steepfile CHANGED
@@ -1,6 +1,4 @@
1
1
  target :lib do
2
2
  check "lib/encoded_id"
3
3
  signature "sig"
4
-
5
- library "encoded_id"
6
4
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module EncodedId
6
+ module Rails
7
+ class AnnotatedId
8
+ def initialize(annotation:, id_part:, separator: "_")
9
+ @annotation = annotation
10
+ @id_part = id_part
11
+ @separator = separator
12
+ end
13
+
14
+ def annotated_id
15
+ unless @id_part.present? && @annotation.present?
16
+ raise ::StandardError, "The model does not provide a valid ID and/or annotation"
17
+ end
18
+ "#{@annotation.to_s.parameterize}#{CGI.escape(@separator)}#{@id_part}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedId
4
+ module Rails
5
+ class AnnotatedIdParser
6
+ def initialize(annotated_id, separator: "_")
7
+ if separator && annotated_id.include?(separator)
8
+ parts = annotated_id.split(separator)
9
+ @id = parts.last
10
+ @annotation = parts[0..-2]&.join(separator)
11
+ else
12
+ @id = annotated_id
13
+ end
14
+ end
15
+
16
+ attr_reader :annotation, :id
17
+ end
18
+ end
19
+ end
@@ -4,19 +4,46 @@ module EncodedId
4
4
  module Rails
5
5
  # Configuration class for initializer
6
6
  class Configuration
7
- attr_accessor :salt,
8
- :character_group_size,
9
- :group_separator,
10
- :alphabet,
11
- :id_length,
12
- :slugged_id_separator
7
+ attr_accessor :salt, :character_group_size, :alphabet, :id_length
8
+ attr_accessor :slug_value_method_name, :annotation_method_name
9
+ attr_reader :group_separator, :slugged_id_separator, :annotated_id_separator
13
10
 
14
11
  def initialize
15
12
  @character_group_size = 4
16
13
  @group_separator = "-"
17
14
  @alphabet = ::EncodedId::Alphabet.modified_crockford
18
15
  @id_length = 8
16
+ @slug_value_method_name = :name_for_encoded_id_slug
19
17
  @slugged_id_separator = "--"
18
+ @annotation_method_name = :annotation_for_encoded_id
19
+ @annotated_id_separator = "_"
20
+ end
21
+
22
+ # Perform validation vs alphabet on these assignments
23
+
24
+ def group_separator=(value)
25
+ unless valid_separator?(value, alphabet)
26
+ raise ArgumentError, "Group separator characters must not be part of the alphabet"
27
+ end
28
+ @group_separator = value
29
+ end
30
+
31
+ def slugged_id_separator=(value)
32
+ if value.blank? || value == group_separator || !valid_separator?(value, alphabet)
33
+ raise ArgumentError, "Slugged ID separator characters must not be part of the alphabet or the same as the group separator"
34
+ end
35
+ @slugged_id_separator = value
36
+ end
37
+
38
+ def annotated_id_separator=(value)
39
+ if value.blank? || value == group_separator || !valid_separator?(value, alphabet)
40
+ raise ArgumentError, "Annotated ID separator characters must not be part of the alphabet or the same as the group separator"
41
+ end
42
+ @annotated_id_separator = value
43
+ end
44
+
45
+ def valid_separator?(separator, characters)
46
+ separator.chars.none? { |v| characters.include?(v) }
20
47
  end
21
48
  end
22
49
  end
@@ -10,7 +10,8 @@ module EncodedId
10
10
 
11
11
  def decode_encoded_id(slugged_encoded_id, options = {})
12
12
  return if slugged_encoded_id.blank?
13
- encoded_id = encoded_id_parser(slugged_encoded_id).id
13
+ annotated_encoded_id = SluggedIdParser.new(slugged_encoded_id, separator: EncodedId::Rails.configuration.slugged_id_separator).id
14
+ encoded_id = AnnotatedIdParser.new(annotated_encoded_id, separator: EncodedId::Rails.configuration.annotated_id_separator).id
14
15
  return if !encoded_id || encoded_id.blank?
15
16
  encoded_id_coder(options).decode(encoded_id)
16
17
  end
@@ -21,10 +22,6 @@ module EncodedId
21
22
  EncodedId::Rails::Salt.new(self, EncodedId::Rails.configuration.salt).generate!
22
23
  end
23
24
 
24
- def encoded_id_parser(slugged_encoded_id)
25
- SluggedIdParser.new(slugged_encoded_id, separator: EncodedId::Rails.configuration.slugged_id_separator)
26
- end
27
-
28
25
  def encoded_id_coder(options = {})
29
26
  config = EncodedId::Rails.configuration
30
27
  EncodedId::Rails::Coder.new(
@@ -4,24 +4,38 @@ module EncodedId
4
4
  module Rails
5
5
  module FinderMethods
6
6
  # Find by encoded ID and optionally ensure record ID is the same as constraint (can be slugged)
7
- def find_by_encoded_id(slugged_encoded_id, with_id: nil)
8
- decoded_id = decode_encoded_id(slugged_encoded_id)
9
- return if decoded_id.blank?
10
- record = find_by(id: decoded_id)
7
+ def find_by_encoded_id(encoded_id, with_id: nil)
8
+ decoded_id = decode_encoded_id(encoded_id)
9
+ return if decoded_id.nil? || decoded_id.blank?
10
+ record = find_by(id: decoded_id.first)
11
11
  return unless record
12
12
  return if with_id && with_id != record.send(:id)
13
13
  record
14
14
  end
15
15
 
16
- def find_by_encoded_id!(slugged_encoded_id, with_id: nil)
17
- decoded_id = decode_encoded_id(slugged_encoded_id)
18
- raise ActiveRecord::RecordNotFound if decoded_id.blank?
19
- record = find_by(id: decoded_id)
16
+ def find_by_encoded_id!(encoded_id, with_id: nil)
17
+ decoded_id = decode_encoded_id(encoded_id)
18
+ raise ActiveRecord::RecordNotFound if decoded_id.nil? || decoded_id.blank?
19
+ record = find_by(id: decoded_id.first)
20
20
  if !record || (with_id && with_id != record.send(:id))
21
21
  raise ActiveRecord::RecordNotFound
22
22
  end
23
23
  record
24
24
  end
25
+
26
+ def find_all_by_encoded_id(encoded_id)
27
+ decoded_ids = decode_encoded_id(encoded_id)
28
+ return if decoded_ids.blank?
29
+ where(id: decoded_ids).to_a
30
+ end
31
+
32
+ def find_all_by_encoded_id!(encoded_id)
33
+ decoded_ids = decode_encoded_id(encoded_id)
34
+ raise ActiveRecord::RecordNotFound if decoded_ids.nil? || decoded_ids.blank?
35
+ records = where(id: decoded_ids).to_a
36
+ raise ActiveRecord::RecordNotFound if records.blank? || records.size != decoded_ids.size
37
+ records
38
+ end
25
39
  end
26
40
  end
27
41
  end
@@ -12,35 +12,51 @@ module EncodedId
12
12
  base.extend(QueryMethods)
13
13
  end
14
14
 
15
+ def encoded_id_hash
16
+ return unless id
17
+ return @encoded_id_hash if defined?(@encoded_id_hash) && !id_changed?
18
+ self.class.encode_encoded_id(id)
19
+ end
20
+
15
21
  def encoded_id
16
22
  return unless id
17
23
  return @encoded_id if defined?(@encoded_id) && !id_changed?
18
- @encoded_id = self.class.encode_encoded_id(id)
24
+ encoded = encoded_id_hash
25
+ annotated_by = EncodedId::Rails.configuration.annotation_method_name
26
+ return @encoded_id = encoded unless annotated_by && encoded
27
+ separator = EncodedId::Rails.configuration.annotated_id_separator
28
+ @encoded_id = EncodedId::Rails::AnnotatedId.new(id_part: encoded, annotation: send(annotated_by.to_s), separator: separator).annotated_id
19
29
  end
20
30
 
21
- def slugged_encoded_id(with: :name_for_encoded_id_slug)
31
+ def slugged_encoded_id
22
32
  return unless id
23
33
  return @slugged_encoded_id if defined?(@slugged_encoded_id) && !id_changed?
24
- @slugged_encoded_id = EncodedId::Rails::SluggedId.new(
25
- self,
26
- slug_method: with,
27
- id_method: :encoded_id,
28
- separator: EncodedId::Rails.configuration.slugged_id_separator
29
- ).slugged_id
34
+ with = EncodedId::Rails.configuration.slug_value_method_name
35
+ separator = EncodedId::Rails.configuration.slugged_id_separator
36
+ encoded = encoded_id
37
+ return unless encoded
38
+ @slugged_encoded_id = EncodedId::Rails::SluggedId.new(id_part: encoded, slug_part: send(with.to_s), separator: separator).slugged_id
39
+ end
40
+
41
+ # By default the annotation is the model name (it will be parameterized)
42
+ def annotation_for_encoded_id
43
+ name = self.class.name
44
+ raise StandardError, "The default annotation requires the model class to have a name" if name.nil?
45
+ name.underscore
30
46
  end
31
47
 
32
- # By default slug created from class name, but can be overridden
48
+ # By default trying to generate a slug without defining how will raise.
49
+ # You either override this method per model, pass an alternate method name to
50
+ # #slugged_encoded_id or setup an alias to another model method in your ApplicationRecord class
33
51
  def name_for_encoded_id_slug
34
- class_name = self.class.name
35
- raise StandardError, "Class must have a `name`, cannot create a slug" if !class_name || class_name.blank?
36
- class_name.underscore
52
+ raise StandardError, "You must define a method to generate the slug for the encoded ID of #{self.class.name}"
37
53
  end
38
54
 
39
55
  # When duplicating an ActiveRecord object, we want to reset the memoized encoded_id
40
56
  def dup
41
57
  super.tap do |new_record|
42
58
  new_record.send(:remove_instance_variable, :@encoded_id) if new_record.instance_variable_defined?(:@encoded_id)
43
- new_record.send(:remove_instance_variable, :@slugged_encoded_id) if new_record.instance_variable_defined?(:@slugged_encoded_id)
59
+ new_record.send(:remove_instance_variable, :@slugged_encoded_id) if new_record.instance_variable_defined?(:@slugged_encoded_id)
44
60
  end
45
61
  end
46
62
  end
@@ -7,7 +7,7 @@ module EncodedId
7
7
  module Rails
8
8
  module PathParam
9
9
  def to_param
10
- encoded_id
10
+ encoded_id || raise(StandardError, "Cannot create path param for #{self.class.name} without an encoded id")
11
11
  end
12
12
  end
13
13
  end
@@ -5,20 +5,17 @@ require "cgi"
5
5
  module EncodedId
6
6
  module Rails
7
7
  class SluggedId
8
- def initialize(from_object, slug_method: :name_for_encoded_id_slug, id_method: :id, separator: "--")
9
- @from_object = from_object
10
- @slug_method = slug_method
11
- @id_method = id_method
8
+ def initialize(slug_part:, id_part:, separator: "--")
9
+ @slug_part = slug_part
10
+ @id_part = id_part
12
11
  @separator = separator
13
12
  end
14
13
 
15
14
  def slugged_id
16
- slug_part = @from_object.send(@slug_method)
17
- id_part = @from_object.send(@id_method)
18
- unless id_part.present? && slug_part.present?
19
- raise ::StandardError, "The model does not return a valid ID (:#{@id_method}) and/or slug (:#{@slug_method})"
15
+ unless @id_part.present? && @slug_part.present?
16
+ raise ::StandardError, "The model does not return a valid ID and/or slug"
20
17
  end
21
- "#{slug_part.to_s.parameterize}#{CGI.escape(@separator)}#{id_part}"
18
+ "#{@slug_part.to_s.parameterize}#{CGI.escape(@separator)}#{@id_part}"
22
19
  end
23
20
  end
24
21
  end
@@ -7,7 +7,7 @@ module EncodedId
7
7
  module Rails
8
8
  module SluggedPathParam
9
9
  def to_param
10
- slugged_encoded_id
10
+ slugged_encoded_id || raise(StandardError, "Cannot create path param for #{self.class.name} without an encoded id")
11
11
  end
12
12
  end
13
13
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module EncodedId
4
4
  module Rails
5
- VERSION = "0.6.2"
5
+ VERSION = "1.0.0.beta2"
6
6
  end
7
7
  end
@@ -5,6 +5,8 @@ require_relative "rails/configuration"
5
5
  require_relative "rails/coder"
6
6
  require_relative "rails/slugged_id"
7
7
  require_relative "rails/slugged_id_parser"
8
+ require_relative "rails/annotated_id"
9
+ require_relative "rails/annotated_id_parser"
8
10
  require_relative "rails/salt"
9
11
  require_relative "rails/encoder_methods"
10
12
  require_relative "rails/query_methods"
@@ -41,4 +41,30 @@ EncodedId::Rails.configure do |config|
41
41
  # Default: 8
42
42
  #
43
43
  # config.id_length = 8
44
+
45
+ # The name of the method that returns the value to be used in the slug.
46
+ #
47
+ # Default: :name_for_encoded_id_slug
48
+ #
49
+ # config.slug_value_method_name = :name_for_encoded_id_slug
50
+
51
+ # The separator used between the slug and the encoded ID.
52
+ # `nil` disables grouping.
53
+ #
54
+ # Default: "--"
55
+ #
56
+ # config.slugged_id_separator = "--"
57
+
58
+ # The name of the method that returns the annotation to be used in the annotated ID.
59
+ #
60
+ # Default: :annotation_for_encoded_id
61
+ #
62
+ # config.annotation_method_name = :annotation_for_encoded_id
63
+
64
+ # The separator used between the annotation and the encoded ID.
65
+ # `nil` disables annotation.
66
+ #
67
+ # Default: "_"
68
+ #
69
+ # config.annotated_id_separator = "_"
44
70
  end
data/rbs_collection.yaml CHANGED
@@ -9,8 +9,6 @@ sources:
9
9
  path: .gem_rbs_collection
10
10
 
11
11
  gems:
12
- - name: encoded_id-rails
13
- ignore: true
14
12
  - name: cgi
15
13
  # Skip loading rbs gem's RBS.
16
14
  # It's unnecessary if you don't use rbs as a library.
@@ -8,7 +8,10 @@ module EncodedId
8
8
  attr_accessor character_group_size: ::Integer
9
9
  attr_accessor alphabet: ::EncodedId::Alphabet
10
10
  attr_accessor id_length: ::Integer
11
+ attr_accessor slug_value_method_name: ::Symbol
11
12
  attr_accessor slugged_id_separator: ::String
13
+ attr_accessor annotation_method_name: ::Symbol
14
+ attr_accessor annotated_id_separator: ::String
12
15
 
13
16
  def initialize: () -> void
14
17
  end
@@ -43,28 +46,41 @@ module EncodedId
43
46
  end
44
47
 
45
48
  class SluggedId
46
- def initialize: (untyped from_object, ?slug_method: ::Symbol, ?id_method: ::Symbol, ?separator: ::String)-> void
47
-
48
- @from_object: untyped
49
- @slug_method: ::Symbol
50
- @id_method: ::Symbol
49
+ def initialize: (slug_part: ::String, id_part: ::String, ?separator: ::String)-> void
50
+ @slug_part: ::String
51
+ @id_part: ::String
51
52
  @separator: ::String
52
53
 
53
54
  def slugged_id: -> ::String
54
55
  end
55
56
 
57
+ class AnnotatedId
58
+ def initialize: (annotation: ::String, id_part: ::String, ?separator: ::String)-> void
59
+ @annotation: ::String
60
+ @id_part: ::String
61
+ @separator: ::String
62
+
63
+ def annotated_id: -> ::String
64
+ end
65
+
56
66
  class SluggedIdParser
57
67
  def initialize: (::String slugged_id, ?separator: ::String) -> void
58
68
 
59
69
  attr_reader slug: ::String?
60
- attr_reader id: ::String?
70
+ attr_reader id: ::String
71
+ end
72
+
73
+ class AnnotatedIdParser
74
+ def initialize: (::String annotated_id, ?separator: ::String) -> void
75
+
76
+ attr_reader annotation: ::String?
77
+ attr_reader id: ::String
61
78
  end
62
79
 
63
80
  module EncoderMethods
64
- def encode_encoded_id: (untyped id, ?::Hash[::Symbol, untyped] options) -> ::String
81
+ def encode_encoded_id: (::Array[::Integer] | ::Integer id, ?::Hash[::Symbol, untyped] options) -> ::String
65
82
  def decode_encoded_id: (::String slugged_encoded_id, ?::Hash[::Symbol, untyped] options) -> ::Array[::Integer]?
66
83
  def encoded_id_salt: () -> ::String
67
- def encoded_id_parser: (::String slugged_encoded_id) -> ::EncodedId::Rails::SluggedIdParser
68
84
  def encoded_id_coder: (?::Hash[::Symbol, untyped] options) -> ::EncodedId::Rails::Coder
69
85
  end
70
86
 
@@ -72,9 +88,11 @@ module EncodedId
72
88
  def find_by: (*untyped) -> (nil | untyped)
73
89
  end
74
90
 
75
- module FinderMethods : EncoderMethods, _ActiveRecordFinderMethod
76
- def find_by_encoded_id: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> untyped?
77
- def find_by_encoded_id!: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> untyped
91
+ module FinderMethods : EncoderMethods, _ActiveRecordFinderMethod, _ActiveRecordQueryMethod
92
+ def find_by_encoded_id: (::String encoded_id, ?with_id: ::Symbol?) -> untyped?
93
+ def find_by_encoded_id!: (::String encoded_id, ?with_id: ::Symbol?) -> untyped
94
+ def find_all_by_encoded_id: (::String encoded_id) -> untyped?
95
+ def find_all_by_encoded_id!: (::String encoded_id) -> untyped
78
96
  end
79
97
 
80
98
  interface _ActiveRecordQueryMethod
@@ -86,9 +104,12 @@ module EncodedId
86
104
  end
87
105
 
88
106
  module Model : ActiveRecord::Base
107
+ # From ActiveRecord
89
108
  extend ActiveRecord::FinderMethods
90
109
  extend ActiveRecord::QueryMethods
110
+ def id_changed?: -> bool
91
111
 
112
+ # From EncodedId::Rails::Model
92
113
  extend EncoderMethods
93
114
  extend FinderMethods
94
115
  extend QueryMethods
@@ -96,13 +117,15 @@ module EncodedId
96
117
  @encoded_id: ::String
97
118
  @slugged_encoded_id: ::String
98
119
 
99
- def encoded_id: () -> ::String
100
- def slugged_encoded_id: (?with: ::Symbol) -> ::String
101
- def name_for_encoded_id_slug: () -> ::String
120
+ def encoded_id_hash: -> ::String?
121
+ def encoded_id: -> ::String?
122
+ def slugged_encoded_id: -> ::String?
123
+ def name_for_encoded_id_slug: -> ::String
124
+ def annotation_for_encoded_id: -> ::String
102
125
  end
103
126
 
104
127
  interface _ActiveRecordToParam
105
- def to_param: () -> ::String
128
+ def to_param: -> ::String
106
129
  end
107
130
 
108
131
  module PathParam : Model, _ActiveRecordToParam
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: encoded_id-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 1.0.0.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-09 00:00:00.000000000 Z
11
+ date: 2023-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -56,14 +56,14 @@ dependencies:
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: '0.4'
59
+ version: 1.0.0.rc3
60
60
  type: :runtime
61
61
  prerelease: false
62
62
  version_requirements: !ruby/object:Gem::Requirement
63
63
  requirements:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
- version: '0.4'
66
+ version: 1.0.0.rc3
67
67
  description: ActiveRecord concern to use EncodedID to turn IDs into reversible and
68
68
  human friendly obfuscated strings.
69
69
  email:
@@ -82,6 +82,8 @@ files:
82
82
  - gemfiles/rails_6.0.gemfile
83
83
  - gemfiles/rails_7.0.gemfile
84
84
  - lib/encoded_id/rails.rb
85
+ - lib/encoded_id/rails/annotated_id.rb
86
+ - lib/encoded_id/rails/annotated_id_parser.rb
85
87
  - lib/encoded_id/rails/coder.rb
86
88
  - lib/encoded_id/rails/configuration.rb
87
89
  - lib/encoded_id/rails/encoder_methods.rb
@@ -117,11 +119,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
119
  version: 2.6.0
118
120
  required_rubygems_version: !ruby/object:Gem::Requirement
119
121
  requirements:
120
- - - ">="
122
+ - - ">"
121
123
  - !ruby/object:Gem::Version
122
- version: '0'
124
+ version: 1.3.1
123
125
  requirements: []
124
- rubygems_version: 3.3.26
126
+ rubygems_version: 3.4.20
125
127
  signing_key:
126
128
  specification_version: 4
127
129
  summary: Use `encoded_id` with ActiveRecord models