encoded_id 1.0.0.rc2 → 1.0.0.rc3

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: 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