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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe9c0b9513dfdf8a610a5c9a518b1a17aba9a4f4a35702fbc5fd792bf19b03ca
4
- data.tar.gz: f4608f0b323f1e54e4bc1b08df66a9ec99998aaebe047f7e577c7db724b093c3
3
+ metadata.gz: 0aceb28e50697bb0257054c2731936a7cad5f6617d479543c63999a156c153bf
4
+ data.tar.gz: 59be3ddf7327f26726df0aa3120f9a70de0ac225d77291ad8e35b10b937e6264
5
5
  SHA512:
6
- metadata.gz: e6dee975ecb993c901706f39b39f6bee2fb0a6f2ee64bae36039c863c5229c6f9268cb098e5c8aee03fa383aee401466ae1520b689318ab7aa60024d88f98999
7
- data.tar.gz: b4f6293c7185cea8e5d997c5fc6f2d54499b621b5888e0a6f2a5891c7dd59f4eaa7efa7d067d8f61263441a5d83baaed14d34f5b1bcb3759b3466985a32634f2
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
- 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
14
- @alphabet = "0123456789abcdefghjkmnpqrstuvwxyz"
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module EncodedId
4
4
  module Rails
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  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
- 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)
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
- 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
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
- 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)
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(extract_id_part(slugged_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 where_fixed_slug(slug, attribute: :slug)
64
- where(attribute => slug)
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
- 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)
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
- 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
54
+ EncodedId::Rails::Salt.new(self, EncodedId::Rails.configuration.salt).generate!
135
55
  end
136
56
 
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
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 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
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
- # (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)
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 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
197
- end
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
@@ -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
@@ -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,104 +1,101 @@
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) -> (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
- attr_reader self.configuration: Configuration
18
-
19
- def self.configure: () { (Configuration config) -> void } -> void
32
+ class Salt
33
+ def initialize: (untyped klass, ::String salt) -> void
20
34
 
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)
35
+ @klass: untyped
36
+ @salt: ::String
24
37
 
25
- def self.find_by_encoded_id!: (::String slugged_encoded_id, ?with_id: ::Symbol?) -> untyped
38
+ def generate!: -> ::String
39
+ end
26
40
 
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)
41
+ class SluggedId
42
+ def initialize: (untyped from_object, ?slug_method: ::Symbol, ?id_method: ::Symbol, ?separator: ::String)-> void
29
43
 
30
- def self.find_by_fixed_slug!: (::String slug, ?attribute: ::Symbol, ?with_id: ::Symbol?) -> untyped
44
+ @from_object: untyped
45
+ @slug_method: ::Symbol
46
+ @id_method: ::Symbol
47
+ @separator: ::String
31
48
 
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)
49
+ def slugged_id: -> ::String
50
+ end
34
51
 
35
- def self.find_by_slugged_id!: (::String slugged_id, ?with_id: ::Symbol?) -> untyped
52
+ class SluggedIdParser
53
+ def initialize: (::String slugged_id, ?separator: ::String) -> void
36
54
 
37
- def self.where_encoded_id: (::String slugged_encoded_id) -> untyped
55
+ attr_reader slug: (nil | ::String)
56
+ attr_reader id: (nil | ::String)
57
+ end
38
58
 
39
- def self.where_fixed_slug: (::String slug, ?attribute: ::Symbol) -> untyped
59
+ attr_reader self.configuration: Configuration
40
60
 
41
- def self.where_slugged_id: (::String slugged_id) -> untyped
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: () -> 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
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.3.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-15 00:00:00.000000000 Z
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: 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,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 EncodedIds with ActiveRecord models
122
+ summary: Use `encoded_id` with ActiveRecord models
106
123
  test_files: []