encoded_id-rails 0.3.1 → 0.5.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 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: []