encoded_id 1.0.0.rc2 → 1.0.0.rc3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7e11278f039a19d4fa201541b907a3f52e5b73c440ba0870c6e8a4fd068bb99
4
- data.tar.gz: 70d726b45f982670895c1ddccbbf7e9f42b4a6ab3e6eca8439ac5e58ce399fe0
3
+ metadata.gz: 44375d994a7eabe8245cf6bcb0e889e4d9de13f3e67feb98ee77959bc8f076a5
4
+ data.tar.gz: 4e6db397ee9564375ae7387d57f341266f22fd58214ccecc890b7fb68d8cc4ab
5
5
  SHA512:
6
- metadata.gz: fcfad8b207316c8041ddd6cd8687a17b9e04c5bcdca8b73084aae2e4a241c5b2c10247fe24f513e66d939ab97289bb1206458f0fab70cc6a04415224ec62083b
7
- data.tar.gz: c3d4148b9ddddac5eb2dc47b58c2beb6e88e0e3be9c207328e36c825adbce05826c3eaeb6b4d2023160da65286c177e3a3085f222ba7ba7eb529dab995939cb3
6
+ metadata.gz: aade9242ac4d6fffd5ea3957af76a065d14143e547e31b4e7fda5203dcb2aa27f05166e5e803a8677f874bb443fc136196f0ce3bdd78b514606f251721f95d27
7
+ data.tar.gz: fa4cc2385cbb6a726fc00bdbb5e70d1546abe46b86ba020243327942f32886a4928b8b65bc7a76c9e134bb162c17f4c69798a5e0080c43bf2e7802f92dfa580f
data/CHANGELOG.md CHANGED
@@ -1,15 +1,32 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [1.0.0] - 2023-08-06
3
+ ## [2.0.0.alpha1] - unreleased
4
+
5
+ ### Breaking changes
6
+
7
+ - `ReversibleId` now no longer downcases the encodedid input string by default on decode, ie the `decode` option `downcase` is now `false`. In a future release the `downcase` option will be removed.
8
+
9
+ ## [1.0.0.rc3] - 2023-10-23
10
+
11
+ - Add an optional `max_length` argument to `ReversibleId`, thanks to [@jugglebird](https://github.com/jugglebird)
12
+ - Alphabet validations to prevent whitespace and null chars
13
+ - Add `Alphabet#to_a`, `Alphabet#to_s`, `Alphabet#size` and a custom `Alphabet#inspect`
14
+ - Fixes to input validations
15
+ - hashids are case-sensitive, as are `Alphabet`s, however `ReversibleId` was always `downcase`ing the encodedid input string on decode. A new option has been added to `decode` and `decode_hex`, `downcase`, which defaults to `true`. Thus, the default behaviour is unchanged, but you can opt out to allow mixed case encodedid decode. *Note:* In V2 this will default to `false`.
16
+
17
+ ## [1.0.0.rc2] - 2023-08-07
18
+
19
+ - `Alphabet` now has `#include?` and `#unique_charaters` methods
20
+
21
+ ## [1.0.0.rc1] - 2023-08-06
4
22
 
5
23
  - Improved RBS definitions
6
24
  - Improved test coverage
7
- - `Alphabet` now has `#include?` and `#unique_charaters` methods
8
25
 
9
26
  ## [0.4.0] - 2022-12-04
10
27
 
11
- - Support custom split character which must not be in the alphabet
12
- - Ability to provide a custom character equivalences mapping
28
+ - Support custom 'split' character which must not be in the alphabet
29
+ - Ability to provide a custom character equivalence mapping
13
30
 
14
31
  ## [0.3.0] - 2022-10-12
15
32
 
data/Gemfile CHANGED
@@ -7,8 +7,18 @@ gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
9
 
10
- gem "minitest", "~> 5.0"
10
+ gem "minitest"
11
11
 
12
12
  gem "standard", "~> 1.30"
13
13
 
14
- gem "steep", "~> 1.5"
14
+ gem "rbs"
15
+
16
+ gem "steep"
17
+
18
+ gem "simplecov"
19
+
20
+ gem "benchmark-ips"
21
+
22
+ gem "benchmark-memory"
23
+
24
+ gem "fuzzbert"
data/README.md CHANGED
@@ -33,16 +33,27 @@ coder.decode("z2j7-Odmw") # (note the capital 'o' instead of zero)
33
33
 
34
34
  ## Features
35
35
 
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)
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 custom alphabets for the encoded string (at least 16 characters needed)
40
39
  - 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
40
+ - 👓 easily confused characters (eg `i` and `j`, `0` and `O`, `1` and `I` etc) are mapped to counterpart characters, to help
42
41
  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
42
+ - 🤬 build in profanity limitation
43
+ * 🤓 encoded string can be split into groups of letters to improve human-readability
45
44
  - eg `nft9hr834htu` as `nft9-hr83-4htu`
45
+ * 🥽 supports limits on length to prevent resource exhaustion on encoding and decoding
46
+ * configured with sensible defaults
47
+
48
+ I aim for 100% test coverage and have fuzz tested quite extensively. But please report any issues!
49
+
50
+ ### Experimental
51
+
52
+ * support for encoding of hex strings (eg UUIDs), including multiple IDs encoded in one string
53
+
54
+ ### Coming soon
55
+
56
+ Performance improvements and benchmarking!
46
57
 
47
58
  ### Rails support `encoded_id-rails`
48
59
 
@@ -117,9 +128,18 @@ The encoded ID is configurable. The following can be changed:
117
128
 
118
129
  The actual length of the encoded string can be longer if the inputs cannot be represented in the minimum length.
119
130
 
131
+ ### `max_length`
132
+
133
+ `max_length`: the maximum length of the encoded string. The default is 128 characters.
134
+
135
+ The maximum length represents both the longest encoded string that will be generated and also a limit on
136
+ the maximum input length that will be decoded. If the encoded string exceeds `max_length` then a
137
+ `EncodedIdLengthError` will be raised. If the input exceeds `max_length` then a `InvalidInputError` will
138
+ be raised. If `max_length` is set to `nil`, then no validation, even using the default will be performed.
139
+
120
140
  ### `alphabet`
121
141
 
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).
142
+ `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
143
 
124
144
  `alphabet` must be an instance of `EncodedId::Alphabet`.
125
145
 
@@ -137,7 +157,8 @@ alphabet = EncodedId::Alphabet.new("0123456789abcdef")
137
157
 
138
158
  `characters`: the characters of the alphabet. Can be a string or array of strings.
139
159
 
140
- Note that the `characters` of the alphabet must be at least 16 _unique_ characters long.
160
+ Note that the `characters` of the alphabet must be at least 16 _unique_ characters long and must not contain any
161
+ whitespace characters.
141
162
 
142
163
 
143
164
  ```ruby
@@ -268,16 +289,17 @@ coder.decode_hex("5jjy-c8d9-hxp2-qsve-rgh9-rxnt-7nb5-tve7-bf84-vr")
268
289
 
269
290
  ## Development
270
291
 
271
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also
272
- run `bin/console` for an interactive prompt that will allow you to experiment.
292
+ After checking out the repo, run `bin/setup` to install dependencies.
293
+
294
+ Run `bin/console` for an interactive prompt that will allow you to experiment.
273
295
 
274
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version
275
- number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git
276
- commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
296
+ ### Running tests
297
+
298
+ Run `bundle exec rake test` to run the tests.
277
299
 
278
300
  ### Type check
279
301
 
280
- First install dependencies:
302
+ First install RBS dependencies:
281
303
 
282
304
  ```bash
283
305
  rbs collection install
@@ -289,7 +311,6 @@ Then run:
289
311
  steep check
290
312
  ```
291
313
 
292
-
293
314
  ## See also
294
315
 
295
316
  - https://hashids.org
@@ -20,8 +20,9 @@ module EncodedId
20
20
  def initialize(characters, equivalences = nil)
21
21
  raise_invalid_alphabet! unless valid_input_characters?(characters)
22
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)
23
+ raise_invalid_alphabet! unless valid_characters?
24
+ raise_character_set_too_small! unless sufficient_characters?
25
+ raise_invalid_equivalences! unless valid_equivalences?(equivalences)
25
26
 
26
27
  @characters = unique_characters.join
27
28
  @equivalences = equivalences
@@ -33,29 +34,52 @@ module EncodedId
33
34
  unique_characters.include?(character)
34
35
  end
35
36
 
37
+ def to_a
38
+ unique_characters.dup
39
+ end
40
+
41
+ def to_s
42
+ @characters.dup
43
+ end
44
+
45
+ def inspect
46
+ "#<#{self.class.name} chars: #{unique_characters.inspect}>"
47
+ end
48
+
49
+ def size
50
+ unique_characters.size
51
+ end
52
+ alias_method :length, :size
53
+
36
54
  private
37
55
 
38
56
  def valid_input_characters?(characters)
39
- (characters.is_a?(Array) || characters.is_a?(String)) && characters.size > 0
57
+ return false unless characters.is_a?(Array) || characters.is_a?(String)
58
+ characters.size > 0
40
59
  end
41
60
 
42
61
  def unique_character_alphabet(characters)
43
62
  (characters.is_a?(Array) ? characters : characters.chars).uniq
44
63
  end
45
64
 
46
- def sufficient_characters?(size)
47
- size >= MIN_UNIQUE_CHARACTERS
65
+ def valid_characters?
66
+ unique_characters.size > 0 && unique_characters.grep(/\s|\0/).size == 0
67
+ end
68
+
69
+ def sufficient_characters?
70
+ unique_characters.size >= MIN_UNIQUE_CHARACTERS
48
71
  end
49
72
 
50
- def valid_equivalences?(equivalences, unique_characters)
73
+ def valid_equivalences?(equivalences)
51
74
  return true if equivalences.nil?
52
75
  return false unless equivalences.is_a?(Hash)
76
+ return false if equivalences.any? { |key, value| key.size != 1 || value.size != 1 }
53
77
 
54
78
  (unique_characters & equivalences.keys).empty? && (equivalences.values - unique_characters).empty?
55
79
  end
56
80
 
57
81
  def raise_invalid_alphabet!
58
- raise InvalidAlphabetError, "Alphabet must be a string or array."
82
+ raise InvalidAlphabetError, "Alphabet must be a string or array and not contain whitespace."
59
83
  end
60
84
 
61
85
  def raise_character_set_too_small!
@@ -63,7 +87,7 @@ module EncodedId
63
87
  end
64
88
 
65
89
  def raise_invalid_equivalences!
66
- raise InvalidConfigurationError, "Character equivalences must be a hash or nil."
90
+ raise InvalidConfigurationError, "Character equivalences must be a hash or nil and contain mappings to valid alphabet characters."
67
91
  end
68
92
  end
69
93
  end
@@ -8,13 +8,14 @@ require "hashids"
8
8
  # Note hashIds already has a built in profanity limitation algorithm
9
9
  module EncodedId
10
10
  class ReversibleId
11
- def initialize(salt:, length: 8, split_at: 4, split_with: "-", alphabet: Alphabet.modified_crockford, hex_digit_encoding_group_size: 4)
11
+ def initialize(salt:, length: 8, split_at: 4, split_with: "-", alphabet: Alphabet.modified_crockford, hex_digit_encoding_group_size: 4, max_length: 128)
12
12
  @alphabet = validate_alphabet(alphabet)
13
13
  @salt = validate_salt(salt)
14
14
  @length = validate_length(length)
15
15
  @split_at = validate_split_at(split_at)
16
16
  @split_with = validate_split_with(split_with, alphabet)
17
17
  @hex_represention_encoder = HexRepresentation.new(hex_digit_encoding_group_size)
18
+ @max_length = validate_max_length(max_length)
18
19
  end
19
20
 
20
21
  # Encode the input values into a hash
@@ -22,6 +23,9 @@ module EncodedId
22
23
  inputs = prepare_input(values)
23
24
  encoded_id = encoded_id_generator.encode(inputs)
24
25
  encoded_id = humanize_length(encoded_id) unless split_at.nil?
26
+
27
+ raise EncodedIdLengthError if max_length_exceeded?(encoded_id)
28
+
25
29
  encoded_id
26
30
  end
27
31
 
@@ -31,15 +35,17 @@ module EncodedId
31
35
  end
32
36
 
33
37
  # Decode the hash to original array
34
- def decode(str)
35
- encoded_id_generator.decode(convert_to_hash(str))
38
+ def decode(str, downcase: true)
39
+ raise InvalidInputError if max_length_exceeded?(str)
40
+
41
+ encoded_id_generator.decode(convert_to_hash(str, downcase))
36
42
  rescue ::Hashids::InputError => e
37
43
  raise EncodedIdFormatError, e.message
38
44
  end
39
45
 
40
46
  # Decode hex strings from a hash
41
- def decode_hex(str)
42
- integers = encoded_id_generator.decode(convert_to_hash(str))
47
+ def decode_hex(str, downcase: true)
48
+ integers = encoded_id_generator.decode(convert_to_hash(str, downcase))
43
49
  hex_represention_encoder.integers_as_hex(integers)
44
50
  end
45
51
 
@@ -50,37 +56,43 @@ module EncodedId
50
56
  :alphabet,
51
57
  :split_at,
52
58
  :split_with,
53
- :hex_represention_encoder
59
+ :hex_represention_encoder,
60
+ :max_length
54
61
 
55
62
  def validate_alphabet(alphabet)
56
- raise InvalidAlphabetError, "alphabet must be an instance of Alphabet" unless alphabet.is_a?(Alphabet)
57
- alphabet
63
+ return alphabet if alphabet.is_a?(Alphabet)
64
+ raise InvalidAlphabetError, "alphabet must be an instance of Alphabet"
58
65
  end
59
66
 
60
67
  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
68
+ return salt if salt.is_a?(String) && salt.size > 3
69
+ raise InvalidConfigurationError, "Salt must be a string and longer than 3 characters"
63
70
  end
64
71
 
65
72
  # Target length of the encoded string (the minimum but not maximum length)
66
73
  def validate_length(length)
67
- raise InvalidConfigurationError, "Length must be an integer greater than 0" unless length.is_a?(Integer) && length > 0
68
- length
74
+ return length if valid_integer_option?(length)
75
+ raise InvalidConfigurationError, "Length must be an integer greater than 0"
76
+ end
77
+
78
+ def validate_max_length(max_length)
79
+ return max_length if valid_integer_option?(max_length) || max_length.nil?
80
+ raise InvalidConfigurationError, "Max length must be an integer greater than 0"
69
81
  end
70
82
 
71
83
  # Split the encoded string into groups of this size
72
84
  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
85
+ return split_at if valid_integer_option?(split_at) || split_at.nil?
86
+ raise InvalidConfigurationError, "Split at must be an integer greater than 0 or nil"
77
87
  end
78
88
 
79
89
  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
90
+ return split_with if split_with.is_a?(String) && !alphabet.characters.include?(split_with)
91
+ raise InvalidConfigurationError, "Split with must be a string and not part of the alphabet"
92
+ end
93
+
94
+ def valid_integer_option?(value)
95
+ value.is_a?(Integer) && value > 0
84
96
  end
85
97
 
86
98
  def prepare_input(value)
@@ -102,16 +114,25 @@ module EncodedId
102
114
  hash.gsub(split_regex, "\\0#{split_with}")
103
115
  end
104
116
 
105
- def convert_to_hash(str)
106
- clean = str.delete(split_with).downcase
107
- alphabet.equivalences.nil? ? clean : map_equivalent_characters(clean)
117
+ def convert_to_hash(str, downcase)
118
+ clean = str.gsub(split_with, "")
119
+ clean = clean.downcase if downcase
120
+ map_equivalent_characters(clean)
108
121
  end
109
122
 
110
123
  def map_equivalent_characters(str)
124
+ return str unless alphabet.equivalences
125
+
111
126
  alphabet.equivalences.reduce(str) do |cleaned, ceq|
112
127
  from, to = ceq
113
128
  cleaned.tr(from, to)
114
129
  end
115
130
  end
131
+
132
+ def max_length_exceeded?(str)
133
+ return false if max_length.nil?
134
+
135
+ str.length > max_length
136
+ end
116
137
  end
117
138
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EncodedId
4
- VERSION = "1.0.0.rc2"
4
+ VERSION = "1.0.0.rc3"
5
5
  end
data/lib/encoded_id.rb CHANGED
@@ -12,5 +12,7 @@ module EncodedId
12
12
 
13
13
  class EncodedIdFormatError < ArgumentError; end
14
14
 
15
+ class EncodedIdLengthError < ArgumentError; end
16
+
15
17
  class InvalidInputError < ArgumentError; end
16
18
  end
data/sig/encoded_id.rbs CHANGED
@@ -3,17 +3,18 @@ module EncodedId
3
3
 
4
4
  InvalidConfigurationError: ::StandardError
5
5
  EncodedIdFormatError: ::ArgumentError
6
+ EncodedIdLengthError: ::ArgumentError
6
7
  InvalidAlphabetError: ::ArgumentError
7
8
  InvalidInputError: ::ArgumentError
8
9
 
9
10
  class Alphabet
10
11
  MIN_UNIQUE_CHARACTERS: ::Integer
11
12
 
12
- def initialize: (String, ?::Hash[::String, ::String]) -> void
13
+ def initialize: (String | ::Array[::String] characters, ::Hash[::String, ::String] ?equivalences) -> void
13
14
 
14
15
  attr_reader unique_characters: ::Array[::String]
15
16
  attr_reader characters: String
16
- attr_reader equivalences: ::Hash[::String, ::String]
17
+ attr_reader equivalences: ::Hash[::String, ::String] | nil
17
18
 
18
19
  def include?: (::String character) -> bool
19
20
 
@@ -23,11 +24,13 @@ module EncodedId
23
24
 
24
25
  def valid_input_characters?: ((::Array[::String] | ::String) characters) -> bool
25
26
 
26
- def sufficient_characters?: (::Integer size) -> bool
27
+ def valid_characters?: -> bool
28
+
29
+ def sufficient_characters?: -> bool
27
30
 
28
31
  def unique_character_alphabet: ((::Array[::String] | ::String) characters) -> ::Array[::String]
29
32
 
30
- def valid_equivalences?: (::Hash[::String, ::String] equivalences, ::Array[::String] unique_characters) -> bool
33
+ def valid_equivalences?: (::Hash[::String, ::String] ?equivalences) -> bool
31
34
 
32
35
  def raise_character_set_too_small!: -> untyped
33
36
 
@@ -56,7 +59,7 @@ module EncodedId
56
59
  end
57
60
 
58
61
  class ReversibleId
59
- def initialize: (salt: ::String, ?length: ::Integer, ?split_at: ::Integer, ?split_with: ::String, ?alphabet: Alphabet, ?hex_digit_encoding_group_size: ::Integer) -> void
62
+ def initialize: (salt: ::String, ?length: ::Integer, ?split_at: ::Integer, ?split_with: ::String, ?alphabet: Alphabet, ?hex_digit_encoding_group_size: ::Integer, ?max_length: ::Integer) -> void
60
63
 
61
64
  # Encode the input values into a hash
62
65
  def encode: (encodeableValue values) -> ::String
@@ -79,6 +82,7 @@ module EncodedId
79
82
  attr_reader salt: ::String
80
83
 
81
84
  attr_reader length: ::Integer
85
+ attr_reader max_length: ::Integer | nil
82
86
 
83
87
  attr_reader alphabet: Alphabet
84
88
 
@@ -90,9 +94,11 @@ module EncodedId
90
94
  def validate_alphabet: (Alphabet) -> Alphabet
91
95
  def validate_salt: (::String) -> ::String
92
96
  def validate_length: (::Integer) -> ::Integer
97
+ def validate_max_length: (::Integer | nil) -> (::Integer | nil)
93
98
  def validate_split_at: (::Integer | nil) -> (::Integer | nil)
94
99
  def validate_split_with: (::String, Alphabet) -> ::String
95
100
  def validate_hex_digit_encoding_group_size: (::Integer) -> ::Integer
101
+ def valid_integer_option?: (::Integer | nil) -> bool
96
102
 
97
103
  def prepare_input: (untyped value) -> ::Array[::Integer]
98
104
 
@@ -105,5 +111,6 @@ module EncodedId
105
111
  def convert_to_hash: (::String str) -> ::String
106
112
 
107
113
  def map_equivalent_characters: (::String str) -> ::String
114
+ def max_length_exceeded?: (::String str) -> bool
108
115
  end
109
116
  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: 1.0.0.rc2
4
+ version: 1.0.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-08-07 00:00:00.000000000 Z
11
+ date: 2023-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashids
@@ -71,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
71
  - !ruby/object:Gem::Version
72
72
  version: 1.3.1
73
73
  requirements: []
74
- rubygems_version: 3.4.10
74
+ rubygems_version: 3.4.20
75
75
  signing_key:
76
76
  specification_version: 4
77
77
  summary: EncodedId is a gem for creating reversible obfuscated IDs from numerical