encoded_id 0.3.0 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b69898cce6c9514fc7269f139e4280673b319cf2c879cbb925fdc5a8c03abc27
4
- data.tar.gz: b0f17c21e5baa1a171df12629d27ebae511c647a66c0dc82dd8690e573c94754
3
+ metadata.gz: a860b12577a70042f6d17a1788cb8557f3ab5ab477441a44c3be037b70165ded
4
+ data.tar.gz: bd9d57c92ada9e47a1080ea5b4d04cb3069a055a213305b698f1a2c7c424297f
5
5
  SHA512:
6
- metadata.gz: 133e6d70f9a3613e9009e0d5fe65dd3fdebd095c79533d7976924ed2d9589a47d4854a19d0a9f718caf864673a066f07d92b6ba5947ae043ecb170ed2aab6175
7
- data.tar.gz: a5368e9bc5eb8f49438cff06c5c3fbd22d054b386d31ae194ccdea2f8ed148bc9f54492eb93da2e88893056615c6ef73dc24eaef1e22194feac2e160660f0f10
6
+ metadata.gz: e78405858e31da61d3361c3e42f999591e68f2053f1b58944c1f07616f5a57a599a174708eda301d52743d9dabaae8e76bf50442936401a9e943699589e0ad5c
7
+ data.tar.gz: a4e9ee6327f0d0e0d68eab43af5f21126e5ccef888b7478e07e14ade2a1532f618ac95ddb80e3406956ed7e505fda76c577e63368d376955024ab65df5084aa0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.0] - 2023-08-06
4
+
5
+ - Improved RBS definitions
6
+ - Improved test coverage
7
+
8
+ ## [0.4.0] - 2022-12-04
9
+
10
+ - Support custom split character which must not be in the alphabet
11
+ - Ability to provide a custom character equivalences mapping
12
+
13
+ ## [0.3.0] - 2022-10-12
14
+
15
+ - Fix splitting of encoded ID string
16
+ - Checks that integer values to be encoded are positive
17
+ - Experimental support for encoding hex strings
18
+
3
19
  ## [0.1.0] - 2022-10-11
4
20
 
5
21
  - Initial release
data/Gemfile CHANGED
@@ -9,6 +9,6 @@ gem "rake", "~> 13.0"
9
9
 
10
10
  gem "minitest", "~> 5.0"
11
11
 
12
- gem "standard", "~> 1.3"
12
+ gem "standard", "~> 1.25"
13
13
 
14
- gem "steep", "~> 1.2"
14
+ gem "steep", "~> 1.5"
data/README.md CHANGED
@@ -1,65 +1,76 @@
1
1
  # EncodedId
2
2
 
3
- Encode your numerical IDs (eg record primary keys) into obfuscated strings that can be used in URLs.
3
+ Encode numerical or hex IDs into obfuscated strings that can be used in URLs.
4
4
 
5
- `::EncodedId::ReversibleId.new(salt: my_salt).encode(123)` => `"p5w9-z27j"`
5
+ ```ruby
6
+ coder = ::EncodedId::ReversibleId.new(salt: my_salt)
7
+ coder.encode(123)
8
+ # => "p5w9-z27j"
9
+ coder.encode_hex("10f8c")
10
+ # => "w72a-y0az"
11
+ ```
6
12
 
7
- The obfuscated strings are reversible, so you can decode them back into the original numerical IDs. Also supports
8
- encoding multiple IDs at once.
13
+ The obfuscated strings are reversible (they decode them back into the original IDs).
9
14
 
10
- ```
11
- reversibles = ::EncodedId::ReversibleId.new(salt: my_salt)
12
- reversibles.encode([78, 45]) # "7aq6-0zqw"
13
- reversibles.decode("7aq6-0zqw") # [78, 45]
14
- ```
15
+ Also supports encoding multiple IDs at once.
15
16
 
16
- Length of the ID, the alphabet used, and the number of characters per group can be configured.
17
+ ```ruby
18
+ my_salt = "salt!"
19
+ coder = ::EncodedId::ReversibleId.new(salt: my_salt)
17
20
 
18
- The custom alphabet (at least 16 characters needed) and character group sizes is to make the IDs easier to read or share.
19
- Easily confused characters (eg `i` and `j`, `0` and `O`, `1` and `I` etc) are mapped to counterpart characters, to help
20
- common mistakes when sharing (eg customer over phone to customer service agent).
21
+ # One of more values can be encoded
22
+ coder.encode([78, 45])
23
+ # => "z2j7-0dmw"
21
24
 
22
- Also supports UUIDs if needed
25
+ # The encoded string can then be reversed back into the original IDs
26
+ coder.decode("z2j7-0dmw")
27
+ # => [78, 45]
23
28
 
24
- ```
25
- ::EncodedId::ReversibleId.new(salt: my_salt).encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
26
- => "rppv-tg8a-cx8q-gu9e-zq15-jxes-4gpr-06xk-wfk8-aw"
29
+ # The decoder can be resilient to easily confused characters
30
+ coder.decode("z2j7-Odmw") # (note the capital 'o' instead of zero)
31
+ # => [78, 45]
27
32
  ```
28
33
 
29
34
  ## Features
30
35
 
31
- Build with https://hashids.org
36
+ * encoded IDs are reversible (uses with https://hashids.org))
37
+ * supports multiple IDs encoded in one encoded string (eg `7aq6-0zqw` decodes to `[78, 45]`)
38
+ * supports encoding of hex strings (eg UUIDs), including multiple IDs encoded in one string **(experimental)**
39
+ * supports custom alphabets for the encoded string (at least 16 characters needed)
40
+ - by default uses a variation of the Crockford reduced character set (https://www.crockford.com/base32.html)
41
+ - easily confused characters (eg `i` and `j`, `0` and `O`, `1` and `I` etc) are mapped to counterpart characters, to help
42
+ avoid common readability mistakes when reading/sharing
43
+ - build in profanity limitation
44
+ * encoded string can be split into groups of letters to improve human-readability
45
+ - eg `nft9hr834htu` as `nft9-hr83-4htu`
32
46
 
33
- * Hashids are reversible, no need to persist the generated Id
34
- * supports slugged IDs (eg 'beef-tenderloins-prime--p5w9-z27j')
35
- * supports multiple IDs encoded in one `EncodedId` (eg '7aq6-0zqw' decodes to `[78, 45]`)
36
- * supports encoding of hex strings (eg UUIDs), including mutliple IDs encoded in one `EncodedId`
37
- * uses a reduced character set (Crockford alphabet) & ids split into groups of letters, ie 'human-readability'
38
- * profanity limitation
47
+ ### Rails support `encoded_id-rails`
39
48
 
40
- To use with **Rails** check out the `encoded_id-rails` gem.
49
+ To use with **Rails** check out the [`encoded_id-rails`](https://github.com/stevegeek/encoded_id-rails) gem.
41
50
 
42
- ## Note on security of encoded IDs (hashids)
51
+ ```ruby
52
+ class User < ApplicationRecord
53
+ include EncodedId::WithEncodedId
54
+ end
55
+
56
+ User.find_by_encoded_id("p5w9-z27j")
57
+ # => #<User id: 78>
58
+ ```
59
+
60
+ ### Note on security of encoded IDs (hashids)
43
61
 
44
62
  **Encoded IDs are not secure**. It maybe possible to reverse them via brute-force. They are meant to be used in URLs as
45
63
  an obfuscation. The algorithm is not an encryption.
46
64
 
47
65
  Please read more on https://hashids.org/
48
66
 
49
-
50
- ## Compared to alternate Gems
67
+ ## Compare to alternate Gems
51
68
 
52
69
  - https://github.com/excid3/prefixed_ids
53
70
  - https://github.com/namick/obfuscate_id
54
71
  - https://github.com/norman/friendly_id
55
72
  - https://github.com/SPBTV/with_uid
56
73
 
57
- ## See also
58
-
59
- - https://hashids.org
60
- - https://www.crockford.com/wrmg/base32.html
61
-
62
-
63
74
  ## Installation
64
75
 
65
76
  Install the gem and add to the application's Gemfile by executing:
@@ -70,20 +81,189 @@ If bundler is not being used to manage dependencies, install the gem by executin
70
81
 
71
82
  $ gem install encoded_id
72
83
 
73
- ## Usage
84
+ ## `EncodedId::ReversibleId.new`
85
+
86
+ To create an instance of the encoder/decoder use `.new` with the `salt` option:
87
+
88
+ ```ruby
89
+ coder = EncodedId::ReversibleId.new(
90
+ # The salt is required
91
+ salt: ...,
92
+ # And then the following options are optional
93
+ length: 8,
94
+ split_at: 4,
95
+ split_with: "-",
96
+ alphabet: EncodedId::Alphabet.modified_crockford,
97
+ hex_digit_encoding_group_size: 4 # Experimental
98
+ )
99
+ ```
100
+
101
+ Note the `salt` value is required and should be a string of some length (greater than 3 characters). This is used to generate the encoded string.
102
+
103
+ It will need to be the same value when decoding the string back into the original ID. If the salt is changed, the encoded
104
+ strings will be different and possibly decode to different IDs.
105
+
106
+ ### Options
107
+
108
+ The encoded ID is configurable. The following can be changed:
109
+
110
+ - the length, eg 8 characters for `p5w9-z27j`
111
+ - the alphabet used in it (min 16 characters)
112
+ - and the number of characters to split the output into and the separator
74
113
 
75
- TODO: Write usage instructions here
114
+ ### `length`
76
115
 
77
- ### Rails
116
+ `length`: the minimum length of the encoded string. The default is 8 characters.
78
117
 
79
- To use with rails try the `encoded_id-rails` gem.
118
+ The actual length of the encoded string can be longer if the inputs cannot be represented in the minimum length.
119
+
120
+ ### `alphabet`
121
+
122
+ `alphabet`: the alphabet used in the encoded string. By default it uses a variation of the Crockford reduced character set (https://www.crockford.com/base32.html).
123
+
124
+ `alphabet` must be an instance of `EncodedId::Alphabet`.
125
+
126
+ The default alphabet is `EncodedId::Alphabet.modified_crockford`.
127
+
128
+ To create a new alphabet, use `EncodedId::Alphabet.new`:
80
129
 
81
130
  ```ruby
82
- class User < ApplicationRecord
83
- include EncodedId::WithEncodedId
84
- end
131
+ alphabet = EncodedId::Alphabet.new("0123456789abcdef")
132
+ ```
133
+
134
+ `EncodedId::Alphabet.new(characters, equivalences)`
85
135
 
86
- User.find_by_encoded_id("p5w9-z27j") # => #<User id: 78>
136
+ **characters**
137
+
138
+ `characters`: the characters of the alphabet. Can be a string or array of strings.
139
+
140
+ Note that the `characters` of the alphabet must be at least 16 _unique_ characters long.
141
+
142
+
143
+ ```ruby
144
+ alphabet = EncodedId::Alphabet.new("ςερτυθιοπλκξηγφδσαζχψωβνμ")
145
+ coder = ::EncodedId::ReversibleId.new(salt: my_salt, alphabet: alphabet)
146
+ coder.encode(123)
147
+ # => "πφλχ-ψησω"
148
+ ```
149
+
150
+ Note that larger alphabets can result in shorter encoded strings (but remember that `length` specifies the minimum length
151
+ of the encoded string).
152
+
153
+ **equivalences**
154
+
155
+ You can optionally pass an appropriate character `equivalences` mapping. This is used to map easily confused characters
156
+ to their counterpart.
157
+
158
+ `equivalences`: a hash of characters keys, with their equivalent alphabet character mapped to in the values.
159
+
160
+ Note that the characters to be mapped:
161
+ - must not be in the alphabet,
162
+ - must map to a character that is in the alphabet.
163
+
164
+ `nil` is the default value which means no equivalences are used.
165
+
166
+ ```ruby
167
+ alphabet = EncodedId::Alphabet.new("!@#$%^&*()+-={}", {"_" => "-"})
168
+ coder = ::EncodedId::ReversibleId.new(salt: my_salt, alphabet: alphabet)
169
+ coder.encode(123)
170
+ # => "}*^(-^}*="
171
+ ```
172
+
173
+ ### `split_at` and `split_with`
174
+
175
+ For readability, the encoded string can be split into groups of characters.
176
+
177
+ `split_at`: specifies the number of characters to split the encoded string into. Defaults to 4.
178
+
179
+ `split_with`: specifies the separator to use between the groups. Default is `-`.
180
+
181
+ ### `hex_digit_encoding_group_size`
182
+
183
+ **Experimental**
184
+
185
+ `hex_digit_encoding_group_size`: specifies the number of hex digits to encode in a group. Defaults to 4. Can be
186
+ between 1 and 32.
187
+
188
+ Can be used to control the size of the encoded string when encoding hex strings. Larger values will result in shorter
189
+ encoded strings for long inputs, and shorter values will result in shorter encoded strings for smaller inputs.
190
+
191
+ But note that bigger values will also result in larger markers that separate the groups so could end up increasing
192
+ the encoded string length undesirably.
193
+
194
+ See below section `Using with hex strings` for more details.
195
+
196
+ ## `EncodedId::ReversibleId#encode`
197
+
198
+ `#encode(id)`: where `id` is an integer or array of integers to encode.
199
+
200
+ ```ruby
201
+ coder.encode(123)
202
+ # => "p5w9-z27j"
203
+
204
+ # One of more values can be encoded
205
+ coder.encode([78, 45])
206
+ # => "z2j7-0dmw"
207
+ ```
208
+
209
+ ## `EncodedId::ReversibleId#decode`
210
+
211
+ `#decode(encoded_id)`: where `encoded_id` is a string to decode.
212
+
213
+ ```ruby
214
+ # The encoded string can then be reversed back into the original IDs
215
+ coder.decode("z2j7-0dmw")
216
+ # => [78, 45]
217
+ ```
218
+
219
+ ## Using with hex strings
220
+
221
+ **Experimental** (subject to incompatible changes in future versions)
222
+
223
+ ```ruby
224
+ # Hex input strings are also supported
225
+ coder.encode_hex("10f8c")
226
+ # => "w72a-y0az"
227
+ ```
228
+
229
+ When encoding hex strings, the input is split into groups of hex digits, and each group is encoded separately as its
230
+ integer equivalent. In other words the input is converted into an array of integers and encoded as normal with the
231
+ `encode` method.
232
+
233
+ eg with `hex_digit_encoding_group_size=1` and inpu `f1`, is split into `f` and `1`, and then encoded as `15` and `1`
234
+ respectively, ie `encode` is called with `[15, 1]`.
235
+
236
+ To encode multiple hex inputs the encoded string contains markers to indicate the start of a new hex input. This
237
+ marker is equal to an integer value which is 1 larger than the maximum value the hex digit encoding group size can
238
+ represent (ie it is `2^(hex_digit_encoding_group_size * 4)`).
239
+
240
+ So for a hex digit encoding group size of 4 (ie group max value is `0xFFFF`), the marker is `65536`
241
+
242
+ For example with `hex_digit_encoding_group_size=1` for the inputs `f1` and `e2` encoded together, the
243
+ actual encoded integer array is `[15, 1, 16, 14, 2]`.
244
+
245
+ ### `EncodedId::ReversibleId#encode_hex`
246
+
247
+ `encode_hex(hex_string)` , where `hex_string` is a string of hex digits or an array of hex strings.
248
+
249
+ ```ruby
250
+ # UUIDs will result in long output strings...
251
+ coder.encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
252
+ # => "5jjy-c8d9-hxp2-qsve-rgh9-rxnt-7nb5-tve7-bf84-vr"
253
+ #
254
+ # but there is an option to help reduce this...
255
+ coder = ::EncodedId::ReversibleId.new(salt: my_salt, hex_digit_encoding_group_size: 32)
256
+ coder.encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
257
+ # => "vr7m-qra8-m5y6-dkgj-5rqr-q44e-gp4a-52"
258
+ ```
259
+
260
+ ### `EncodedId::ReversibleId#decode_hex`
261
+
262
+ `decode_hex(encoded_id)` , where the output is an array of hex strings.
263
+
264
+ ```ruby
265
+ coder.decode_hex("5jjy-c8d9-hxp2-qsve-rgh9-rxnt-7nb5-tve7-bf84-vr")
266
+ # => ["9a566b8b-8618-42ab-8db7-a5a0276401fd"]
87
267
  ```
88
268
 
89
269
  ## Development
@@ -95,9 +275,29 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
95
275
  number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git
96
276
  commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
97
277
 
278
+ ### Type check
279
+
280
+ First install dependencies:
281
+
282
+ ```bash
283
+ rbs collection install
284
+ ```
285
+
286
+ Then run:
287
+
288
+ ```bash
289
+ steep check
290
+ ```
291
+
292
+
293
+ ## See also
294
+
295
+ - https://hashids.org
296
+ - https://www.crockford.com/base32.html
297
+
98
298
  ## Contributing
99
299
 
100
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/encoded_id.
300
+ Bug reports and pull requests are welcome on GitHub at https://github.com/stevegeek/encoded_id.
101
301
 
102
302
  ## License
103
303
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedId
4
+ class Alphabet
5
+ MIN_UNIQUE_CHARACTERS = 16
6
+
7
+ class << self
8
+ def modified_crockford
9
+ new(
10
+ "0123456789abcdefghjkmnpqrstuvwxyz",
11
+ {
12
+ "o" => "0",
13
+ "i" => "j",
14
+ "l" => "1"
15
+ }
16
+ )
17
+ end
18
+ end
19
+
20
+ def initialize(characters, equivalences = nil)
21
+ raise_invalid_alphabet! unless valid_input_characters?(characters)
22
+ unique_characters = unique_character_alphabet(characters)
23
+ raise_character_set_too_small! unless sufficient_characters?(unique_characters.size)
24
+ raise_invalid_equivalences! unless valid_equivalences?(equivalences, unique_characters)
25
+
26
+ @characters = unique_characters.join
27
+ @equivalences = equivalences
28
+ end
29
+
30
+ attr_reader :characters, :equivalences
31
+
32
+ private
33
+
34
+ def valid_input_characters?(characters)
35
+ (characters.is_a?(Array) || characters.is_a?(String)) && characters.size > 0
36
+ end
37
+
38
+ def unique_character_alphabet(characters)
39
+ (characters.is_a?(Array) ? characters : characters.chars).uniq
40
+ end
41
+
42
+ def sufficient_characters?(size)
43
+ size >= MIN_UNIQUE_CHARACTERS
44
+ end
45
+
46
+ def valid_equivalences?(equivalences, unique_characters)
47
+ return true if equivalences.nil?
48
+ return false unless equivalences.is_a?(Hash)
49
+
50
+ (unique_characters & equivalences.keys).empty? && (equivalences.values - unique_characters).empty?
51
+ end
52
+
53
+ def raise_invalid_alphabet!
54
+ raise InvalidAlphabetError, "Alphabet must be a string or array."
55
+ end
56
+
57
+ def raise_character_set_too_small!
58
+ raise InvalidAlphabetError, "Alphabet must contain at least #{MIN_UNIQUE_CHARACTERS} unique characters."
59
+ end
60
+
61
+ def raise_invalid_equivalences!
62
+ raise InvalidConfigurationError, "Character equivalences must be a hash or nil."
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedId
4
+ class HexRepresentation
5
+ def initialize(hex_digit_encoding_group_size)
6
+ @hex_digit_encoding_group_size = validate_hex_digit_encoding_group_size(hex_digit_encoding_group_size)
7
+ end
8
+
9
+ def hex_as_integers(hexs)
10
+ integer_representation(hexs)
11
+ end
12
+
13
+ def integers_as_hex(integers)
14
+ integers_to_hex_strings(integers)
15
+ end
16
+
17
+ private
18
+
19
+ # Number of hex digits to encode in each group, larger values will result in shorter hashes for longer inputs.
20
+ # Vice versa for smaller values, ie a smaller value will result in smaller hashes for small inputs.
21
+ def validate_hex_digit_encoding_group_size(hex_digit_encoding_group_size)
22
+ if !hex_digit_encoding_group_size.is_a?(Integer) || hex_digit_encoding_group_size < 1 || hex_digit_encoding_group_size > 32
23
+ raise InvalidConfigurationError, "hex_digit_encoding_group_size must be > 0 and <= 32"
24
+ end
25
+ hex_digit_encoding_group_size
26
+ end
27
+
28
+ # Convert hex strings to integer representations
29
+ def integer_representation(hexs)
30
+ inputs = Array(hexs).map(&:to_s)
31
+ digits_to_encode = []
32
+
33
+ inputs.map { |hex_string| hex_string_as_integer_representation(hex_string) }.each do |integer_groups|
34
+ digits_to_encode.concat(integer_groups)
35
+ digits_to_encode << hex_string_separator
36
+ end
37
+
38
+ # Remove the last marker
39
+ digits_to_encode.pop unless digits_to_encode.empty?
40
+ digits_to_encode
41
+ end
42
+
43
+ # Convert integer representations to hex strings
44
+ def integers_to_hex_strings(integers)
45
+ hex_strings = []
46
+ hex_string = []
47
+ add_leading = false
48
+
49
+ integers.reverse_each do |integer|
50
+ if integer == hex_string_separator # Marker to separate hex strings, so start a new one
51
+ hex_strings << hex_string.join
52
+ hex_string = []
53
+ add_leading = false
54
+ else
55
+ hex_string << (add_leading ? "%.#{@hex_digit_encoding_group_size}x" % integer : integer.to_s(16))
56
+ add_leading = true
57
+ end
58
+ end
59
+
60
+ # Add the last hex string
61
+ hex_strings << hex_string.join unless hex_string.empty?
62
+ hex_strings.reverse
63
+ end
64
+
65
+ def hex_string_as_integer_representation(hex_string)
66
+ cleaned = remove_non_hex_characters(hex_string)
67
+ convert_to_integer_groups(cleaned)
68
+ end
69
+
70
+ # Marker to separate hex strings, must be greater than largest value encoded
71
+ def hex_string_separator
72
+ @hex_string_separator ||= 2.pow(@hex_digit_encoding_group_size * 4)
73
+ end
74
+
75
+ def remove_non_hex_characters(hex_string)
76
+ hex_string.gsub(/[^0-9a-f]/i, "")
77
+ end
78
+
79
+ def convert_to_integer_groups(hex_string_cleaned)
80
+ groups = []
81
+ hex_string_cleaned.chars.reverse.each_with_index do |char, i|
82
+ group_id = i / @hex_digit_encoding_group_size
83
+ groups[group_id] ||= []
84
+ groups[group_id].unshift(char)
85
+ end
86
+ groups.map { |c| c.join.to_i(16) }
87
+ end
88
+ end
89
+ end
@@ -8,19 +8,13 @@ require "hashids"
8
8
  # Note hashIds already has a built in profanity limitation algorithm
9
9
  module EncodedId
10
10
  class ReversibleId
11
- ALPHABET = "0123456789abcdefghjkmnpqrstuvwxyz"
12
-
13
- def initialize(salt:, length: 8, split_at: 4, alphabet: ALPHABET, hex_digit_encoding_group_size: 4)
14
- unique_alphabet = alphabet.chars.uniq
15
- raise InvalidAlphabetError, "Alphabet must be at least 16 characters" if unique_alphabet.size < 16
16
-
17
- @human_friendly_alphabet = unique_alphabet.join
18
- @salt = salt
19
- @length = length
20
- @split_at = split_at
21
- # Number of hex digits to encode in each group, larger values will result in shorter hashes for longer inputs.
22
- # Vice versa for smaller values, ie a smaller value will result in smaller hashes for small inputs.
23
- @hex_digit_encoding_group_size = hex_digit_encoding_group_size
11
+ def initialize(salt:, length: 8, split_at: 4, split_with: "-", alphabet: Alphabet.modified_crockford, hex_digit_encoding_group_size: 4)
12
+ @alphabet = validate_alphabet(alphabet)
13
+ @salt = validate_salt(salt)
14
+ @length = validate_length(length)
15
+ @split_at = validate_split_at(split_at)
16
+ @split_with = validate_split_with(split_with, alphabet)
17
+ @hex_represention_encoder = HexRepresentation.new(hex_digit_encoding_group_size)
24
18
  end
25
19
 
26
20
  # Encode the input values into a hash
@@ -33,7 +27,7 @@ module EncodedId
33
27
 
34
28
  # Encode hex strings into a hash
35
29
  def encode_hex(hexs)
36
- encode(integer_representation(hexs))
30
+ encode(hex_represention_encoder.hex_as_integers(hexs))
37
31
  end
38
32
 
39
33
  # Decode the hash to original array
@@ -46,12 +40,48 @@ module EncodedId
46
40
  # Decode hex strings from a hash
47
41
  def decode_hex(str)
48
42
  integers = encoded_id_generator.decode(convert_to_hash(str))
49
- integers_to_hex_strings(integers)
43
+ hex_represention_encoder.integers_as_hex(integers)
50
44
  end
51
45
 
52
46
  private
53
47
 
54
- attr_reader :salt, :length, :human_friendly_alphabet, :split_at, :hex_digit_encoding_group_size
48
+ attr_reader :salt,
49
+ :length,
50
+ :alphabet,
51
+ :split_at,
52
+ :split_with,
53
+ :hex_represention_encoder
54
+
55
+ def validate_alphabet(alphabet)
56
+ raise InvalidAlphabetError, "alphabet must be an instance of Alphabet" unless alphabet.is_a?(Alphabet)
57
+ alphabet
58
+ end
59
+
60
+ def validate_salt(salt)
61
+ raise InvalidConfigurationError, "Salt must be a string and longer than 3 characters" unless salt.is_a?(String) && salt.size > 3
62
+ salt
63
+ end
64
+
65
+ # Target length of the encoded string (the minimum but not maximum length)
66
+ def validate_length(length)
67
+ raise InvalidConfigurationError, "Length must be an integer greater than 0" unless length.is_a?(Integer) && length > 0
68
+ length
69
+ end
70
+
71
+ # Split the encoded string into groups of this size
72
+ def validate_split_at(split_at)
73
+ unless (split_at.is_a?(Integer) && split_at > 0) || split_at.nil?
74
+ raise InvalidConfigurationError, "Split at must be an integer greater than 0 or nil"
75
+ end
76
+ split_at
77
+ end
78
+
79
+ def validate_split_with(split_with, alphabet)
80
+ unless split_with.is_a?(String) && !alphabet.characters.include?(split_with)
81
+ raise InvalidConfigurationError, "Split with must be a string and not part of the alphabet"
82
+ end
83
+ split_with
84
+ end
55
85
 
56
86
  def prepare_input(value)
57
87
  inputs = value.is_a?(Array) ? value.map(&:to_i) : [value.to_i]
@@ -61,7 +91,7 @@ module EncodedId
61
91
  end
62
92
 
63
93
  def encoded_id_generator
64
- @encoded_id_generator ||= ::Hashids.new(salt, length, human_friendly_alphabet)
94
+ @encoded_id_generator ||= ::Hashids.new(salt, length, alphabet.characters)
65
95
  end
66
96
 
67
97
  def split_regex
@@ -69,65 +99,19 @@ module EncodedId
69
99
  end
70
100
 
71
101
  def humanize_length(hash)
72
- hash.gsub(split_regex, '\0-')
102
+ hash.gsub(split_regex, "\\0#{split_with}")
73
103
  end
74
104
 
75
105
  def convert_to_hash(str)
76
- map_crockford_set(str.delete("-").downcase)
77
- end
78
-
79
- def map_crockford_set(str)
80
- # Crockford suggest i==1 , but I think i==j is more appropriate as we
81
- # only use lowercase
82
- str.tr("o", "0").tr("l", "1").tr("i", "j")
83
- end
84
-
85
- # TODO: optimize this
86
- def integer_representation(hexs)
87
- inputs = hexs.is_a?(Array) ? hexs.map(&:to_s) : [hexs.to_s]
88
- inputs.map! do |hex_string|
89
- cleaned = hex_string.gsub(/[^0-9a-f]/i, "")
90
- # Convert to groups of integers. Process least significant hex digits first
91
- groups = []
92
- cleaned.chars.reverse.each_with_index do |char, i|
93
- group_id = i / hex_digit_encoding_group_size.to_i
94
- groups[group_id] ||= []
95
- groups[group_id].unshift(char)
96
- end
97
- groups.map { |c| c.join.to_i(16) }
98
- end
99
- digits_to_encode = []
100
- inputs.each_with_object(digits_to_encode) do |hex_digits, digits|
101
- digits.concat(hex_digits)
102
- digits << hex_string_separator
103
- end
104
- digits_to_encode.pop unless digits_to_encode.empty? # Remove the last marker
105
- digits_to_encode
106
- end
107
-
108
- # Marker to separate hex strings, must be greater than largest value encoded
109
- def hex_string_separator
110
- @hex_string_separator ||= 2.pow(hex_digit_encoding_group_size * 4) + 1
111
- end
112
-
113
- # TODO: optimize this
114
- def integers_to_hex_strings(integers)
115
- hex_strings = []
116
- hex_string = []
117
- add_leading = false
118
- # Digits are encoded in least significant digit first order, but string is most significant first, so reverse
119
- integers.reverse_each do |integer|
120
- if integer == hex_string_separator # Marker to separate hex strings, so start a new one
121
- hex_strings << hex_string.join
122
- hex_string = []
123
- add_leading = false
124
- else
125
- hex_string << (add_leading ? "%.#{hex_digit_encoding_group_size}x" % integer : integer.to_s(16))
126
- add_leading = true
127
- end
106
+ clean = str.delete(split_with).downcase
107
+ alphabet.equivalences.nil? ? clean : map_equivalent_characters(clean)
108
+ end
109
+
110
+ def map_equivalent_characters(str)
111
+ alphabet.equivalences.reduce(str) do |cleaned, ceq|
112
+ from, to = ceq
113
+ cleaned.tr(from, to)
128
114
  end
129
- hex_strings << hex_string.join unless hex_string.empty? # Add the last hex string
130
- hex_strings.reverse # Reverse final values to get the original order (the encoding process also reverses the encoded value order)
131
115
  end
132
116
  end
133
117
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EncodedId
4
- VERSION = "0.3.0"
4
+ VERSION = "1.0.0.rc1"
5
5
  end
data/lib/encoded_id.rb CHANGED
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "encoded_id/version"
4
+ require_relative "encoded_id/alphabet"
5
+ require_relative "encoded_id/hex_representation"
4
6
  require_relative "encoded_id/reversible_id"
5
7
 
6
8
  module EncodedId
7
- class EncodedIdFormatError < ArgumentError; end
9
+ class InvalidConfigurationError < StandardError; end
8
10
 
9
11
  class InvalidAlphabetError < ArgumentError; end
10
12
 
13
+ class EncodedIdFormatError < ArgumentError; end
14
+
11
15
  class InvalidInputError < ArgumentError; end
12
16
  end
data/rbs_collection.yaml CHANGED
@@ -9,8 +9,6 @@ sources:
9
9
  path: .gem_rbs_collection
10
10
 
11
11
  gems:
12
- - name: encoded_id
13
- ignore: true
14
12
  # Skip loading rbs gem's RBS.
15
13
  # It's unnecessary if you don't use rbs as a library.
16
14
  - name: rbs
data/sig/encoded_id.rbs CHANGED
@@ -1,18 +1,65 @@
1
1
  module EncodedId
2
+ VERSION: ::String
3
+
4
+ InvalidConfigurationError: ::StandardError
2
5
  EncodedIdFormatError: ::ArgumentError
3
6
  InvalidAlphabetError: ::ArgumentError
4
7
  InvalidInputError: ::ArgumentError
5
8
 
6
- class ReversibleId
7
- ALPHABET: ::String
9
+ class Alphabet
10
+ MIN_UNIQUE_CHARACTERS: ::Integer
11
+
12
+ def initialize: (String, ?::Hash[::String, ::String]) -> void
13
+
14
+ attr_reader characters: String
15
+ attr_reader equivalences: ::Hash[::String, ::String]
16
+
17
+ def self.modified_crockford: () -> Alphabet
18
+
19
+ private
20
+
21
+ def valid_input_characters?: ((::Array[::String] | ::String) characters) -> bool
22
+
23
+ def sufficient_characters?: (::Integer size) -> bool
24
+
25
+ def unique_character_alphabet: ((::Array[::String] | ::String) characters) -> ::Array[::String]
26
+
27
+ def valid_equivalences?: (::Hash[::String, ::String] equivalences, ::Array[::String] unique_characters) -> bool
28
+
29
+ def raise_character_set_too_small!: -> untyped
30
+
31
+ def raise_invalid_alphabet!: -> void
8
32
 
9
- def initialize: (salt: ::String, ?length: ::Integer, ?split_at: ::Integer, ?alphabet: ::String, ?hex_digit_encoding_group_size: ::Integer) -> void
33
+ def raise_invalid_equivalences!: -> void
34
+ end
35
+
36
+ type encodeableValue = ::Array[::String | ::Integer] | ::String | ::Integer
37
+ type encodeableHexValue = ::Array[::String] | ::String
38
+
39
+ class HexRepresentation
40
+ def initialize: (::Integer) -> void
41
+ def hex_as_integers: (encodeableHexValue) -> ::Array[::Integer]
42
+ def integers_as_hex: (::Array[::Integer]) -> ::Array[::String]
43
+
44
+ private
45
+
46
+ def validate_hex_digit_encoding_group_size: (::Integer) -> ::Integer
47
+ def integer_representation: (encodeableHexValue) -> ::Array[::Integer]
48
+ def integers_to_hex_strings: (::Array[::Integer]) -> ::Array[::String]
49
+ def hex_string_as_integer_representation: (::String) -> ::Array[::Integer]
50
+ def hex_string_separator: -> ::Integer
51
+ def remove_non_hex_characters: (::String) -> ::String
52
+ def convert_to_integer_groups: (::String) -> ::Array[::Integer]
53
+ end
54
+
55
+ class ReversibleId
56
+ def initialize: (salt: ::String, ?length: ::Integer, ?split_at: ::Integer, ?split_with: ::String, ?alphabet: Alphabet, ?hex_digit_encoding_group_size: ::Integer) -> void
10
57
 
11
58
  # Encode the input values into a hash
12
- def encode: (untyped values) -> ::String
59
+ def encode: (encodeableValue values) -> ::String
13
60
 
14
61
  # Encode hex strings into a hash
15
- def encode_hex: (untyped hexs) -> ::String
62
+ def encode_hex: (encodeableHexValue hexs) -> ::String
16
63
 
17
64
  # Decode the hash to original array
18
65
  def decode: (::String str) -> ::Array[::Integer]
@@ -30,11 +77,19 @@ module EncodedId
30
77
 
31
78
  attr_reader length: ::Integer
32
79
 
33
- attr_reader human_friendly_alphabet: ::String
80
+ attr_reader alphabet: Alphabet
34
81
 
35
82
  attr_reader split_at: ::Integer | nil
83
+ attr_reader split_with: ::String
84
+
85
+ attr_reader hex_represention_encoder: HexRepresentation
36
86
 
37
- attr_reader hex_digit_encoding_group_size: ::Integer
87
+ def validate_alphabet: (Alphabet) -> Alphabet
88
+ def validate_salt: (::String) -> ::String
89
+ def validate_length: (::Integer) -> ::Integer
90
+ def validate_split_at: (::Integer | nil) -> (::Integer | nil)
91
+ def validate_split_with: (::String, Alphabet) -> ::String
92
+ def validate_hex_digit_encoding_group_size: (::Integer) -> ::Integer
38
93
 
39
94
  def prepare_input: (untyped value) -> ::Array[::Integer]
40
95
 
@@ -46,12 +101,6 @@ module EncodedId
46
101
 
47
102
  def convert_to_hash: (::String str) -> ::String
48
103
 
49
- def map_crockford_set: (::String str) -> ::String
50
-
51
- def integer_representation: (untyped hexs) -> ::Array[::Integer]
52
-
53
- def integers_to_hex_strings: (::Array[::Integer] integers) -> ::Array[::String]
54
-
55
- def hex_string_separator: () -> ::Integer
104
+ def map_equivalent_characters: (::String str) -> ::String
56
105
  end
57
106
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: encoded_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0.rc1
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-10-12 00:00:00.000000000 Z
11
+ date: 2023-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashids
@@ -42,6 +42,8 @@ files:
42
42
  - Rakefile
43
43
  - Steepfile
44
44
  - lib/encoded_id.rb
45
+ - lib/encoded_id/alphabet.rb
46
+ - lib/encoded_id/hex_representation.rb
45
47
  - lib/encoded_id/reversible_id.rb
46
48
  - lib/encoded_id/version.rb
47
49
  - rbs_collection.yaml
@@ -65,11 +67,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
65
67
  version: 2.7.0
66
68
  required_rubygems_version: !ruby/object:Gem::Requirement
67
69
  requirements:
68
- - - ">="
70
+ - - ">"
69
71
  - !ruby/object:Gem::Version
70
- version: '0'
72
+ version: 1.3.1
71
73
  requirements: []
72
- rubygems_version: 3.3.7
74
+ rubygems_version: 3.4.10
73
75
  signing_key:
74
76
  specification_version: 4
75
77
  summary: EncodedId is a gem for creating reversible obfuscated IDs from numerical