encoded_id 0.3.0 → 1.0.0.rc1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/Gemfile +2 -2
- data/README.md +243 -43
- data/lib/encoded_id/alphabet.rb +65 -0
- data/lib/encoded_id/hex_representation.rb +89 -0
- data/lib/encoded_id/reversible_id.rb +56 -72
- data/lib/encoded_id/version.rb +1 -1
- data/lib/encoded_id.rb +5 -1
- data/rbs_collection.yaml +0 -2
- data/sig/encoded_id.rbs +63 -14
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a860b12577a70042f6d17a1788cb8557f3ab5ab477441a44c3be037b70165ded
|
4
|
+
data.tar.gz: bd9d57c92ada9e47a1080ea5b4d04cb3069a055a213305b698f1a2c7c424297f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/README.md
CHANGED
@@ -1,65 +1,76 @@
|
|
1
1
|
# EncodedId
|
2
2
|
|
3
|
-
Encode
|
3
|
+
Encode numerical or hex IDs into obfuscated strings that can be used in URLs.
|
4
4
|
|
5
|
-
|
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
|
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
|
-
|
17
|
+
```ruby
|
18
|
+
my_salt = "salt!"
|
19
|
+
coder = ::EncodedId::ReversibleId.new(salt: my_salt)
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
+
# One of more values can be encoded
|
22
|
+
coder.encode([78, 45])
|
23
|
+
# => "z2j7-0dmw"
|
21
24
|
|
22
|
-
|
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
|
-
|
26
|
-
=>
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
##
|
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
|
-
|
114
|
+
### `length`
|
76
115
|
|
77
|
-
|
116
|
+
`length`: the minimum length of the encoded string. The default is 8 characters.
|
78
117
|
|
79
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
131
|
+
alphabet = EncodedId::Alphabet.new("0123456789abcdef")
|
132
|
+
```
|
133
|
+
|
134
|
+
`EncodedId::Alphabet.new(characters, equivalences)`
|
85
135
|
|
86
|
-
|
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/
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
@
|
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(
|
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
|
-
|
43
|
+
hex_represention_encoder.integers_as_hex(integers)
|
50
44
|
end
|
51
45
|
|
52
46
|
private
|
53
47
|
|
54
|
-
attr_reader :salt,
|
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,
|
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,
|
102
|
+
hash.gsub(split_regex, "\\0#{split_with}")
|
73
103
|
end
|
74
104
|
|
75
105
|
def convert_to_hash(str)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
data/lib/encoded_id/version.rb
CHANGED
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
|
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
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
|
7
|
-
|
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
|
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: (
|
59
|
+
def encode: (encodeableValue values) -> ::String
|
13
60
|
|
14
61
|
# Encode hex strings into a hash
|
15
|
-
def encode_hex: (
|
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
|
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
|
-
|
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
|
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.
|
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:
|
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:
|
72
|
+
version: 1.3.1
|
71
73
|
requirements: []
|
72
|
-
rubygems_version: 3.
|
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
|