encoded_id-rails 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +6 -2
- 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
|
14
|
-
@
|
16
|
+
@group_separator = "-"
|
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: []
|