encoded_id-rails 0.6.2 → 1.0.0.beta2
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 +23 -0
- data/Gemfile +2 -2
- data/README.md +159 -36
- data/Steepfile +0 -2
- data/lib/encoded_id/rails/annotated_id.rb +22 -0
- data/lib/encoded_id/rails/annotated_id_parser.rb +19 -0
- data/lib/encoded_id/rails/configuration.rb +33 -6
- data/lib/encoded_id/rails/encoder_methods.rb +2 -5
- data/lib/encoded_id/rails/finder_methods.rb +22 -8
- data/lib/encoded_id/rails/model.rb +29 -13
- data/lib/encoded_id/rails/path_param.rb +1 -1
- data/lib/encoded_id/rails/slugged_id.rb +6 -9
- data/lib/encoded_id/rails/slugged_path_param.rb +1 -1
- data/lib/encoded_id/rails/version.rb +1 -1
- data/lib/encoded_id/rails.rb +2 -0
- data/lib/generators/encoded_id/rails/templates/encoded_id.rb +26 -0
- data/rbs_collection.yaml +0 -2
- data/sig/encoded_id/rails.rbs +38 -15
- metadata +9 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a92cfd619fa254d0996950214c14aa115d6c62327e4ec2a6715dc9d7d898ebf
|
4
|
+
data.tar.gz: 27b448535392de1e1e21c37b82c324e4b5aecdc5225d4c7b2757a1ce8d5d18a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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)
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
-
|
26
|
-
-
|
27
|
-
- supports
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
#
|
232
|
-
|
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
@@ -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
|
-
|
9
|
-
|
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
|
-
|
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(
|
8
|
-
decoded_id = decode_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!(
|
17
|
-
decoded_id = decode_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
|
-
|
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
|
31
|
+
def slugged_encoded_id
|
22
32
|
return unless id
|
23
33
|
return @slugged_encoded_id if defined?(@slugged_encoded_id) && !id_changed?
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
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
|
-
|
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)
|
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
|
@@ -5,20 +5,17 @@ require "cgi"
|
|
5
5
|
module EncodedId
|
6
6
|
module Rails
|
7
7
|
class SluggedId
|
8
|
-
def initialize(
|
9
|
-
@
|
10
|
-
@
|
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
|
-
|
17
|
-
|
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
|
data/lib/encoded_id/rails.rb
CHANGED
@@ -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
data/sig/encoded_id/rails.rbs
CHANGED
@@ -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: (
|
47
|
-
|
48
|
-
@
|
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: (
|
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
|
77
|
-
def find_by_encoded_id!: (::String
|
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
|
100
|
-
def
|
101
|
-
def
|
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:
|
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.
|
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-
|
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:
|
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:
|
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:
|
124
|
+
version: 1.3.1
|
123
125
|
requirements: []
|
124
|
-
rubygems_version: 3.
|
126
|
+
rubygems_version: 3.4.20
|
125
127
|
signing_key:
|
126
128
|
specification_version: 4
|
127
129
|
summary: Use `encoded_id` with ActiveRecord models
|