encoded_id-rails 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of encoded_id-rails might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Gemfile +7 -5
- data/README.md +206 -18
- data/lib/encoded_id/rails/coder.rb +37 -0
- data/lib/encoded_id/rails/configuration.rb +5 -1
- data/lib/encoded_id/rails/salt.rb +21 -0
- data/lib/encoded_id/rails/slugged_id.rb +25 -0
- data/lib/encoded_id/rails/slugged_id_parser.rb +19 -0
- data/lib/encoded_id/rails/version.rb +1 -1
- data/lib/encoded_id/rails/with_encoded_id.rb +47 -158
- data/lib/encoded_id/rails.rb +4 -0
- data/lib/generators/encoded_id/rails/templates/encoded_id.rb +7 -0
- data/rbs_collection.yaml +1 -0
- data/sig/encoded_id/rails.rbs +70 -73
- metadata +21 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0aceb28e50697bb0257054c2731936a7cad5f6617d479543c63999a156c153bf
|
4
|
+
data.tar.gz: 59be3ddf7327f26726df0aa3120f9a70de0ac225d77291ad8e35b10b937e6264
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 821e8ffcb7f77bdc84160a69bbc504ec891f9b11d925e4aa5d52f1209f426d894671cc603b850ee1a408dfa255c9b00eeaed6938eb1440aec4a31af27209f484
|
7
|
+
data.tar.gz: a168a3da9c882a0e1c9f159cfcfeaeaf02109049aed2f2330b9a51708cee927896a911cae63833ec11955a0139af6aa9a3302a9e970a9f461024d4a870730bf2
|
data/Gemfile
CHANGED
@@ -5,12 +5,14 @@ source "https://rubygems.org"
|
|
5
5
|
# Specify your gem's dependencies in encoded_id-rails.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
|
-
|
8
|
+
group :development, :test do
|
9
|
+
gem "rake", "~> 13.0"
|
9
10
|
|
10
|
-
gem "minitest", "~> 5.0"
|
11
|
+
gem "minitest", "~> 5.0"
|
11
12
|
|
12
|
-
gem "standard", "~> 1.3"
|
13
|
+
gem "standard", "~> 1.3"
|
13
14
|
|
14
|
-
gem "steep", "~> 1.2"
|
15
|
+
gem "steep", "~> 1.2"
|
15
16
|
|
16
|
-
gem "sqlite3", "~> 1.5"
|
17
|
+
gem "sqlite3", "~> 1.5"
|
18
|
+
end
|
data/README.md
CHANGED
@@ -1,41 +1,58 @@
|
|
1
|
-
# EncodedId::Rails
|
1
|
+
# EncodedId::Rails (`encoded_id-rails`)
|
2
2
|
|
3
|
-
EncodedId
|
3
|
+
[EncodedId](https://github.com/stevegeek/encoded_id) for Rails and `ActiveRecord` models.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
TODO: Delete this and the text above, and describe your gem
|
5
|
+
EncodedID lets you turn numeric or hex IDs into reversible and human friendly obfuscated strings.
|
8
6
|
|
9
7
|
```ruby
|
10
8
|
class User < ApplicationRecord
|
11
9
|
include EncodedId::WithEncodedId
|
12
10
|
|
13
|
-
def
|
11
|
+
def name_for_encoded_id_slug
|
14
12
|
full_name.parameterize
|
15
13
|
end
|
16
14
|
end
|
17
15
|
|
18
16
|
user = User.find_by_encoded_id("p5w9-z27j") # => #<User id: 78>
|
19
17
|
user.encoded_id # => "p5w9-z27j"
|
20
|
-
user.
|
18
|
+
user.slugged_encoded_id # => "bob-smith--p5w9-z27j"
|
21
19
|
```
|
20
|
+
|
22
21
|
# Features
|
23
22
|
|
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)
|
28
|
+
- by default uses a variation of the Crockford reduced character set (https://www.crockford.com/base32.html)
|
29
|
+
- easily confused characters (eg i and j, 0 and O, 1 and I etc) are mapped to counterpart characters, to
|
30
|
+
help avoid common readability mistakes when reading/sharing
|
31
|
+
- build in profanity limitation
|
24
32
|
|
33
|
+
The gem provides:
|
25
34
|
|
26
|
-
|
35
|
+
- methods to mixin to ActiveRecord models which will allow you to encode and decode IDs, and find
|
36
|
+
or query by encoded IDs
|
37
|
+
- sensible defaults to allow you to get started out of the box
|
38
|
+
|
39
|
+
### Coming in future (?)
|
27
40
|
|
28
41
|
- support for UUIDs for IDs (which will be encoded as an array of integers)
|
29
42
|
|
30
|
-
|
43
|
+
# Why this gem?
|
44
|
+
|
45
|
+
With this gem you can easily obfuscate your IDs in your URLs, and still be able to find records by using
|
46
|
+
the encoded IDs. The encoded IDs are meant to be somewhat human friendly, to make communication easier
|
47
|
+
when sharing encoded IDs with other people.
|
31
48
|
|
32
49
|
* Hashids are reversible, no need to persist the generated Id
|
33
|
-
* we don't override any methods
|
34
|
-
|
35
|
-
* we support
|
36
|
-
* we
|
37
|
-
|
38
|
-
* can be stable across environments, or not (you can set the salt to different values per environment)
|
50
|
+
* we don't override any AR methods. `encoded_id`s are intentionally not interchangeable with normal record `id`s
|
51
|
+
(ie you can't use `.find` to find by encoded ID or record ID, you must be explicit)
|
52
|
+
* we support slugged IDs (eg `my-amazing-product--p5w9-z27j`)
|
53
|
+
* we support multiple model IDs encoded in one `EncodedId` (eg `7aq6-0zqw` might decode to `[78, 45]`)
|
54
|
+
* the gem is configurable
|
55
|
+
* encoded IDs can be stable across environments, or not (you can set the salt to different values per environment)
|
39
56
|
|
40
57
|
## Installation
|
41
58
|
|
@@ -53,9 +70,165 @@ Then run the generator to add the initializer:
|
|
53
70
|
|
54
71
|
## Usage
|
55
72
|
|
56
|
-
|
73
|
+
### Configuration
|
74
|
+
|
75
|
+
The install generator will create an initializer file [`config/initializers/encoded_id.rb`](https://github.com/stevegeek/encoded_id-rails/blob/main/lib/generators/encoded_id/rails/templates/encoded_id.rb). It is documented
|
76
|
+
and should be self-explanatory.
|
77
|
+
|
78
|
+
You can configure:
|
79
|
+
|
80
|
+
- a global salt needed to generate the encoded IDs (if you dont use a global salt, you can set a salt per model)
|
81
|
+
- the size of the character groups in the encoded string (default is 4)
|
82
|
+
- the separator between the character groups (default is '-')
|
83
|
+
- the alphabet used to generate the encoded string (default is a variation of the Crockford reduced character set)
|
84
|
+
- the minimum length of the encoded ID string (default is 8 characters)
|
85
|
+
|
86
|
+
### ActiveRecord model setup
|
87
|
+
|
88
|
+
Include `EncodedId::WithEncodedId` in your model and optionally specify a encoded id salt (or not if using a global one):
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class User < ApplicationRecord
|
92
|
+
include EncodedId::WithEncodedId
|
93
|
+
|
94
|
+
# and optionally the model's salt
|
95
|
+
def encoded_id_salt
|
96
|
+
"my-user-model-salt"
|
97
|
+
end
|
98
|
+
|
99
|
+
# ...
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
## Documentation
|
104
|
+
|
105
|
+
### `.find_by_encoded_id`
|
106
|
+
|
107
|
+
Like `.find` but accepts an encoded ID string instead of an ID. Will return `nil` if no record is found.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
user = User.find_by_encoded_id("p5w9-z27j") # => #<User id: 78>
|
111
|
+
user.encoded_id # => "p5w9-z27j"
|
112
|
+
```
|
57
113
|
|
58
|
-
###
|
114
|
+
### `.find_by_encoded_id!`
|
115
|
+
|
116
|
+
Like `.find!` but accepts an encoded ID string instead of an ID. Raises `ActiveRecord::RecordNotFound` if no record is found.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
user = User.find_by_encoded_id!("p5w9-z27j") # => #<User id: 78>
|
120
|
+
|
121
|
+
# raises ActiveRecord::RecordNotFound
|
122
|
+
user = User.find_by_encoded_id!("encoded-id-that-is-not-found") # => ActiveRecord::RecordNotFound
|
123
|
+
```
|
124
|
+
|
125
|
+
### `.where_encoded_id`
|
126
|
+
|
127
|
+
A helper for creating relations. Decodes the encoded ID string before passing it to `.where`.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
encoded_id = User.encode_encoded_id([user1.id, user2.id]) # => "p5w9-z27j"
|
131
|
+
User.where(active: true)
|
132
|
+
.where_encoded_id(encoded_id)
|
133
|
+
.map(&:name) # => ["Bob Smith", "Jane Doe"]
|
134
|
+
```
|
135
|
+
|
136
|
+
### `.encode_encoded_id`
|
137
|
+
|
138
|
+
Encodes an ID or array of IDs into an encoded ID string.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
User.encode_encoded_id(78) # => "p5w9-z27j"
|
142
|
+
User.encode_encoded_id([78, 45]) # => "7aq6-0zqw"
|
143
|
+
```
|
144
|
+
|
145
|
+
### `.decode_encoded_id`
|
146
|
+
|
147
|
+
Decodes an encoded ID string into an array of IDs.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
User.decode_encoded_id("p5w9-z27j") # => [78]
|
151
|
+
User.decode_encoded_id("7aq6-0zqw") # => [78, 45]
|
152
|
+
```
|
153
|
+
|
154
|
+
### `.encoded_id_salt`
|
155
|
+
|
156
|
+
Returns the salt used to generate the encoded ID string. If not defined, the global salt is used
|
157
|
+
with `EncodedId::Rails::Salt` to generate a model specific one.
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
User.encoded_id_salt # => "User/the-salt-from-the-initializer"
|
161
|
+
```
|
162
|
+
|
163
|
+
Otherwise override this method to return a salt specific to the model.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
class User < ApplicationRecord
|
167
|
+
include EncodedId::WithEncodedId
|
168
|
+
|
169
|
+
def encoded_id_salt
|
170
|
+
"my-user-model-salt"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
User.encoded_id_salt # => "my-user-model-salt"
|
174
|
+
```
|
175
|
+
|
176
|
+
### `#encoded_id`
|
177
|
+
|
178
|
+
Use the `encoded_id` instance method to get the encoded ID for the record:
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
user = User.create(name: "Bob Smith")
|
182
|
+
user.encoded_id # => "p5w9-z27j"
|
183
|
+
```
|
184
|
+
|
185
|
+
|
186
|
+
### `#slugged_encoded_id`
|
187
|
+
|
188
|
+
Use the `slugged_encoded_id` instance method to get the slugged version of the encoded ID for the record.
|
189
|
+
Calls `#name_for_encoded_id_slug` on the record to get the slug part of the encoded ID:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
user = User.create(name: "Bob Smith")
|
193
|
+
user.slugged_encoded_id # => "bob-smith--p5w9-z27j"
|
194
|
+
```
|
195
|
+
|
196
|
+
### `#name_for_encoded_id_slug`
|
197
|
+
|
198
|
+
Use `#name_for_encoded_id_slug` to specify what will be used to create the slug part of the encoded ID.
|
199
|
+
By default it calls `#name` on the instance, or if the instance does not respond to
|
200
|
+
`name` (or the value returned is blank) then uses the Model name.
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
class User < ApplicationRecord
|
204
|
+
include EncodedId::WithEncodedId
|
205
|
+
|
206
|
+
# If User has an attribute `name`, that will be used for the slug,
|
207
|
+
# otherwise `user` will be used as determined by the class name.
|
208
|
+
end
|
209
|
+
|
210
|
+
user = User.create(name: "Bob Smith")
|
211
|
+
user.slugged_encoded_id # => "bob-smith--p5w9-z27j"
|
212
|
+
user2 = User.create(name: "")
|
213
|
+
user2.slugged_encoded_id # => "user--i74r-bn28"
|
214
|
+
```
|
215
|
+
|
216
|
+
You can optionally override this method to define your own slug:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
class User < ApplicationRecord
|
220
|
+
include EncodedId::WithEncodedId
|
221
|
+
|
222
|
+
def name_for_encoded_id_slug
|
223
|
+
superhero_name
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
user = User.create(superhero_name: "Super Dev")
|
228
|
+
user.slugged_encoded_id # => "super-dev--37nw-8nh7"
|
229
|
+
```
|
230
|
+
|
231
|
+
## To use on all models
|
59
232
|
|
60
233
|
Simply add the mixin to your `ApplicationRecord`:
|
61
234
|
|
@@ -76,9 +249,24 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
76
249
|
|
77
250
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
78
251
|
|
252
|
+
### Type check
|
253
|
+
|
254
|
+
First install dependencies:
|
255
|
+
|
256
|
+
```bash
|
257
|
+
rbs collection install
|
258
|
+
```
|
259
|
+
|
260
|
+
Then run:
|
261
|
+
|
262
|
+
```bash
|
263
|
+
steep check
|
264
|
+
```
|
265
|
+
|
266
|
+
|
79
267
|
## Contributing
|
80
268
|
|
81
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
269
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/stevegeek/encoded_id-rails.
|
82
270
|
|
83
271
|
## License
|
84
272
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EncodedId
|
4
|
+
module Rails
|
5
|
+
class Coder
|
6
|
+
def initialize(salt:, id_length:, character_group_size:, separator:, alphabet:)
|
7
|
+
@salt = salt
|
8
|
+
@id_length = id_length
|
9
|
+
@character_group_size = character_group_size
|
10
|
+
@separator = separator
|
11
|
+
@alphabet = alphabet
|
12
|
+
end
|
13
|
+
|
14
|
+
def encode(id)
|
15
|
+
coder.encode(id)
|
16
|
+
end
|
17
|
+
|
18
|
+
def decode(encoded_id)
|
19
|
+
coder.decode(encoded_id)
|
20
|
+
rescue EncodedId::EncodedIdFormatError
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def coder
|
27
|
+
::EncodedId::ReversibleId.new(
|
28
|
+
salt: @salt,
|
29
|
+
length: @id_length,
|
30
|
+
split_at: @character_group_size,
|
31
|
+
split_with: @separator,
|
32
|
+
alphabet: @alphabet
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -6,13 +6,17 @@ module EncodedId
|
|
6
6
|
class Configuration
|
7
7
|
attr_accessor :salt,
|
8
8
|
:character_group_size,
|
9
|
+
:group_separator,
|
9
10
|
:alphabet,
|
10
|
-
:id_length
|
11
|
+
:id_length,
|
12
|
+
:slugged_id_separator
|
11
13
|
|
12
14
|
def initialize
|
13
15
|
@character_group_size = 4
|
16
|
+
@group_separator = "-"
|
14
17
|
@alphabet = ::EncodedId::Alphabet.modified_crockford
|
15
18
|
@id_length = 8
|
19
|
+
@slugged_id_separator = "--"
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EncodedId
|
4
|
+
module Rails
|
5
|
+
class Salt
|
6
|
+
def initialize(klass, salt)
|
7
|
+
@klass = klass
|
8
|
+
@salt = salt
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate!
|
12
|
+
unless @klass.respond_to?(:name) && @klass.name.present?
|
13
|
+
raise ::StandardError, "The class must have a `name` to ensure encode id uniqueness. " \
|
14
|
+
"Please set a name on the class or override `encoded_id_salt`."
|
15
|
+
end
|
16
|
+
raise ::StandardError, "Encoded ID salt is invalid" if !@salt || @salt.blank? || @salt.size < 4
|
17
|
+
"#{@klass.name}/#{@salt}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cgi"
|
4
|
+
|
5
|
+
module EncodedId
|
6
|
+
module Rails
|
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
|
12
|
+
@separator = separator
|
13
|
+
end
|
14
|
+
|
15
|
+
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})"
|
20
|
+
end
|
21
|
+
"#{slug_part.to_s.parameterize}#{CGI.escape(@separator)}#{id_part}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EncodedId
|
4
|
+
module Rails
|
5
|
+
class SluggedIdParser
|
6
|
+
def initialize(slugged_id, separator: "--")
|
7
|
+
if separator && slugged_id.include?(separator)
|
8
|
+
parts = slugged_id.split(separator)
|
9
|
+
@slug = parts.first
|
10
|
+
@id = parts.last
|
11
|
+
else
|
12
|
+
@id = slugged_id
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :slug, :id
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -13,161 +13,60 @@ module EncodedId
|
|
13
13
|
module ClassMethods
|
14
14
|
# Find by encoded ID and optionally ensure record ID is the same as constraint (can be slugged)
|
15
15
|
def find_by_encoded_id(slugged_encoded_id, with_id: nil)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
decoded_id = decode_encoded_id(slugged_encoded_id)
|
17
|
+
return if decoded_id.blank?
|
18
|
+
record = find_by(id: decoded_id)
|
19
|
+
return unless record
|
20
|
+
return if with_id && with_id != record.send(:id)
|
21
|
+
record
|
20
22
|
end
|
21
23
|
|
22
24
|
def find_by_encoded_id!(slugged_encoded_id, with_id: nil)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
# Find by a fixed slug value (assumed as an attribute value in the DB)
|
30
|
-
def find_by_fixed_slug(slug, attribute: :slug, with_id: nil)
|
31
|
-
find_via_custom_id(slug, attribute, compare_to: with_id)
|
32
|
-
end
|
33
|
-
|
34
|
-
def find_by_fixed_slug!(slug, attribute: :slug, with_id: nil)
|
35
|
-
find_via_custom_id!(slug, attribute, compare_to: with_id)
|
36
|
-
end
|
37
|
-
|
38
|
-
# Find by record ID where the ID has been slugged
|
39
|
-
def find_by_slugged_id(slugged_id, with_id: nil)
|
40
|
-
id_part = decode_slugged_ids(slugged_id)
|
41
|
-
unless with_id.nil?
|
42
|
-
return unless with_id == id_part
|
25
|
+
decoded_id = decode_encoded_id(slugged_encoded_id)
|
26
|
+
raise ActiveRecord::RecordNotFound if decoded_id.blank?
|
27
|
+
record = find_by(id: decoded_id)
|
28
|
+
if !record || (with_id && with_id != record.send(:id))
|
29
|
+
raise ActiveRecord::RecordNotFound
|
43
30
|
end
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
def find_by_slugged_id!(slugged_id, with_id: nil)
|
48
|
-
id_part = decode_slugged_ids(slugged_id)
|
49
|
-
unless with_id.nil?
|
50
|
-
raise ActiveRecord::RecordNotFound unless with_id == id_part
|
51
|
-
end
|
52
|
-
find(id_part)
|
31
|
+
record
|
53
32
|
end
|
54
33
|
|
55
|
-
# relation helpers
|
56
|
-
|
57
34
|
def where_encoded_id(slugged_encoded_id)
|
58
|
-
decoded_id = decode_encoded_id(
|
35
|
+
decoded_id = decode_encoded_id(slugged_encoded_id)
|
59
36
|
raise ActiveRecord::RecordNotFound if decoded_id.nil?
|
60
37
|
where(id: decoded_id)
|
61
38
|
end
|
62
39
|
|
63
|
-
def
|
64
|
-
|
40
|
+
def encode_encoded_id(ids, options = {})
|
41
|
+
raise StandardError, "You must pass an ID or array of IDs" if ids.blank?
|
42
|
+
encoded_id_coder(options).encode(ids)
|
65
43
|
end
|
66
44
|
|
67
|
-
def where_slugged_id(slugged_id)
|
68
|
-
id_part = decode_slugged_ids(slugged_id)
|
69
|
-
raise ActiveRecord::RecordNotFound if id_part.nil?
|
70
|
-
where(id: id_part)
|
71
|
-
end
|
72
|
-
|
73
|
-
# Encode helpers
|
74
|
-
|
75
|
-
def encode_encoded_id(id, options = {})
|
76
|
-
raise StandardError, "You must pass an ID" if id.blank?
|
77
|
-
hash_id_encoder(options).encode(id)
|
78
|
-
end
|
79
|
-
|
80
|
-
def encode_multi_encoded_id(encoded_ids, options = {})
|
81
|
-
raise ::StandardError, "You must pass IDs" if encoded_ids.blank?
|
82
|
-
hash_id_encoder(options).encode(encoded_ids)
|
83
|
-
end
|
84
|
-
|
85
|
-
# Decode helpers
|
86
|
-
|
87
|
-
# Decode a encoded_id (can be slugged)
|
88
45
|
def decode_encoded_id(slugged_encoded_id, options = {})
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
internal_decode_encoded_id(slugged_encoded_id, options)
|
94
|
-
end
|
95
|
-
|
96
|
-
# Decode a Slugged ID
|
97
|
-
def decode_slugged_id(slugged)
|
98
|
-
return if slugged.blank?
|
99
|
-
extract_id_part(slugged).to_i
|
100
|
-
end
|
101
|
-
|
102
|
-
# Decode a set of slugged IDs
|
103
|
-
def decode_slugged_ids(slugged)
|
104
|
-
return if slugged.blank?
|
105
|
-
extract_id_part(slugged).split("-").map(&:to_i)
|
46
|
+
return if slugged_encoded_id.blank?
|
47
|
+
encoded_id = encoded_id_parser(slugged_encoded_id).id
|
48
|
+
return if !encoded_id || encoded_id.blank?
|
49
|
+
encoded_id_coder(options).decode(encoded_id)
|
106
50
|
end
|
107
51
|
|
108
52
|
# This can be overridden in the model to provide a custom salt
|
109
53
|
def encoded_id_salt
|
110
|
-
|
111
|
-
raise ::StandardError, "You must set a model specific encoded_id_salt or a gem wide one"
|
112
|
-
end
|
113
|
-
unless name.present?
|
114
|
-
raise ::StandardError, "The class must have a name to ensure encode id uniqueness. " \
|
115
|
-
"Please set a name on the class or override `encoded_id_salt`."
|
116
|
-
end
|
117
|
-
salt = config.salt
|
118
|
-
raise ::StandardError, "Encoded ID salt is invalid" if salt.blank? || salt.size < 4
|
119
|
-
"#{name}/#{salt}"
|
120
|
-
end
|
121
|
-
|
122
|
-
private
|
123
|
-
|
124
|
-
def hash_id_encoder(options)
|
125
|
-
::EncodedId::ReversibleId.new(
|
126
|
-
salt: options[:salt].presence || encoded_id_salt,
|
127
|
-
length: options[:id_length] || config.id_length,
|
128
|
-
split_at: options[:character_group_size] || config.character_group_size,
|
129
|
-
alphabet: options[:alphabet] || config.alphabet
|
130
|
-
)
|
131
|
-
end
|
132
|
-
|
133
|
-
def config
|
134
|
-
::EncodedId::Rails.configuration
|
54
|
+
EncodedId::Rails::Salt.new(self, EncodedId::Rails.configuration.salt).generate!
|
135
55
|
end
|
136
56
|
|
137
|
-
def
|
138
|
-
|
139
|
-
encoded_id = extract_id_part(slugged_encoded_id)
|
140
|
-
return if encoded_id.blank?
|
141
|
-
hash_id_encoder(options).decode(encoded_id)
|
142
|
-
rescue EncodedId::EncodedIdFormatError
|
143
|
-
nil
|
57
|
+
def encoded_id_parser(slugged_encoded_id)
|
58
|
+
SluggedIdParser.new(slugged_encoded_id, separator: EncodedId::Rails.configuration.slugged_id_separator)
|
144
59
|
end
|
145
60
|
|
146
|
-
def
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
def find_via_custom_id!(value, attribute, compare_to: nil)
|
157
|
-
raise ::ActiveRecord::RecordNotFound if value.blank?
|
158
|
-
record = find_by!({attribute => value})
|
159
|
-
unless compare_to.nil?
|
160
|
-
raise ::ActiveRecord::RecordNotFound unless compare_to == record.send(attribute)
|
161
|
-
end
|
162
|
-
record
|
163
|
-
end
|
164
|
-
|
165
|
-
def extract_id_part(slugged_id)
|
166
|
-
return if slugged_id.blank?
|
167
|
-
has_slug = slugged_id.include?("--")
|
168
|
-
return slugged_id unless has_slug
|
169
|
-
split_slug = slugged_id.split("--")
|
170
|
-
split_slug.last if has_slug && split_slug.size > 1
|
61
|
+
def encoded_id_coder(options = {})
|
62
|
+
config = EncodedId::Rails.configuration
|
63
|
+
EncodedId::Rails::Coder.new(
|
64
|
+
salt: options[:salt] || encoded_id_salt,
|
65
|
+
id_length: options[:id_length] || config.id_length,
|
66
|
+
character_group_size: options[:character_group_size] || config.character_group_size,
|
67
|
+
alphabet: options[:alphabet] || config.alphabet,
|
68
|
+
separator: options[:separator] || config.group_separator
|
69
|
+
)
|
171
70
|
end
|
172
71
|
end
|
173
72
|
|
@@ -177,34 +76,24 @@ module EncodedId
|
|
177
76
|
@encoded_id ||= self.class.encode_encoded_id(id)
|
178
77
|
end
|
179
78
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
@slugged_id ||= generate_composite_id(with, :id)
|
79
|
+
def slugged_encoded_id(with: :name_for_encoded_id_slug)
|
80
|
+
@slugged_encoded_id ||= EncodedId::Rails::SluggedId.new(
|
81
|
+
self,
|
82
|
+
slug_method: with,
|
83
|
+
id_method: :encoded_id,
|
84
|
+
separator: EncodedId::Rails.configuration.slugged_id_separator
|
85
|
+
).slugged_id
|
188
86
|
end
|
189
87
|
|
190
88
|
# By default slug calls `name` if it exists or returns class name
|
191
|
-
def
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
private
|
200
|
-
|
201
|
-
def generate_composite_id(name_method, id_method)
|
202
|
-
name_part = send(name_method)
|
203
|
-
id_part = send(id_method)
|
204
|
-
unless id_part.present? && name_part.present?
|
205
|
-
raise(::StandardError, "The model has no #{id_method} or #{name_method}")
|
206
|
-
end
|
207
|
-
"#{name_part.to_s.parameterize}--#{id_part}"
|
89
|
+
def name_for_encoded_id_slug
|
90
|
+
if respond_to? :name
|
91
|
+
given_name = name
|
92
|
+
return given_name if given_name.present?
|
93
|
+
end
|
94
|
+
class_name = self.class.name
|
95
|
+
raise StandardError, "No name or class name found, cannot create a slug" if !class_name || class_name.blank?
|
96
|
+
class_name.underscore
|
208
97
|
end
|
209
98
|
end
|
210
99
|
end
|
data/lib/encoded_id/rails.rb
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
require_relative "rails/version"
|
4
4
|
require_relative "rails/configuration"
|
5
|
+
require_relative "rails/coder"
|
6
|
+
require_relative "rails/slugged_id"
|
7
|
+
require_relative "rails/slugged_id_parser"
|
8
|
+
require_relative "rails/salt"
|
5
9
|
require_relative "rails/with_encoded_id"
|
6
10
|
|
7
11
|
module EncodedId
|
@@ -18,6 +18,13 @@ EncodedId::Rails.configure do |config|
|
|
18
18
|
#
|
19
19
|
# config.character_group_size = 4
|
20
20
|
|
21
|
+
# The separator used between character groups in the encoded ID.
|
22
|
+
# `nil` disables grouping.
|
23
|
+
#
|
24
|
+
# Default: "-"
|
25
|
+
#
|
26
|
+
# config.group_separator = "-"
|
27
|
+
|
21
28
|
# The characters allowed in the encoded ID.
|
22
29
|
# Note, hash ids requires at least 16 unique alphabet characters.
|
23
30
|
#
|
data/rbs_collection.yaml
CHANGED
data/sig/encoded_id/rails.rbs
CHANGED
@@ -1,104 +1,101 @@
|
|
1
1
|
module EncodedId
|
2
2
|
module Rails
|
3
|
-
|
3
|
+
VERSION: ::String
|
4
|
+
|
5
|
+
class Coder
|
6
|
+
def initialize: (salt: ::String, id_length: ::Integer, character_group_size: ::Integer, separator: ::String, alphabet: ::EncodedId::Alphabet) -> void
|
7
|
+
def encode: (::Integer | ::Array[::Integer]) -> String
|
8
|
+
def decode: (::String) -> (nil | ::Array[::Integer])
|
9
|
+
|
10
|
+
@salt: ::String
|
11
|
+
@id_length: ::Integer
|
12
|
+
@character_group_size: ::Integer
|
13
|
+
@separator: ::String
|
14
|
+
@alphabet: ::EncodedId::Alphabet
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def coder: -> ::EncodedId::ReversibleId
|
19
|
+
end
|
4
20
|
|
5
21
|
class Configuration
|
6
22
|
attr_accessor salt: ::String
|
7
|
-
|
23
|
+
attr_accessor group_separator: ::String
|
8
24
|
attr_accessor character_group_size: ::Integer
|
9
|
-
|
10
25
|
attr_accessor alphabet: ::EncodedId::Alphabet
|
11
|
-
|
12
26
|
attr_accessor id_length: ::Integer
|
27
|
+
attr_accessor slugged_id_separator: ::String
|
13
28
|
|
14
29
|
def initialize: () -> void
|
15
30
|
end
|
16
31
|
|
17
|
-
|
18
|
-
|
19
|
-
def self.configure: () { (Configuration config) -> void } -> void
|
32
|
+
class Salt
|
33
|
+
def initialize: (untyped klass, ::String salt) -> void
|
20
34
|
|
21
|
-
|
22
|
-
|
23
|
-
def self.find_by_encoded_id: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> (nil | untyped)
|
35
|
+
@klass: untyped
|
36
|
+
@salt: ::String
|
24
37
|
|
25
|
-
def
|
38
|
+
def generate!: -> ::String
|
39
|
+
end
|
26
40
|
|
27
|
-
|
28
|
-
def
|
41
|
+
class SluggedId
|
42
|
+
def initialize: (untyped from_object, ?slug_method: ::Symbol, ?id_method: ::Symbol, ?separator: ::String)-> void
|
29
43
|
|
30
|
-
|
44
|
+
@from_object: untyped
|
45
|
+
@slug_method: ::Symbol
|
46
|
+
@id_method: ::Symbol
|
47
|
+
@separator: ::String
|
31
48
|
|
32
|
-
|
33
|
-
|
49
|
+
def slugged_id: -> ::String
|
50
|
+
end
|
34
51
|
|
35
|
-
|
52
|
+
class SluggedIdParser
|
53
|
+
def initialize: (::String slugged_id, ?separator: ::String) -> void
|
36
54
|
|
37
|
-
|
55
|
+
attr_reader slug: (nil | ::String)
|
56
|
+
attr_reader id: (nil | ::String)
|
57
|
+
end
|
38
58
|
|
39
|
-
|
59
|
+
attr_reader self.configuration: Configuration
|
40
60
|
|
41
|
-
|
61
|
+
def self.configure: () { (Configuration config) -> void } -> void
|
42
62
|
|
63
|
+
module WithEncodedId
|
64
|
+
module ClassMethods
|
65
|
+
def find_by_encoded_id: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> (nil | untyped)
|
66
|
+
def find_by_encoded_id!: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> untyped
|
67
|
+
def where_encoded_id: (::String slugged_encoded_id) -> untyped
|
68
|
+
def encode_encoded_id: (untyped id, ?::Hash[::Symbol, untyped] options) -> ::String
|
69
|
+
def decode_encoded_id: (::String slugged_encoded_id, ?::Hash[::Symbol, untyped] options) -> (nil | ::Array[::Integer])
|
70
|
+
def encoded_id_salt: () -> ::String
|
71
|
+
def encoded_id_parser: (::String slugged_encoded_id) -> ::EncodedId::Rails::SluggedIdParser
|
72
|
+
def encoded_id_coder: (?::Hash[::Symbol, untyped] options) -> ::EncodedId::Rails::Coder
|
73
|
+
|
74
|
+
# FIXME: Methods defined on AR, how to tell steep that this will be composed with AR?
|
75
|
+
def where: (*untyped) -> untyped
|
76
|
+
def find: (*untyped) -> (nil | untyped)
|
77
|
+
def find!: (*untyped) -> untyped
|
78
|
+
def find_by: (*untyped) -> (nil | untyped)
|
79
|
+
def find_by!: (*untyped) -> untyped
|
80
|
+
end
|
81
|
+
|
82
|
+
# FIXME: steep doesnt understand that methods defined in ClassMethods will be added to the class, hence
|
83
|
+
# the duplication here
|
84
|
+
def self.find_by_encoded_id: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> (nil | untyped)
|
85
|
+
def self.find_by_encoded_id!: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> untyped
|
86
|
+
def self.where_encoded_id: (::String slugged_encoded_id) -> untyped
|
43
87
|
def self.encode_encoded_id: (untyped id, ?::Hash[::Symbol, untyped] options) -> ::String
|
44
|
-
|
45
|
-
def self.encode_multi_encoded_id: (::Array[untyped] encoded_ids, ?::Hash[::Symbol, untyped] options) -> ::String
|
46
|
-
|
47
|
-
# Decode a encoded_id (can be slugged)
|
48
|
-
def self.decode_encoded_id: (::String slugged_encoded_id, ?::Hash[::Symbol, untyped] options) -> (nil | ::Integer)
|
49
|
-
|
50
|
-
def self.decode_multi_encoded_id: (::String slugged_encoded_id, ?::Hash[::Symbol, untyped] options) -> (nil | Array[::Integer])
|
51
|
-
|
52
|
-
# Decode a Slugged ID
|
53
|
-
def self.decode_slugged_id: (::String slugged) -> (nil | ::Integer)
|
54
|
-
|
55
|
-
# Decode a set of slugged IDs
|
56
|
-
def self.decode_slugged_ids: (::String slugged) -> (nil | Array[::Integer])
|
57
|
-
|
58
|
-
# This can be overridden in the model to provide a custom salt
|
88
|
+
def self.decode_encoded_id: (::String slugged_encoded_id, ?::Hash[::Symbol, untyped] options) -> (nil | ::Array[::Integer])
|
59
89
|
def self.encoded_id_salt: () -> ::String
|
90
|
+
def self.encoded_id_parser: (::String slugged_encoded_id) -> ::EncodedId::Rails::SluggedIdParser
|
91
|
+
def self.encoded_id_coder: (?::Hash[::Symbol, untyped] options) -> ::EncodedId::Rails::Coder
|
60
92
|
|
61
|
-
private
|
62
|
-
|
63
|
-
def self.hash_id_encoder: (untyped options) -> untyped
|
64
|
-
|
65
|
-
def self.config: () -> untyped
|
66
|
-
|
67
|
-
@slugged_id: ::String
|
68
93
|
@encoded_id: ::String
|
69
94
|
@slugged_encoded_id: ::String
|
70
95
|
|
71
|
-
def encoded_id: () ->
|
72
|
-
|
73
|
-
|
74
|
-
def slugged_encoded_id: (?with: ::Symbol) -> untyped
|
75
|
-
|
76
|
-
# (name slug)--(record id(s) (separated by hyphen))
|
77
|
-
def slugged_id: (?with: ::Symbol) -> untyped
|
78
|
-
|
79
|
-
# By default slug calls `name` if it exists or returns class name
|
80
|
-
def slug: () -> untyped
|
81
|
-
|
82
|
-
def self.internal_decode_encoded_id: (untyped slugged_encoded_id, untyped options) -> (nil | untyped)
|
83
|
-
|
84
|
-
def self.find_via_custom_id: (untyped value, untyped attribute, ?compare_to: untyped?) -> (nil | untyped)
|
85
|
-
|
86
|
-
def self.find_via_custom_id!: (untyped value, untyped attribute, ?compare_to: untyped?) -> untyped
|
87
|
-
|
88
|
-
def self.extract_id_part: (untyped slugged_id) -> (nil | untyped)
|
89
|
-
|
90
|
-
def generate_composite_id: (untyped name_method, untyped id_method) -> ::String
|
91
|
-
|
92
|
-
# Methods defined on AR
|
93
|
-
def self.where: (*untyped) -> untyped
|
94
|
-
|
95
|
-
def self.find: (*untyped) -> (nil | untyped)
|
96
|
-
|
97
|
-
def self.find!: (*untyped) -> untyped
|
98
|
-
|
99
|
-
def self.find_by: (*untyped) -> (nil | untyped)
|
100
|
-
|
101
|
-
def self.find_by!: (*untyped) -> untyped
|
96
|
+
def encoded_id: () -> ::String
|
97
|
+
def slugged_encoded_id: (?with: ::Symbol) -> ::String
|
98
|
+
def name_for_encoded_id_slug: () -> ::String
|
102
99
|
|
103
100
|
# FIXME: To make type check happy, but may not exist!
|
104
101
|
# We call if respond_to? but type checker doesn't know that
|
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: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Ierodiaconou
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-12-
|
11
|
+
date: 2022-12-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -17,6 +17,9 @@ dependencies:
|
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '6.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '8.0'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -24,6 +27,9 @@ dependencies:
|
|
24
27
|
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: '6.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '8.0'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: activerecord
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -31,6 +37,9 @@ dependencies:
|
|
31
37
|
- - ">="
|
32
38
|
- !ruby/object:Gem::Version
|
33
39
|
version: '6.0'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '8.0'
|
34
43
|
type: :runtime
|
35
44
|
prerelease: false
|
36
45
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -38,6 +47,9 @@ dependencies:
|
|
38
47
|
- - ">="
|
39
48
|
- !ruby/object:Gem::Version
|
40
49
|
version: '6.0'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '8.0'
|
41
53
|
- !ruby/object:Gem::Dependency
|
42
54
|
name: encoded_id
|
43
55
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,7 +64,8 @@ dependencies:
|
|
52
64
|
- - "~>"
|
53
65
|
- !ruby/object:Gem::Version
|
54
66
|
version: '0.4'
|
55
|
-
description:
|
67
|
+
description: ActiveRecord concern to use EncodedID to turn IDs into reversible and
|
68
|
+
human friendly obfuscated strings.
|
56
69
|
email:
|
57
70
|
- stevegeek@gmail.com
|
58
71
|
executables: []
|
@@ -69,7 +82,11 @@ files:
|
|
69
82
|
- gemfiles/rails_6.0.gemfile
|
70
83
|
- gemfiles/rails_7.0.gemfile
|
71
84
|
- lib/encoded_id/rails.rb
|
85
|
+
- lib/encoded_id/rails/coder.rb
|
72
86
|
- lib/encoded_id/rails/configuration.rb
|
87
|
+
- lib/encoded_id/rails/salt.rb
|
88
|
+
- lib/encoded_id/rails/slugged_id.rb
|
89
|
+
- lib/encoded_id/rails/slugged_id_parser.rb
|
73
90
|
- lib/encoded_id/rails/version.rb
|
74
91
|
- lib/encoded_id/rails/with_encoded_id.rb
|
75
92
|
- lib/generators/encoded_id/rails/USAGE
|
@@ -102,5 +119,5 @@ requirements: []
|
|
102
119
|
rubygems_version: 3.3.26
|
103
120
|
signing_key:
|
104
121
|
specification_version: 4
|
105
|
-
summary: Use
|
122
|
+
summary: Use `encoded_id` with ActiveRecord models
|
106
123
|
test_files: []
|