encoded_id-rails 0.3.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4be4ce77532e0194a29d624cb4e9ae4dd1f783ead388ba73b00abc21c27b431
4
- data.tar.gz: 41a11cd3abe1a15096a1eac101e54374edd69ec813689879fa2debdce684d949
3
+ metadata.gz: 6cfa444903c74b47cf55b4c8aa45d862f6eecbe843ff0af9b987b713c4b226d4
4
+ data.tar.gz: 7f530fdd9cd3b8a83a308f45e236c92c2bbdaae394cc8b455178ce1ff3de9802
5
5
  SHA512:
6
- metadata.gz: d68f3f4cbec0a64615141ff9edc627e986fa18b5ac2d2e274e86cc149893482e1331fc8b0fb6c9e1f7dbe411790cbb0c7dfdd301ecd46f1b5da233320f461fb4
7
- data.tar.gz: a879ab60759348cb5b53e9f092736debd515dca16a852af79ea53750a3b97bea0dd69151825379e578e65d024fcf375db15746a7f4f398b7746a179490077dec
6
+ metadata.gz: e8e9be56253a8956f1a850905c6a9c53e33f7593d419c7348a158e8ff76e3a0285319c116b8a81bfb22d964ceec6aac85fcbe128b3a4ad8265b0b790be27b83b
7
+ data.tar.gz: bc63195e7d397c41350b80783526d1733e6109ddc9cd9cbace00d827ff95b19046b287030436da09e359bb3f92774efe702bc17dc4fea006192748463c5eb9b7
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
- gem "rake", "~> 13.0"
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 mixin for your ActiveRecord models.
3
+ [EncodedId](https://github.com/stevegeek/encoded_id) for Rails and `ActiveRecord` models.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/encoded_id/rails`. To experiment with that code, run `bin/console` for an interactive prompt.
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 slug
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.slugged_id # => "bob-smith--p5w9-z27j"
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
- # Coming in future (?)
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
- ## Why this?
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 or mess with ActiveRecord
34
- * we support slugged IDs (eg 'my-amazing-product--p5w9-z27j')
35
- * we support multiple model IDs encoded in one `EncodedId` (eg '7aq6-0zqw' decodes to `[78, 45]`)
36
- * we use a reduced character set (Crockford alphabet),
37
- and ids split into groups of letters, ie we aim for 'human-readability'
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
- TODO: Write usage instructions here
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
- ### Use on all models (but I recommend you don't)
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/[USERNAME]/encoded_id-rails.
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,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedId
4
+ module Rails
5
+ module EncoderMethods
6
+ def encode_encoded_id(ids, options = {})
7
+ raise StandardError, "You must pass an ID or array of IDs" if ids.blank?
8
+ encoded_id_coder(options).encode(ids)
9
+ end
10
+
11
+ def decode_encoded_id(slugged_encoded_id, options = {})
12
+ return if slugged_encoded_id.blank?
13
+ encoded_id = encoded_id_parser(slugged_encoded_id).id
14
+ return if !encoded_id || encoded_id.blank?
15
+ encoded_id_coder(options).decode(encoded_id)
16
+ end
17
+
18
+ # This can be overridden in the model to provide a custom salt
19
+ def encoded_id_salt
20
+ # @type self: Class
21
+ EncodedId::Rails::Salt.new(self, EncodedId::Rails.configuration.salt).generate!
22
+ end
23
+
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
+ def encoded_id_coder(options = {})
29
+ config = EncodedId::Rails.configuration
30
+ EncodedId::Rails::Coder.new(
31
+ salt: options[:salt] || encoded_id_salt,
32
+ id_length: options[:id_length] || config.id_length,
33
+ character_group_size: options[:character_group_size] || config.character_group_size,
34
+ alphabet: options[:alphabet] || config.alphabet,
35
+ separator: options[:separator] || config.group_separator
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedId
4
+ module Rails
5
+ module FinderMethods
6
+ # Find by encoded ID and optionally ensure record ID is the same as constraint (can be slugged)
7
+ def find_by_encoded_id(slugged_encoded_id, with_id: nil)
8
+ decoded_id = decode_encoded_id(slugged_encoded_id)
9
+ return if decoded_id.blank?
10
+ record = find_by(id: decoded_id)
11
+ return unless record
12
+ return if with_id && with_id != record.send(:id)
13
+ record
14
+ end
15
+
16
+ def find_by_encoded_id!(slugged_encoded_id, with_id: nil)
17
+ decoded_id = decode_encoded_id(slugged_encoded_id)
18
+ raise ActiveRecord::RecordNotFound if decoded_id.blank?
19
+ record = find_by(id: decoded_id)
20
+ if !record || (with_id && with_id != record.send(:id))
21
+ raise ActiveRecord::RecordNotFound
22
+ end
23
+ record
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedId
4
+ module Rails
5
+ module QueryMethods
6
+ def where_encoded_id(slugged_encoded_id)
7
+ decoded_id = decode_encoded_id(slugged_encoded_id)
8
+ raise ActiveRecord::RecordNotFound if decoded_id.nil?
9
+ where(id: decoded_id)
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module EncodedId
4
4
  module Rails
5
- VERSION = "0.3.1"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -7,204 +7,29 @@ module EncodedId
7
7
  module Rails
8
8
  module WithEncodedId
9
9
  def self.included(base)
10
- base.extend(ClassMethods)
10
+ base.extend(EncoderMethods)
11
+ base.extend(FinderMethods)
12
+ base.extend(QueryMethods)
11
13
  end
12
14
 
13
- module ClassMethods
14
- # Find by encoded ID and optionally ensure record ID is the same as constraint (can be slugged)
15
- def find_by_encoded_id(slugged_encoded_id, with_id: nil)
16
- encoded_id = extract_id_part(slugged_encoded_id)
17
- decoded_id = decode_encoded_id(encoded_id)
18
- return nil if decoded_id.nil?
19
- find_via_custom_id(decoded_id, :id, compare_to: with_id)
20
- end
21
-
22
- def find_by_encoded_id!(slugged_encoded_id, with_id: nil)
23
- encoded_id = extract_id_part(slugged_encoded_id)
24
- decoded_id = decode_encoded_id(encoded_id)
25
- raise ActiveRecord::RecordNotFound if decoded_id.nil?
26
- find_via_custom_id!(decoded_id, :id, compare_to: with_id)
27
- end
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
43
- end
44
- where(id: id_part)&.first
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)
53
- end
54
-
55
- # relation helpers
56
-
57
- def where_encoded_id(slugged_encoded_id)
58
- decoded_id = decode_encoded_id(extract_id_part(slugged_encoded_id))
59
- raise ActiveRecord::RecordNotFound if decoded_id.nil?
60
- where(id: decoded_id)
61
- end
62
-
63
- def where_fixed_slug(slug, attribute: :slug)
64
- where(attribute => slug)
65
- end
66
-
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
- def decode_encoded_id(slugged_encoded_id, options = {})
89
- internal_decode_encoded_id(slugged_encoded_id, options)&.first
90
- end
91
-
92
- def decode_multi_encoded_id(slugged_encoded_id, options = {})
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)
106
- end
107
-
108
- # This can be overridden in the model to provide a custom salt
109
- def encoded_id_salt
110
- unless config && config.salt.present?
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
135
- end
136
-
137
- def internal_decode_encoded_id(slugged_encoded_id, options)
138
- return if slugged_encoded_id.blank?
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
144
- end
145
-
146
- def find_via_custom_id(value, attribute, compare_to: nil)
147
- return if value.blank?
148
- record = find_by({attribute => value})
149
- return unless record
150
- unless compare_to.nil?
151
- return unless compare_to == record.send(attribute)
152
- end
153
- record
154
- end
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
171
- end
172
- end
173
-
174
- # Instance methods
175
-
176
15
  def encoded_id
177
16
  @encoded_id ||= self.class.encode_encoded_id(id)
178
17
  end
179
18
 
180
- # (slug)--(hash id)
181
- def slugged_encoded_id(with: :slug)
182
- @slugged_encoded_id ||= generate_composite_id(with, :encoded_id)
183
- end
184
-
185
- # (name slug)--(record id(s) (separated by hyphen))
186
- def slugged_id(with: :slug)
187
- @slugged_id ||= generate_composite_id(with, :id)
188
- end
189
-
190
- # By default slug calls `name` if it exists or returns class name
191
- def slug
192
- klass = self.class.name&.underscore
193
- return klass unless respond_to? :name
194
- given_name = name
195
- return given_name if given_name.present?
196
- klass
19
+ def slugged_encoded_id(with: :name_for_encoded_id_slug)
20
+ @slugged_encoded_id ||= EncodedId::Rails::SluggedId.new(
21
+ self,
22
+ slug_method: with,
23
+ id_method: :encoded_id,
24
+ separator: EncodedId::Rails.configuration.slugged_id_separator
25
+ ).slugged_id
197
26
  end
198
27
 
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}"
28
+ # By default slug created from class name, but can be overridden
29
+ def name_for_encoded_id_slug
30
+ class_name = self.class.name
31
+ raise StandardError, "Class must have a `name`, cannot create a slug" if !class_name || class_name.blank?
32
+ class_name.underscore
208
33
  end
209
34
  end
210
35
  end
@@ -2,6 +2,13 @@
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"
9
+ require_relative "rails/encoder_methods"
10
+ require_relative "rails/query_methods"
11
+ require_relative "rails/finder_methods"
5
12
  require_relative "rails/with_encoded_id"
6
13
 
7
14
  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
@@ -11,6 +11,7 @@ path: .gem_rbs_collection
11
11
  gems:
12
12
  - name: encoded_id-rails
13
13
  ignore: true
14
+ - name: cgi
14
15
  # Skip loading rbs gem's RBS.
15
16
  # It's unnecessary if you don't use rbs as a library.
16
17
  - name: rbs
@@ -1,109 +1,104 @@
1
1
  module EncodedId
2
2
  module Rails
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
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) -> ::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
- attr_reader self.configuration: Configuration
18
-
19
- def self.configure: () { (Configuration config) -> void } -> void
20
-
21
- module WithEncodedId
22
- # Find by encoded ID and optionally ensure record ID is the same as constraint (can be slugged)
23
- def self.find_by_encoded_id: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> (nil | untyped)
24
-
25
- def self.find_by_encoded_id!: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> untyped
26
-
27
- # Find by a fixed slug value (assumed as an attribute value in the DB)
28
- def self.find_by_fixed_slug: (::String slug, ?attribute: ::Symbol, ?with_id: ::Symbol?) -> (nil | untyped)
32
+ class Salt
33
+ def initialize: (Class klass, ::String salt) -> void
29
34
 
30
- def self.find_by_fixed_slug!: (::String slug, ?attribute: ::Symbol, ?with_id: ::Symbol?) -> untyped
35
+ @klass: Class
36
+ @salt: ::String
31
37
 
32
- # Find by record ID where the ID has been slugged
33
- def self.find_by_slugged_id: (::String slugged_id, ?with_id: ::Symbol?) -> (nil | untyped)
38
+ def generate!: -> ::String
39
+ end
34
40
 
35
- def self.find_by_slugged_id!: (::String slugged_id, ?with_id: ::Symbol?) -> untyped
41
+ class SluggedId
42
+ def initialize: (untyped from_object, ?slug_method: ::Symbol, ?id_method: ::Symbol, ?separator: ::String)-> void
36
43
 
37
- def self.where_encoded_id: (::String slugged_encoded_id) -> untyped
44
+ @from_object: untyped
45
+ @slug_method: ::Symbol
46
+ @id_method: ::Symbol
47
+ @separator: ::String
38
48
 
39
- def self.where_fixed_slug: (::String slug, ?attribute: ::Symbol) -> untyped
49
+ def slugged_id: -> ::String
50
+ end
40
51
 
41
- def self.where_slugged_id: (::String slugged_id) -> untyped
52
+ class SluggedIdParser
53
+ def initialize: (::String slugged_id, ?separator: ::String) -> void
42
54
 
43
- def self.encode_encoded_id: (untyped id, ?::Hash[::Symbol, untyped] options) -> ::String
55
+ attr_reader slug: ::String?
56
+ attr_reader id: ::String?
57
+ end
44
58
 
45
- def self.encode_multi_encoded_id: (::Array[untyped] encoded_ids, ?::Hash[::Symbol, untyped] options) -> ::String
59
+ attr_reader self.configuration: Configuration
46
60
 
47
- # Decode a encoded_id (can be slugged)
48
- def self.decode_encoded_id: (::String slugged_encoded_id, ?::Hash[::Symbol, untyped] options) -> (nil | ::Integer)
61
+ def self.configure: () { (Configuration config) -> void } -> void
49
62
 
50
- def self.decode_multi_encoded_id: (::String slugged_encoded_id, ?::Hash[::Symbol, untyped] options) -> (nil | Array[::Integer])
63
+ module EncoderMethods
64
+ def encode_encoded_id: (untyped id, ?::Hash[::Symbol, untyped] options) -> ::String
65
+ def decode_encoded_id: (::String slugged_encoded_id, ?::Hash[::Symbol, untyped] options) -> ::Array[::Integer]?
66
+ def encoded_id_salt: () -> ::String
67
+ def encoded_id_parser: (::String slugged_encoded_id) -> ::EncodedId::Rails::SluggedIdParser
68
+ def encoded_id_coder: (?::Hash[::Symbol, untyped] options) -> ::EncodedId::Rails::Coder
69
+ end
51
70
 
52
- # Decode a Slugged ID
53
- def self.decode_slugged_id: (::String slugged) -> (nil | ::Integer)
71
+ interface _ActiveRecordFinderMethod
72
+ def find_by: (*untyped) -> (nil | untyped)
73
+ end
54
74
 
55
- # Decode a set of slugged IDs
56
- def self.decode_slugged_ids: (::String slugged) -> (nil | Array[::Integer])
75
+ module FinderMethods : EncoderMethods, _ActiveRecordFinderMethod
76
+ def find_by_encoded_id: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> untyped?
77
+ def find_by_encoded_id!: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> untyped
78
+ end
57
79
 
58
- # This can be overridden in the model to provide a custom salt
59
- def self.encoded_id_salt: () -> ::String
80
+ interface _ActiveRecordQueryMethod
81
+ def where: (*untyped) -> untyped
82
+ end
60
83
 
61
- private
84
+ module QueryMethods : EncoderMethods, _ActiveRecordQueryMethod
85
+ def where_encoded_id: (::String slugged_encoded_id) -> untyped
86
+ end
62
87
 
63
- def self.hash_id_encoder: (untyped options) -> untyped
88
+ module WithEncodedId : ActiveRecord::Base
89
+ extend ActiveRecord::FinderMethods
90
+ extend ActiveRecord::QueryMethods
64
91
 
65
- def self.config: () -> untyped
92
+ extend EncoderMethods
93
+ extend FinderMethods
94
+ extend QueryMethods
66
95
 
67
- @slugged_id: ::String
68
96
  @encoded_id: ::String
69
97
  @slugged_encoded_id: ::String
70
98
 
71
- def encoded_id: () -> untyped
72
-
73
- # (slug)--(hash id)
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
102
-
103
- # FIXME: To make type check happy, but may not exist!
104
- # We call if respond_to? but type checker doesn't know that
105
- def name: () -> ::String
106
- def id: () -> ::String
99
+ def encoded_id: () -> ::String
100
+ def slugged_encoded_id: (?with: ::Symbol) -> ::String
101
+ def name_for_encoded_id_slug: () -> ::String
107
102
  end
108
103
  end
109
104
  end
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.3.1
4
+ version: 0.5.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-15 00:00:00.000000000 Z
11
+ date: 2022-12-21 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: Write a longer description or delete this line.
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,14 @@ 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/encoder_methods.rb
88
+ - lib/encoded_id/rails/finder_methods.rb
89
+ - lib/encoded_id/rails/query_methods.rb
90
+ - lib/encoded_id/rails/salt.rb
91
+ - lib/encoded_id/rails/slugged_id.rb
92
+ - lib/encoded_id/rails/slugged_id_parser.rb
73
93
  - lib/encoded_id/rails/version.rb
74
94
  - lib/encoded_id/rails/with_encoded_id.rb
75
95
  - lib/generators/encoded_id/rails/USAGE
@@ -102,5 +122,5 @@ requirements: []
102
122
  rubygems_version: 3.3.26
103
123
  signing_key:
104
124
  specification_version: 4
105
- summary: Use EncodedIds with ActiveRecord models
125
+ summary: Use `encoded_id` with ActiveRecord models
106
126
  test_files: []