encoded_id 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/README.md +22 -5
- data/Steepfile +5 -0
- data/lib/encoded_id/reversible_id.rb +78 -12
- data/lib/encoded_id/version.rb +1 -1
- data/lib/encoded_id.rb +4 -2
- data/rbs_collection.yaml +26 -0
- data/sig/encoded_id.rbs +57 -2
- data/sig/hash_ids.rbs +72 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7470fbb909ebc82c703fce3e91eecce54ef2231e51f3e2146766d4990992bf6
|
4
|
+
data.tar.gz: 911c4dd0d467e7746484b1c06939ab911e267887a62f77713132a55b069b600e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b21dcfcd8c816ee3b30bc50dbe4254e3ada6a619f2efa416fbca6518d43ac3143edf35d36c2b6130a25bc43bcb57fef6cf28bf13c31f1d9c1c6da327c3b9c2c9
|
7
|
+
data.tar.gz: 571c5b8156462dbc991a3211c2d631bcee9d03d867adff9f2f7f81436a284560824cbef3c5745df0c3cf2585a55c0a2bf3954f154a930d149b8a0d41bf7ae9b9
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -8,9 +8,9 @@ The obfuscated strings are reversible, so you can decode them back into the orig
|
|
8
8
|
encoding multiple IDs at once.
|
9
9
|
|
10
10
|
```
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
reversibles = ::EncodedId::ReversibleId.new(salt: my_salt)
|
12
|
+
reversibles.encode([78, 45]) # "7aq6-0zqw"
|
13
|
+
reversibles.decode("7aq6-0zqw") # [78, 45]
|
14
14
|
```
|
15
15
|
|
16
16
|
Length of the ID, the alphabet used, and the number of characters per group can be configured.
|
@@ -19,6 +19,13 @@ The custom alphabet (at least 16 characters needed) and character group sizes is
|
|
19
19
|
Easily confused characters (eg `i` and `j`, `0` and `O`, `1` and `I` etc) are mapped to counterpart characters, to help
|
20
20
|
common mistakes when sharing (eg customer over phone to customer service agent).
|
21
21
|
|
22
|
+
Also supports UUIDs if needed
|
23
|
+
|
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"
|
27
|
+
```
|
28
|
+
|
22
29
|
## Features
|
23
30
|
|
24
31
|
Build with https://hashids.org
|
@@ -26,10 +33,20 @@ Build with https://hashids.org
|
|
26
33
|
* Hashids are reversible, no need to persist the generated Id
|
27
34
|
* supports slugged IDs (eg 'beef-tenderloins-prime--p5w9-z27j')
|
28
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`
|
29
37
|
* uses a reduced character set (Crockford alphabet) & ids split into groups of letters, ie 'human-readability'
|
38
|
+
* profanity limitation
|
30
39
|
|
31
40
|
To use with **Rails** check out the `encoded_id-rails` gem.
|
32
41
|
|
42
|
+
## Note on security of encoded IDs (hashids)
|
43
|
+
|
44
|
+
**Encoded IDs are not secure**. It maybe possible to reverse them via brute-force. They are meant to be used in URLs as
|
45
|
+
an obfuscation. The algorithm is not an encryption.
|
46
|
+
|
47
|
+
Please read more on https://hashids.org/
|
48
|
+
|
49
|
+
|
33
50
|
## Compared to alternate Gems
|
34
51
|
|
35
52
|
- https://github.com/excid3/prefixed_ids
|
@@ -63,10 +80,10 @@ To use with rails try the `encoded_id-rails` gem.
|
|
63
80
|
|
64
81
|
```ruby
|
65
82
|
class User < ApplicationRecord
|
66
|
-
include EncodedId::
|
83
|
+
include EncodedId::WithEncodedId
|
67
84
|
end
|
68
85
|
|
69
|
-
User.
|
86
|
+
User.find_by_encoded_id("p5w9-z27j") # => #<User id: 78>
|
70
87
|
```
|
71
88
|
|
72
89
|
## Development
|
data/Steepfile
ADDED
@@ -5,12 +5,12 @@ require "hashids"
|
|
5
5
|
# Hashid with a reduced character set Crockford alphabet and split groups
|
6
6
|
# See: https://www.crockford.com/wrmg/base32.html
|
7
7
|
# Build with https://hashids.org
|
8
|
-
# Note hashIds already has a
|
8
|
+
# Note hashIds already has a built in profanity limitation algorithm
|
9
9
|
module EncodedId
|
10
10
|
class ReversibleId
|
11
11
|
ALPHABET = "0123456789abcdefghjkmnpqrstuvwxyz"
|
12
12
|
|
13
|
-
def initialize(salt:, length: 8, split_at: 4, alphabet: ALPHABET)
|
13
|
+
def initialize(salt:, length: 8, split_at: 4, alphabet: ALPHABET, hex_digit_encoding_group_size: 4)
|
14
14
|
unique_alphabet = alphabet.chars.uniq
|
15
15
|
raise InvalidAlphabetError, "Alphabet must be at least 16 characters" if unique_alphabet.size < 16
|
16
16
|
|
@@ -18,32 +18,50 @@ module EncodedId
|
|
18
18
|
@salt = salt
|
19
19
|
@length = length
|
20
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
|
21
24
|
end
|
22
25
|
|
23
26
|
# Encode the input values into a hash
|
24
|
-
def encode(
|
25
|
-
|
26
|
-
|
27
|
-
|
27
|
+
def encode(values)
|
28
|
+
inputs = prepare_input(values)
|
29
|
+
encoded_id = encoded_id_generator.encode(inputs)
|
30
|
+
encoded_id = humanize_length(encoded_id) unless split_at.nil?
|
31
|
+
encoded_id
|
32
|
+
end
|
33
|
+
|
34
|
+
# Encode hex strings into a hash
|
35
|
+
def encode_hex(hexs)
|
36
|
+
encode(integer_representation(hexs))
|
28
37
|
end
|
29
38
|
|
30
39
|
# Decode the hash to original array
|
31
40
|
def decode(str)
|
32
|
-
|
41
|
+
encoded_id_generator.decode(convert_to_hash(str))
|
33
42
|
rescue ::Hashids::InputError => e
|
34
43
|
raise EncodedIdFormatError, e.message
|
35
44
|
end
|
36
45
|
|
46
|
+
# Decode hex strings from a hash
|
47
|
+
def decode_hex(str)
|
48
|
+
integers = encoded_id_generator.decode(convert_to_hash(str))
|
49
|
+
integers_to_hex_strings(integers)
|
50
|
+
end
|
51
|
+
|
37
52
|
private
|
38
53
|
|
39
|
-
attr_reader :salt, :length, :human_friendly_alphabet, :split_at
|
54
|
+
attr_reader :salt, :length, :human_friendly_alphabet, :split_at, :hex_digit_encoding_group_size
|
55
|
+
|
56
|
+
def prepare_input(value)
|
57
|
+
inputs = value.is_a?(Array) ? value.map(&:to_i) : [value.to_i]
|
58
|
+
raise ::EncodedId::InvalidInputError, "Integer IDs to be encoded can only be positive" if inputs.any?(&:negative?)
|
40
59
|
|
41
|
-
|
42
|
-
@uid_generator ||= ::Hashids.new(salt, length, human_friendly_alphabet)
|
60
|
+
inputs
|
43
61
|
end
|
44
62
|
|
45
|
-
def
|
46
|
-
|
63
|
+
def encoded_id_generator
|
64
|
+
@encoded_id_generator ||= ::Hashids.new(salt, length, human_friendly_alphabet)
|
47
65
|
end
|
48
66
|
|
49
67
|
def split_regex
|
@@ -63,5 +81,53 @@ module EncodedId
|
|
63
81
|
# only use lowercase
|
64
82
|
str.tr("o", "0").tr("l", "1").tr("i", "j")
|
65
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
|
128
|
+
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
|
+
end
|
66
132
|
end
|
67
133
|
end
|
data/lib/encoded_id/version.rb
CHANGED
data/lib/encoded_id.rb
CHANGED
@@ -4,7 +4,9 @@ require_relative "encoded_id/version"
|
|
4
4
|
require_relative "encoded_id/reversible_id"
|
5
5
|
|
6
6
|
module EncodedId
|
7
|
-
class EncodedIdFormatError <
|
7
|
+
class EncodedIdFormatError < ArgumentError; end
|
8
8
|
|
9
|
-
class InvalidAlphabetError <
|
9
|
+
class InvalidAlphabetError < ArgumentError; end
|
10
|
+
|
11
|
+
class InvalidInputError < ArgumentError; end
|
10
12
|
end
|
data/rbs_collection.yaml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Download sources
|
2
|
+
sources:
|
3
|
+
- name: ruby/gem_rbs_collection
|
4
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
5
|
+
revision: main
|
6
|
+
repo_dir: gems
|
7
|
+
|
8
|
+
# A directory to install the downloaded RBSs
|
9
|
+
path: .gem_rbs_collection
|
10
|
+
|
11
|
+
gems:
|
12
|
+
- name: encoded_id
|
13
|
+
ignore: true
|
14
|
+
# Skip loading rbs gem's RBS.
|
15
|
+
# It's unnecessary if you don't use rbs as a library.
|
16
|
+
- name: rbs
|
17
|
+
ignore: true
|
18
|
+
- name: rake
|
19
|
+
ignore: true
|
20
|
+
- name: minitest
|
21
|
+
ignore: true
|
22
|
+
- name: standard
|
23
|
+
ignore: true
|
24
|
+
- name: steep
|
25
|
+
ignore: true
|
26
|
+
|
data/sig/encoded_id.rbs
CHANGED
@@ -1,4 +1,59 @@
|
|
1
1
|
module EncodedId
|
2
|
-
VERSION: String
|
3
|
-
|
2
|
+
VERSION: ::String
|
3
|
+
|
4
|
+
EncodedIdFormatError: ::ArgumentError
|
5
|
+
InvalidAlphabetError: ::ArgumentError
|
6
|
+
InvalidInputError: ::ArgumentError
|
7
|
+
|
8
|
+
class ReversibleId
|
9
|
+
ALPHABET: ::String
|
10
|
+
|
11
|
+
def initialize: (salt: ::String, ?length: ::Integer, ?split_at: ::Integer, ?alphabet: ::String, ?hex_digit_encoding_group_size: ::Integer) -> void
|
12
|
+
|
13
|
+
# Encode the input values into a hash
|
14
|
+
def encode: (untyped values) -> ::String
|
15
|
+
|
16
|
+
# Encode hex strings into a hash
|
17
|
+
def encode_hex: (untyped hexs) -> ::String
|
18
|
+
|
19
|
+
# Decode the hash to original array
|
20
|
+
def decode: (::String str) -> ::Array[::Integer]
|
21
|
+
|
22
|
+
# Decode hex strings from a hash
|
23
|
+
def decode_hex: (::String str) -> ::Array[::String]
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
@encoded_id_generator: ::Hashids
|
28
|
+
@split_regex: ::Regexp
|
29
|
+
@hex_string_separator: ::Integer
|
30
|
+
|
31
|
+
attr_reader salt: ::String
|
32
|
+
|
33
|
+
attr_reader length: ::Integer
|
34
|
+
|
35
|
+
attr_reader human_friendly_alphabet: ::String
|
36
|
+
|
37
|
+
attr_reader split_at: ::Integer | nil
|
38
|
+
|
39
|
+
attr_reader hex_digit_encoding_group_size: ::Integer
|
40
|
+
|
41
|
+
def prepare_input: (untyped value) -> ::Array[::Integer]
|
42
|
+
|
43
|
+
def encoded_id_generator: () -> ::Hashids
|
44
|
+
|
45
|
+
def split_regex: () -> ::Regexp
|
46
|
+
|
47
|
+
def humanize_length: (::String hash) -> ::String
|
48
|
+
|
49
|
+
def convert_to_hash: (::String str) -> ::String
|
50
|
+
|
51
|
+
def map_crockford_set: (::String str) -> ::String
|
52
|
+
|
53
|
+
def integer_representation: (untyped hexs) -> ::Array[::Integer]
|
54
|
+
|
55
|
+
def integers_to_hex_strings: (::Array[::Integer] integers) -> ::Array[::String]
|
56
|
+
|
57
|
+
def hex_string_separator: () -> ::Integer
|
58
|
+
end
|
4
59
|
end
|
data/sig/hash_ids.rbs
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
class Hashids
|
2
|
+
VERSION: ::String
|
3
|
+
|
4
|
+
MIN_ALPHABET_LENGTH: ::Integer
|
5
|
+
|
6
|
+
SEP_DIV: ::Float
|
7
|
+
|
8
|
+
GUARD_DIV: ::Float
|
9
|
+
|
10
|
+
DEFAULT_SEPS: ::String
|
11
|
+
|
12
|
+
DEFAULT_ALPHABET: ::String
|
13
|
+
|
14
|
+
attr_reader salt: ::String
|
15
|
+
|
16
|
+
attr_reader min_hash_length: ::Integer
|
17
|
+
|
18
|
+
attr_reader alphabet: ::String
|
19
|
+
|
20
|
+
attr_reader seps: ::String
|
21
|
+
|
22
|
+
attr_reader guards: untyped
|
23
|
+
|
24
|
+
def initialize: (?::String salt, ?::Integer min_hash_length, ?untyped alphabet) -> void
|
25
|
+
|
26
|
+
def encode: (*(Array[::Integer] | ::Integer) numbers) -> ::String
|
27
|
+
|
28
|
+
def encode_hex: (::String str) -> ::String
|
29
|
+
|
30
|
+
def decode: (::String hash) -> ::Array[::Integer]
|
31
|
+
|
32
|
+
def decode_hex: (::String hash) -> ::Array[::Integer]
|
33
|
+
|
34
|
+
# protected
|
35
|
+
|
36
|
+
def internal_encode: (untyped numbers) -> untyped
|
37
|
+
|
38
|
+
def internal_decode: (untyped hash, untyped alphabet) -> untyped
|
39
|
+
|
40
|
+
def consistent_shuffle: (untyped alphabet, untyped salt) -> untyped
|
41
|
+
|
42
|
+
def hash: (untyped input, untyped alphabet) -> untyped
|
43
|
+
|
44
|
+
def unhash: (untyped input, untyped alphabet) -> untyped
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def setup_alphabet: () -> untyped
|
49
|
+
|
50
|
+
def setup_seps: () -> untyped
|
51
|
+
|
52
|
+
def setup_guards: () -> untyped
|
53
|
+
|
54
|
+
SaltError: ArgumentError
|
55
|
+
|
56
|
+
MinLengthError: ArgumentError
|
57
|
+
|
58
|
+
AlphabetError: ArgumentError
|
59
|
+
|
60
|
+
InputError: ArgumentError
|
61
|
+
|
62
|
+
def validate_attributes: () -> untyped
|
63
|
+
|
64
|
+
def validate_alphabet: () -> (untyped | nil)
|
65
|
+
|
66
|
+
def hex_string?: (untyped string) -> untyped
|
67
|
+
|
68
|
+
def pick_characters: (untyped array, untyped index) -> untyped
|
69
|
+
|
70
|
+
def uniq_characters: (untyped string) -> untyped
|
71
|
+
end
|
72
|
+
|
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: 0.2.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-10-
|
11
|
+
date: 2022-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hashids
|
@@ -40,10 +40,13 @@ files:
|
|
40
40
|
- LICENSE.txt
|
41
41
|
- README.md
|
42
42
|
- Rakefile
|
43
|
+
- Steepfile
|
43
44
|
- lib/encoded_id.rb
|
44
45
|
- lib/encoded_id/reversible_id.rb
|
45
46
|
- lib/encoded_id/version.rb
|
47
|
+
- rbs_collection.yaml
|
46
48
|
- sig/encoded_id.rbs
|
49
|
+
- sig/hash_ids.rbs
|
47
50
|
homepage: https://github.com/stevegeek/encoded_id
|
48
51
|
licenses:
|
49
52
|
- MIT
|
@@ -66,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
69
|
- !ruby/object:Gem::Version
|
67
70
|
version: '0'
|
68
71
|
requirements: []
|
69
|
-
rubygems_version: 3.
|
72
|
+
rubygems_version: 3.3.7
|
70
73
|
signing_key:
|
71
74
|
specification_version: 4
|
72
75
|
summary: EncodedId is a gem for creating reversible obfuscated IDs from numerical
|