encoded_id-rails 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of encoded_id-rails might be problematic. Click here for more details.

checksums.yaml 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: []