encoded_id 1.0.0.rc6 → 1.0.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/CHANGELOG.md +29 -4
- data/README.md +44 -33
- data/context/encoded_id.md +229 -75
- data/lib/encoded_id/alphabet.rb +2 -0
- data/lib/encoded_id/blocklist.rb +17 -7
- data/lib/encoded_id/encoders/base_configuration.rb +145 -0
- data/lib/encoded_id/encoders/{hash_id.rb → hashid.rb} +53 -57
- data/lib/encoded_id/encoders/hashid_configuration.rb +33 -0
- data/lib/encoded_id/encoders/{hash_id_consistent_shuffle.rb → hashid_consistent_shuffle.rb} +4 -4
- data/lib/encoded_id/encoders/{hash_id_ordinal_alphabet_separator_guards.rb → hashid_ordinal_alphabet_separator_guards.rb} +28 -54
- data/lib/encoded_id/encoders/{hash_id_salt.rb → hashid_salt.rb} +3 -3
- data/lib/encoded_id/encoders/my_sqids.rb +5 -16
- data/lib/encoded_id/encoders/sqids.rb +25 -11
- data/lib/encoded_id/encoders/sqids_configuration.rb +17 -0
- data/lib/encoded_id/encoders/sqids_with_blocklist_mode.rb +52 -0
- data/lib/encoded_id/hex_representation.rb +6 -9
- data/lib/encoded_id/reversible_id.rb +56 -138
- data/lib/encoded_id/version.rb +1 -2
- data/lib/encoded_id.rb +16 -19
- metadata +27 -10
- data/lib/encoded_id/encoders/base.rb +0 -71
|
@@ -20,17 +20,14 @@ class MySqids
|
|
|
20
20
|
# @rbs @min_length: Integer
|
|
21
21
|
# @rbs @blocklist: (Array[String] | Set[String])
|
|
22
22
|
|
|
23
|
-
# @rbs self.@DEFAULT_ALPHABET: String
|
|
24
23
|
DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
25
24
|
|
|
26
25
|
# Default minimum length of 0 means no padding is applied to generated IDs
|
|
27
|
-
# @rbs self.@DEFAULT_MIN_LENGTH: Integer
|
|
28
26
|
DEFAULT_MIN_LENGTH = 0
|
|
29
27
|
# rubocop:disable Metrics/CollectionLiteralLength, Layout/LineLength
|
|
30
28
|
# Default blocklist containing words that should not appear in generated IDs
|
|
31
29
|
# The blocklist prevents offensive or inappropriate words from appearing in IDs by
|
|
32
30
|
# regenerating IDs that contain these patterns.
|
|
33
|
-
# @rbs self.@DEFAULT_BLOCKLIST: Array[String]
|
|
34
31
|
DEFAULT_BLOCKLIST = %w[0rgasm 1d10t 1d1ot 1di0t 1diot 1eccacu10 1eccacu1o 1eccacul0
|
|
35
32
|
1eccaculo 1mbec11e 1mbec1le 1mbeci1e 1mbecile a11upat0 a11upato a1lupat0 a1lupato aand ah01e ah0le aho1e ahole al1upat0 al1upato allupat0 allupato ana1 ana1e anal anale anus arrapat0 arrapato arsch arse ass b00b b00be b01ata b0ceta b0iata b0ob b0obe b0sta b1tch b1te b1tte ba1atkar balatkar bastard0 bastardo batt0na battona bitch bite bitte bo0b bo0be bo1ata boceta boiata boob boobe bosta bran1age bran1er bran1ette bran1eur bran1euse branlage branler branlette branleur branleuse c0ck c0g110ne c0g11one c0g1i0ne c0g1ione c0gl10ne c0gl1one c0gli0ne c0glione c0na c0nnard c0nnasse c0nne c0u111es c0u11les c0u1l1es c0u1lles c0ui11es c0ui1les c0uil1es c0uilles c11t c11t0 c11to c1it c1it0 c1ito cabr0n cabra0 cabrao cabron caca cacca cacete cagante cagar cagare cagna cara1h0 cara1ho caracu10 caracu1o caracul0 caraculo caralh0 caralho cazz0 cazz1mma cazzata cazzimma cazzo ch00t1a ch00t1ya ch00tia ch00tiya ch0d ch0ot1a ch0ot1ya ch0otia ch0otiya ch1asse ch1avata ch1er ch1ng0 ch1ngadaz0s ch1ngadazos ch1ngader1ta ch1ngaderita ch1ngar ch1ngo ch1ngues ch1nk chatte chiasse chiavata chier ching0 chingadaz0s chingadazos chingader1ta chingaderita chingar chingo chingues chink cho0t1a cho0t1ya cho0tia cho0tiya chod choot1a choot1ya chootia chootiya cl1t cl1t0 cl1to clit clit0 clito cock cog110ne cog11one cog1i0ne cog1ione cogl10ne cogl1one cogli0ne coglione cona connard connasse conne cou111es cou11les cou1l1es cou1lles coui11es coui1les couil1es couilles cracker crap cu10 cu1att0ne cu1attone cu1er0 cu1ero cu1o cul0 culatt0ne culattone culer0 culero culo cum cunt d11d0 d11do d1ck d1ld0 d1ldo damn de1ch deich depp di1d0 di1do dick dild0 dildo dyke encu1e encule enema enf01re enf0ire enfo1re enfoire estup1d0 estup1do estupid0 estupido etr0n etron f0da f0der f0ttere f0tters1 f0ttersi f0tze f0utre f1ca f1cker f1ga fag fica ficker figa foda foder fottere fotters1 fottersi fotze foutre fr0c10 fr0c1o fr0ci0 fr0cio fr0sc10 fr0sc1o fr0sci0 fr0scio froc10 froc1o froci0 frocio frosc10 frosc1o frosci0 froscio fuck g00 g0o g0u1ne g0uine gandu go0 goo gou1ne gouine gr0gnasse grognasse haram1 harami haramzade hund1n hundin id10t id1ot idi0t idiot imbec11e imbec1le imbeci1e imbecile j1zz jerk jizz k1ke kam1ne kamine kike leccacu10 leccacu1o leccacul0 leccaculo m1erda m1gn0tta m1gnotta m1nch1a m1nchia m1st mam0n mamahuev0 mamahuevo mamon masturbat10n masturbat1on masturbate masturbati0n masturbation merd0s0 merd0so merda merde merdos0 merdoso mierda mign0tta mignotta minch1a minchia mist musch1 muschi n1gger neger negr0 negre negro nerch1a nerchia nigger orgasm p00p p011a p01la p0l1a p0lla p0mp1n0 p0mp1no p0mpin0 p0mpino p0op p0rca p0rn p0rra p0uff1asse p0uffiasse p1p1 p1pi p1r1a p1rla p1sc10 p1sc1o p1sci0 p1scio p1sser pa11e pa1le pal1e palle pane1e1r0 pane1e1ro pane1eir0 pane1eiro panele1r0 panele1ro paneleir0 paneleiro patakha pec0r1na pec0rina pecor1na pecorina pen1s pendej0 pendejo penis pip1 pipi pir1a pirla pisc10 pisc1o pisci0 piscio pisser po0p po11a po1la pol1a polla pomp1n0 pomp1no pompin0 pompino poop porca porn porra pouff1asse pouffiasse pr1ck prick pussy put1za puta puta1n putain pute putiza puttana queca r0mp1ba11e r0mp1ba1le r0mp1bal1e r0mp1balle r0mpiba11e r0mpiba1le r0mpibal1e r0mpiballe rand1 randi rape recch10ne recch1one recchi0ne recchione retard romp1ba11e romp1ba1le romp1bal1e romp1balle rompiba11e rompiba1le rompibal1e rompiballe ruff1an0 ruff1ano ruffian0 ruffiano s1ut sa10pe sa1aud sa1ope sacanagem sal0pe salaud salope saugnapf sb0rr0ne sb0rra sb0rrone sbattere sbatters1 sbattersi sborr0ne sborra sborrone sc0pare sc0pata sch1ampe sche1se sche1sse scheise scheisse schlampe schwachs1nn1g schwachs1nnig schwachsinn1g schwachsinnig schwanz scopare scopata sexy sh1t shit slut sp0mp1nare sp0mpinare spomp1nare spompinare str0nz0 str0nza str0nzo stronz0 stronza stronzo stup1d stupid succh1am1 succh1ami succhiam1 succhiami sucker t0pa tapette test1c1e test1cle testic1e testicle tette topa tr01a tr0ia tr0mbare tr1ng1er tr1ngler tring1er tringler tro1a troia trombare turd twat vaffancu10 vaffancu1o vaffancul0 vaffanculo vag1na vagina verdammt verga w1chsen wank wichsen x0ch0ta x0chota xana xoch0ta xochota z0cc01a z0cc0la z0cco1a z0ccola z1z1 z1zi ziz1 zizi zocc01a zocc0la zocco1a zoccola].freeze
|
|
36
33
|
# rubocop:enable Metrics/CollectionLiteralLength, Layout/LineLength
|
|
@@ -83,23 +80,15 @@ class MySqids
|
|
|
83
80
|
"Minimum length has to be between 0 and #{min_length_limit}"
|
|
84
81
|
end
|
|
85
82
|
|
|
86
|
-
# Filter the blocklist to only include words that:
|
|
87
|
-
# 1. Are at least 3 characters long
|
|
88
|
-
# 2. Only contain characters that exist in the alphabet (case-insensitive)
|
|
89
|
-
# This ensures we don't try to block words that could never appear in generated IDs
|
|
90
83
|
filtered_blocklist = if options[:blocklist].nil? && options[:alphabet].nil?
|
|
91
|
-
# If using default blocklist and alphabet, skip filtering since we know it's valid
|
|
92
84
|
blocklist
|
|
93
85
|
else
|
|
94
86
|
downcased_alphabet = alphabet.map(&:downcase)
|
|
95
|
-
# Only keep words that can be formed from the alphabet
|
|
96
87
|
blocklist.select do |word|
|
|
97
88
|
word.length >= 3 && (word.downcase.chars - downcased_alphabet).empty?
|
|
98
89
|
end.to_set(&:downcase)
|
|
99
90
|
end
|
|
100
91
|
|
|
101
|
-
# Store the alphabet as an array of integer codepoints after shuffling
|
|
102
|
-
# Shuffling ensures the alphabet order is unique to this instance
|
|
103
92
|
@alphabet = shuffle(alphabet.map(&:ord))
|
|
104
93
|
@min_length = min_length
|
|
105
94
|
@blocklist = filtered_blocklist
|
|
@@ -128,7 +117,10 @@ class MySqids
|
|
|
128
117
|
return "" if numbers.empty?
|
|
129
118
|
|
|
130
119
|
# Validate that all numbers are within the acceptable range
|
|
131
|
-
in_range_numbers = numbers.filter_map { |n|
|
|
120
|
+
in_range_numbers = numbers.filter_map { |n|
|
|
121
|
+
i = n.to_i
|
|
122
|
+
i if i.between?(0, MAX_INT)
|
|
123
|
+
}
|
|
132
124
|
unless in_range_numbers.length == numbers.length
|
|
133
125
|
raise ArgumentError,
|
|
134
126
|
"Encoding supports numbers between 0 and #{MAX_INT}"
|
|
@@ -161,8 +153,7 @@ class MySqids
|
|
|
161
153
|
|
|
162
154
|
return ret if id.empty?
|
|
163
155
|
|
|
164
|
-
|
|
165
|
-
id = id.chars.map(&:ord)
|
|
156
|
+
id = id.codepoints
|
|
166
157
|
|
|
167
158
|
# Validate that all characters in the ID exist in our alphabet
|
|
168
159
|
# If any character is invalid, return empty array
|
|
@@ -291,7 +282,6 @@ class MySqids
|
|
|
291
282
|
offset = (offset + increment) % alphabet_length
|
|
292
283
|
|
|
293
284
|
prefix = @alphabet[offset]
|
|
294
|
-
# Now working with modified alphabet
|
|
295
285
|
alphabet = rotate_and_reverse_alphabet(@alphabet, offset)
|
|
296
286
|
id = [prefix]
|
|
297
287
|
|
|
@@ -352,7 +342,6 @@ class MySqids
|
|
|
352
342
|
while true # rubocop:disable Style/InfiniteLoop
|
|
353
343
|
new_char_index = (result % alphabet_length) + 1
|
|
354
344
|
new_char = alphabet[new_char_index]
|
|
355
|
-
# id is an array, we want to insert the new char at the start_index position.
|
|
356
345
|
id.insert(start_index, new_char)
|
|
357
346
|
result /= alphabet_length
|
|
358
347
|
break if result <= 0
|
|
@@ -4,27 +4,41 @@
|
|
|
4
4
|
|
|
5
5
|
module EncodedId
|
|
6
6
|
module Encoders
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# Encoder implementation using the Sqids algorithm for encoding/decoding IDs.
|
|
8
|
+
class Sqids
|
|
9
|
+
# @rbs @sqids: SqidsWithBlocklistMode
|
|
10
|
+
# @rbs @blocklist_mode: Symbol
|
|
11
|
+
# @rbs @blocklist_max_length: Integer
|
|
9
12
|
|
|
10
|
-
# @rbs (
|
|
11
|
-
def initialize(
|
|
12
|
-
|
|
13
|
-
@
|
|
13
|
+
# @rbs (?Integer min_hash_length, ?Alphabet alphabet, ?Blocklist blocklist, ?Symbol blocklist_mode, ?Integer blocklist_max_length) -> void
|
|
14
|
+
def initialize(min_hash_length = 0, alphabet = Alphabet.alphanum, blocklist = Blocklist.empty, blocklist_mode = :length_threshold, blocklist_max_length = 32)
|
|
15
|
+
@min_hash_length = min_hash_length
|
|
16
|
+
@alphabet = alphabet
|
|
17
|
+
@blocklist = blocklist
|
|
18
|
+
@blocklist_mode = blocklist_mode
|
|
19
|
+
@blocklist_max_length = blocklist_max_length
|
|
20
|
+
|
|
21
|
+
@sqids = ::SqidsWithBlocklistMode.new(
|
|
14
22
|
{
|
|
15
23
|
min_length: min_hash_length,
|
|
16
24
|
alphabet: alphabet.characters,
|
|
17
|
-
blocklist: blocklist
|
|
25
|
+
blocklist: blocklist,
|
|
26
|
+
blocklist_mode: blocklist_mode,
|
|
27
|
+
blocklist_max_length: blocklist_max_length
|
|
18
28
|
}
|
|
19
29
|
)
|
|
20
|
-
rescue TypeError, ArgumentError =>
|
|
21
|
-
raise InvalidInputError, "unable to create sqids instance: #{
|
|
30
|
+
rescue TypeError, ArgumentError => error
|
|
31
|
+
raise InvalidInputError, "unable to create sqids instance: #{error.message}"
|
|
22
32
|
end
|
|
23
33
|
|
|
34
|
+
attr_reader :min_hash_length #: Integer
|
|
35
|
+
attr_reader :alphabet #: Alphabet
|
|
36
|
+
attr_reader :blocklist #: Blocklist
|
|
37
|
+
|
|
24
38
|
# @rbs (Array[Integer] numbers) -> String
|
|
25
39
|
def encode(numbers)
|
|
26
|
-
numbers.all? {
|
|
27
|
-
return "" if numbers.empty? || numbers.any?
|
|
40
|
+
numbers.all? { Integer(_1) } # raises if conversion fails
|
|
41
|
+
return "" if numbers.empty? || numbers.any?(&:negative?)
|
|
28
42
|
|
|
29
43
|
@sqids.encode(numbers)
|
|
30
44
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
module EncodedId
|
|
6
|
+
module Encoders
|
|
7
|
+
# Configuration for Sqids encoder
|
|
8
|
+
# Sqids does not use a salt - it shuffles the alphabet deterministically
|
|
9
|
+
class SqidsConfiguration < BaseConfiguration
|
|
10
|
+
# Create the Sqids encoder instance
|
|
11
|
+
# @rbs () -> Sqids
|
|
12
|
+
def create_encoder
|
|
13
|
+
Sqids.new(min_length, alphabet, blocklist, blocklist_mode, blocklist_max_length)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
# Extension of MySqids (vendored Sqids) that adds blocklist mode support.
|
|
6
|
+
# This subclass overrides blocklist checking to support different modes
|
|
7
|
+
# without modifying the vendored library.
|
|
8
|
+
# In the future, the base class can be changed from MySqids to ::Sqids::Sqids
|
|
9
|
+
# once we use the official gem.
|
|
10
|
+
class SqidsWithBlocklistMode < MySqids
|
|
11
|
+
# @rbs @blocklist_mode: Symbol
|
|
12
|
+
# @rbs @blocklist_max_length: Integer
|
|
13
|
+
|
|
14
|
+
# @rbs (?Hash[Symbol, untyped] options) -> void
|
|
15
|
+
def initialize(options = {})
|
|
16
|
+
@blocklist_mode = options[:blocklist_mode] || :length_threshold
|
|
17
|
+
@blocklist_max_length = options[:blocklist_max_length] || 32
|
|
18
|
+
|
|
19
|
+
# Remove our custom options before passing to parent
|
|
20
|
+
parent_options = options.dup
|
|
21
|
+
parent_options.delete(:blocklist_mode)
|
|
22
|
+
parent_options.delete(:blocklist_max_length)
|
|
23
|
+
|
|
24
|
+
super(parent_options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Override blocked_id? to implement blocklist mode logic
|
|
30
|
+
# @rbs (String id) -> bool
|
|
31
|
+
def blocked_id?(id)
|
|
32
|
+
return false unless check_blocklist?(id)
|
|
33
|
+
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Determines if blocklist checking should be performed based on mode and ID length
|
|
38
|
+
# @rbs (String id) -> bool
|
|
39
|
+
def check_blocklist?(id)
|
|
40
|
+
return false if @blocklist.empty?
|
|
41
|
+
|
|
42
|
+
case @blocklist_mode
|
|
43
|
+
when :always
|
|
44
|
+
true
|
|
45
|
+
when :length_threshold
|
|
46
|
+
id.length <= @blocklist_max_length
|
|
47
|
+
else
|
|
48
|
+
# If :raise_if_likely mode it raises at configuration time, so if we get here, we check
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -37,7 +37,6 @@ module EncodedId
|
|
|
37
37
|
hex_digit_encoding_group_size
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
# Convert hex strings to integer representations
|
|
41
40
|
# @rbs (encodeableHexValue hexs) -> Array[Integer]
|
|
42
41
|
def integer_representation(hexs)
|
|
43
42
|
inputs = Array(hexs).map(&:to_s)
|
|
@@ -48,12 +47,10 @@ module EncodedId
|
|
|
48
47
|
digits_to_encode << hex_string_separator
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
# Remove the last marker
|
|
52
50
|
digits_to_encode.pop unless digits_to_encode.empty?
|
|
53
51
|
digits_to_encode
|
|
54
52
|
end
|
|
55
53
|
|
|
56
|
-
# Convert integer representations to hex strings
|
|
57
54
|
# @rbs (Array[Integer] integers) -> Array[String]
|
|
58
55
|
def integers_to_hex_strings(integers)
|
|
59
56
|
hex_strings = [] #: Array[String]
|
|
@@ -66,12 +63,12 @@ module EncodedId
|
|
|
66
63
|
hex_string = []
|
|
67
64
|
add_leading = false
|
|
68
65
|
else
|
|
66
|
+
# Add leading zeros to maintain group size for all groups except the first
|
|
69
67
|
hex_string << (add_leading ? "%.#{@hex_digit_encoding_group_size}x" % integer : integer.to_s(16))
|
|
70
68
|
add_leading = true
|
|
71
69
|
end
|
|
72
70
|
end
|
|
73
71
|
|
|
74
|
-
# Add the last hex string
|
|
75
72
|
hex_strings << hex_string.join unless hex_string.empty?
|
|
76
73
|
hex_strings.reverse
|
|
77
74
|
end
|
|
@@ -96,12 +93,12 @@ module EncodedId
|
|
|
96
93
|
# @rbs (String hex_string_cleaned) -> Array[Integer]
|
|
97
94
|
def convert_to_integer_groups(hex_string_cleaned)
|
|
98
95
|
groups = [] #: Array[Array[String]]
|
|
99
|
-
hex_string_cleaned.chars.reverse.each_with_index do |char,
|
|
100
|
-
group_id =
|
|
101
|
-
groups[group_id] ||= []
|
|
102
|
-
|
|
96
|
+
hex_string_cleaned.chars.reverse.each_with_index do |char, index|
|
|
97
|
+
group_id = index / @hex_digit_encoding_group_size
|
|
98
|
+
group = (groups[group_id] ||= [])
|
|
99
|
+
group.unshift(char)
|
|
103
100
|
end
|
|
104
|
-
groups.map {
|
|
101
|
+
groups.map { _1.join.to_i(16) }
|
|
105
102
|
end
|
|
106
103
|
end
|
|
107
104
|
end
|
|
@@ -10,53 +10,46 @@ module EncodedId
|
|
|
10
10
|
# type encodeableValue = Array[String | Integer] | String | Integer
|
|
11
11
|
|
|
12
12
|
class ReversibleId
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# @rbs @alphabet: Alphabet
|
|
19
|
-
# @rbs @salt: String
|
|
20
|
-
# @rbs @length: Integer
|
|
21
|
-
# @rbs @split_at: Integer?
|
|
22
|
-
# @rbs @split_with: String?
|
|
23
|
-
# @rbs @hex_represention_encoder: HexRepresentation
|
|
24
|
-
# @rbs @max_length: Integer?
|
|
25
|
-
# @rbs @max_inputs_per_id: Integer
|
|
26
|
-
# @rbs @blocklist: Blocklist
|
|
27
|
-
# @rbs @encoder: Encoders::Base
|
|
28
|
-
|
|
29
|
-
# @rbs (salt: String, ?length: Integer, ?split_at: Integer?, ?split_with: String?, ?alphabet: Alphabet, ?hex_digit_encoding_group_size: Integer, ?max_length: Integer?, ?max_inputs_per_id: Integer, ?encoder: Symbol | Encoders::Base, ?blocklist: Blocklist | Array[String] | Set[String] | nil) -> void
|
|
30
|
-
def initialize(salt:, length: 8, split_at: 4, split_with: "-", alphabet: Alphabet.modified_crockford, hex_digit_encoding_group_size: 4, max_length: 128, max_inputs_per_id: 32, encoder: DEFAULT_ENCODER, blocklist: Blocklist.empty)
|
|
31
|
-
@alphabet = validate_alphabet(alphabet)
|
|
32
|
-
@salt = validate_salt(salt)
|
|
33
|
-
@length = validate_length(length)
|
|
34
|
-
@split_at = validate_split_at(split_at)
|
|
35
|
-
@split_with = validate_split_with(split_with, alphabet)
|
|
36
|
-
@hex_represention_encoder = HexRepresentation.new(hex_digit_encoding_group_size)
|
|
37
|
-
@max_length = validate_max_length(max_length)
|
|
38
|
-
@max_inputs_per_id = validate_max_input(max_inputs_per_id)
|
|
39
|
-
@blocklist = validate_blocklist(blocklist)
|
|
40
|
-
@encoder = create_encoder(validate_encoder(encoder))
|
|
13
|
+
# Factory method to create a Hashid-based reversible ID
|
|
14
|
+
# @rbs (salt: String, **untyped options) -> ReversibleId
|
|
15
|
+
def self.hashid(salt:, **options)
|
|
16
|
+
new(Encoders::HashidConfiguration.new(salt: salt, **options))
|
|
41
17
|
end
|
|
42
18
|
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
19
|
+
# Factory method to create a Sqids-based reversible ID (default)
|
|
20
|
+
# @rbs (**untyped options) -> ReversibleId
|
|
21
|
+
def self.sqids(**options)
|
|
22
|
+
new(Encoders::SqidsConfiguration.new(**options))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Initialize with a configuration object
|
|
26
|
+
# Defaults to Sqids configuration if called with no arguments
|
|
27
|
+
# @rbs (?Encoders::BaseConfiguration? config) -> void
|
|
28
|
+
def initialize(config = nil)
|
|
29
|
+
@config = config || Encoders::SqidsConfiguration.new
|
|
30
|
+
|
|
31
|
+
unless @config.is_a?(Encoders::BaseConfiguration)
|
|
32
|
+
raise InvalidConfigurationError, "config must be an instance of Encoders::BaseConfiguration (or nil for default Sqids)"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@hex_represention_encoder = HexRepresentation.new(@config.hex_digit_encoding_group_size)
|
|
36
|
+
@encoder = create_encoder
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The configuration object for this encoder instance.
|
|
40
|
+
# Returns either an Encoders::HashidConfiguration or Encoders::SqidsConfiguration.
|
|
41
|
+
# Useful for introspecting settings like alphabet, min_length, blocklist, etc.
|
|
42
|
+
attr_reader :config #: Encoders::BaseConfiguration
|
|
43
|
+
|
|
49
44
|
attr_reader :hex_represention_encoder #: HexRepresentation
|
|
50
|
-
attr_reader :
|
|
51
|
-
attr_reader :blocklist #: Blocklist
|
|
52
|
-
attr_reader :encoder #: Encoders::Base
|
|
45
|
+
attr_reader :encoder #: Encoders::Hashid | Encoders::Sqids
|
|
53
46
|
|
|
54
47
|
# Encode the input values into a hash
|
|
55
48
|
# @rbs (encodeableValue values) -> String
|
|
56
49
|
def encode(values)
|
|
57
50
|
inputs = prepare_input(values)
|
|
58
51
|
encoded_id = encoder.encode(inputs)
|
|
59
|
-
encoded_id = humanize_length(encoded_id) if split_with && split_at
|
|
52
|
+
encoded_id = humanize_length(encoded_id) if @config.split_with && @config.split_at
|
|
60
53
|
|
|
61
54
|
raise EncodedIdLengthError if max_length_exceeded?(encoded_id)
|
|
62
55
|
|
|
@@ -66,155 +59,79 @@ module EncodedId
|
|
|
66
59
|
# Encode hex strings into a hash
|
|
67
60
|
# @rbs (encodeableHexValue hexs) -> String
|
|
68
61
|
def encode_hex(hexs)
|
|
69
|
-
encode(hex_represention_encoder.hex_as_integers(hexs))
|
|
62
|
+
encode(@hex_represention_encoder.hex_as_integers(hexs))
|
|
70
63
|
end
|
|
71
64
|
|
|
72
65
|
# Decode the hash to original array
|
|
73
66
|
# @rbs (String str, ?downcase: bool) -> Array[Integer]
|
|
74
|
-
def decode(str, downcase:
|
|
67
|
+
def decode(str, downcase: false)
|
|
75
68
|
raise EncodedIdFormatError, "Max length of input exceeded" if max_length_exceeded?(str)
|
|
76
69
|
|
|
77
70
|
encoder.decode(convert_to_hash(str, downcase))
|
|
78
|
-
rescue InvalidInputError =>
|
|
79
|
-
raise EncodedIdFormatError,
|
|
71
|
+
rescue InvalidInputError => error
|
|
72
|
+
raise EncodedIdFormatError, error.message
|
|
80
73
|
end
|
|
81
74
|
|
|
82
75
|
# Decode hex strings from a hash
|
|
83
76
|
# @rbs (String str, ?downcase: bool) -> Array[String]
|
|
84
|
-
def decode_hex(str, downcase:
|
|
77
|
+
def decode_hex(str, downcase: false)
|
|
85
78
|
integers = encoder.decode(convert_to_hash(str, downcase))
|
|
86
|
-
hex_represention_encoder.integers_as_hex(integers)
|
|
79
|
+
@hex_represention_encoder.integers_as_hex(integers)
|
|
87
80
|
end
|
|
88
81
|
|
|
89
82
|
private
|
|
90
83
|
|
|
91
|
-
# @rbs (Alphabet alphabet) -> Alphabet
|
|
92
|
-
def validate_alphabet(alphabet)
|
|
93
|
-
return alphabet if alphabet.is_a?(Alphabet)
|
|
94
|
-
raise InvalidAlphabetError, "alphabet must be an instance of Alphabet"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# @rbs (String salt) -> String
|
|
98
|
-
def validate_salt(salt)
|
|
99
|
-
return salt if salt.is_a?(String) && salt.size > 3
|
|
100
|
-
raise InvalidConfigurationError, "Salt must be a string and longer than 3 characters"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Target length of the encoded string (the minimum but not maximum length)
|
|
104
|
-
# @rbs (Integer length) -> Integer
|
|
105
|
-
def validate_length(length)
|
|
106
|
-
return length if valid_integer_option?(length)
|
|
107
|
-
raise InvalidConfigurationError, "Length must be an integer greater than 0"
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# @rbs (Integer? max_length) -> Integer?
|
|
111
|
-
def validate_max_length(max_length)
|
|
112
|
-
return max_length if valid_integer_option?(max_length) || max_length.nil?
|
|
113
|
-
raise InvalidConfigurationError, "Max length must be an integer greater than 0"
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# @rbs (Integer max_inputs_per_id) -> Integer
|
|
117
|
-
def validate_max_input(max_inputs_per_id)
|
|
118
|
-
return max_inputs_per_id if valid_integer_option?(max_inputs_per_id)
|
|
119
|
-
raise InvalidConfigurationError, "Max inputs per ID must be an integer greater than 0"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Split the encoded string into groups of this size
|
|
123
|
-
# @rbs (Integer? split_at) -> Integer?
|
|
124
|
-
def validate_split_at(split_at)
|
|
125
|
-
return split_at if valid_integer_option?(split_at) || split_at.nil?
|
|
126
|
-
raise InvalidConfigurationError, "Split at must be an integer greater than 0 or nil"
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# @rbs (String? split_with, Alphabet alphabet) -> String?
|
|
130
|
-
def validate_split_with(split_with, alphabet)
|
|
131
|
-
return split_with if split_with.nil? || (split_with.is_a?(String) && !alphabet.characters.include?(split_with))
|
|
132
|
-
raise InvalidConfigurationError, "Split with must be a string and not part of the alphabet or nil"
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# @rbs (Integer? value) -> bool
|
|
136
|
-
def valid_integer_option?(value)
|
|
137
|
-
value.is_a?(Integer) && value > 0
|
|
138
|
-
end
|
|
139
|
-
|
|
140
84
|
# @rbs (encodeableValue value) -> Array[Integer]
|
|
141
85
|
def prepare_input(value)
|
|
142
86
|
inputs = value.is_a?(Array) ? value.map(&:to_i) : [value.to_i]
|
|
143
87
|
raise ::EncodedId::InvalidInputError, "Cannot encode an empty array" if inputs.empty?
|
|
144
88
|
raise ::EncodedId::InvalidInputError, "Integer IDs to be encoded can only be positive" if inputs.any?(&:negative?)
|
|
145
|
-
|
|
146
|
-
raise ::EncodedId::InvalidInputError, "%d integer IDs provided, maximum amount of IDs is %d" % [inputs.length, @max_inputs_per_id] if inputs.length > @max_inputs_per_id
|
|
89
|
+
raise ::EncodedId::InvalidInputError, "%d integer IDs provided, maximum amount of IDs is %d" % [inputs.length, @config.max_inputs_per_id] if inputs.length > @config.max_inputs_per_id
|
|
147
90
|
|
|
148
91
|
inputs
|
|
149
92
|
end
|
|
150
93
|
|
|
151
|
-
# @rbs (
|
|
152
|
-
def create_encoder
|
|
153
|
-
|
|
154
|
-
return @encoder if defined?(@encoder) && @encoder.is_a?(Encoders::Base)
|
|
155
|
-
return encoder if encoder.is_a?(Encoders::Base)
|
|
156
|
-
|
|
157
|
-
case encoder
|
|
158
|
-
when :sqids
|
|
159
|
-
if defined?(Encoders::Sqids)
|
|
160
|
-
Encoders::Sqids.new(salt, length, alphabet, @blocklist)
|
|
161
|
-
else
|
|
162
|
-
raise InvalidConfigurationError, "Sqids encoder requested but the sqids gem is not available. Please add 'gem \"sqids\"' to your Gemfile."
|
|
163
|
-
end
|
|
164
|
-
when :hashids
|
|
165
|
-
Encoders::HashId.new(salt, length, alphabet, @blocklist)
|
|
166
|
-
else
|
|
167
|
-
raise InvalidConfigurationError, "The encoder name is not supported '#{encoder}'"
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# @rbs (Symbol | Encoders::Base encoder) -> (Symbol | Encoders::Base)
|
|
172
|
-
def validate_encoder(encoder)
|
|
173
|
-
# Accept either a valid symbol or an Encoders::Base instance
|
|
174
|
-
return encoder if VALID_ENCODERS.include?(encoder) || encoder.is_a?(Encoders::Base)
|
|
175
|
-
raise InvalidConfigurationError, "Encoder must be one of: #{VALID_ENCODERS.join(", ")} or an instance of EncodedId::Encoders::Base"
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# @rbs (Blocklist | Array[String] | Set[String] | nil blocklist) -> Blocklist
|
|
179
|
-
def validate_blocklist(blocklist)
|
|
180
|
-
return blocklist if blocklist.is_a?(Blocklist)
|
|
181
|
-
return Blocklist.empty if blocklist.nil?
|
|
182
|
-
|
|
183
|
-
return Blocklist.new(blocklist) if blocklist.is_a?(Array) || blocklist.is_a?(Set)
|
|
184
|
-
|
|
185
|
-
raise InvalidConfigurationError, "Blocklist must be an instance of Blocklist, a Set, or an Array of strings"
|
|
94
|
+
# @rbs () -> (Encoders::Hashid | Encoders::Sqids)
|
|
95
|
+
def create_encoder
|
|
96
|
+
@config.create_encoder
|
|
186
97
|
end
|
|
187
98
|
|
|
99
|
+
# Splits long encoded strings into groups for readability
|
|
100
|
+
# e.g., "ABCDEFGH" with split_at=4, split_with="-" becomes "ABCD-EFGH"
|
|
188
101
|
# @rbs (String hash) -> String
|
|
189
102
|
def humanize_length(hash)
|
|
190
103
|
len = hash.length
|
|
191
|
-
at = split_at #: Integer
|
|
192
|
-
with = split_with #: String
|
|
104
|
+
at = @config.split_at #: Integer
|
|
105
|
+
with = @config.split_with #: String
|
|
193
106
|
return hash if len <= at
|
|
194
107
|
|
|
195
108
|
separator_count = (len - 1) / at
|
|
196
109
|
result = hash.dup
|
|
197
110
|
insert_offset = 0
|
|
198
|
-
(1..separator_count).each do |
|
|
199
|
-
insert_pos =
|
|
111
|
+
(1..separator_count).each do |separator_index|
|
|
112
|
+
insert_pos = separator_index * at + insert_offset
|
|
200
113
|
result.insert(insert_pos, with)
|
|
201
114
|
insert_offset += with.length
|
|
202
115
|
end
|
|
203
116
|
result
|
|
204
117
|
end
|
|
205
118
|
|
|
119
|
+
# Reverses humanize_length transformation: removes separators and optionally downcases
|
|
206
120
|
# @rbs (String str, bool downcase) -> String
|
|
207
121
|
def convert_to_hash(str, downcase)
|
|
122
|
+
split_with = @config.split_with
|
|
208
123
|
str = str.gsub(split_with, "") if split_with
|
|
209
124
|
str = str.downcase if downcase
|
|
210
125
|
map_equivalent_characters(str)
|
|
211
126
|
end
|
|
212
127
|
|
|
128
|
+
# Maps equivalent characters based on alphabet configuration (e.g., 'O' -> '0', 'I' -> '1')
|
|
213
129
|
# @rbs (String str) -> String
|
|
214
130
|
def map_equivalent_characters(str)
|
|
215
|
-
|
|
131
|
+
equivalences = @config.alphabet.equivalences
|
|
132
|
+
return str unless equivalences
|
|
216
133
|
|
|
217
|
-
|
|
134
|
+
equivalences.reduce(str) do |cleaned, ceq|
|
|
218
135
|
from, to = ceq
|
|
219
136
|
cleaned.tr(from, to)
|
|
220
137
|
end
|
|
@@ -222,9 +139,10 @@ module EncodedId
|
|
|
222
139
|
|
|
223
140
|
# @rbs (String str) -> bool
|
|
224
141
|
def max_length_exceeded?(str)
|
|
225
|
-
|
|
142
|
+
max_len = @config.max_length
|
|
143
|
+
return false unless max_len
|
|
226
144
|
|
|
227
|
-
str.length >
|
|
145
|
+
str.length > max_len
|
|
228
146
|
end
|
|
229
147
|
end
|
|
230
148
|
end
|
data/lib/encoded_id/version.rb
CHANGED
data/lib/encoded_id.rb
CHANGED
|
@@ -8,29 +8,26 @@ require_relative "encoded_id/hex_representation"
|
|
|
8
8
|
require_relative "encoded_id/blocklist"
|
|
9
9
|
|
|
10
10
|
# Load the encoder framework
|
|
11
|
-
require_relative "encoded_id/encoders/
|
|
12
|
-
require_relative "encoded_id/encoders/
|
|
13
|
-
require_relative "encoded_id/encoders/
|
|
14
|
-
require_relative "encoded_id/encoders/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
require_relative "encoded_id/encoders/hashid_salt"
|
|
12
|
+
require_relative "encoded_id/encoders/hashid_consistent_shuffle"
|
|
13
|
+
require_relative "encoded_id/encoders/hashid_ordinal_alphabet_separator_guards"
|
|
14
|
+
require_relative "encoded_id/encoders/hashid"
|
|
15
|
+
|
|
16
|
+
require "sqids"
|
|
17
|
+
# TODO: move back to only using gem once upstreamed our changes
|
|
18
|
+
require_relative "encoded_id/encoders/my_sqids"
|
|
19
|
+
require_relative "encoded_id/encoders/sqids_with_blocklist_mode"
|
|
20
|
+
|
|
21
|
+
require_relative "encoded_id/encoders/sqids"
|
|
22
|
+
|
|
23
|
+
# Load configuration classes
|
|
24
|
+
require_relative "encoded_id/encoders/base_configuration"
|
|
25
|
+
require_relative "encoded_id/encoders/hashid_configuration"
|
|
26
|
+
require_relative "encoded_id/encoders/sqids_configuration"
|
|
25
27
|
|
|
26
28
|
require_relative "encoded_id/reversible_id"
|
|
27
29
|
|
|
28
30
|
# @rbs!
|
|
29
|
-
# class Integer
|
|
30
|
-
# MAX: Integer
|
|
31
|
-
# end
|
|
32
|
-
#
|
|
33
|
-
# # Optional Sqids gem support
|
|
34
31
|
# module Sqids
|
|
35
32
|
# DEFAULT_BLOCKLIST: Array[String]
|
|
36
33
|
# end
|
metadata
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: encoded_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen Ierodiaconou
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-11-
|
|
11
|
-
dependencies:
|
|
10
|
+
date: 2025-11-21 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: sqids
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.2'
|
|
12
26
|
description: Encode your numerical IDs (eg record primary keys) into obfuscated strings
|
|
13
27
|
that can be used in URLs. The obfuscated strings are reversible, so you can decode
|
|
14
28
|
them back into the original numerical IDs. Supports encoding multiple IDs at once,
|
|
15
29
|
and generating IDs with custom alphabets and separators to make the IDs easier to
|
|
16
|
-
read or share.
|
|
30
|
+
read or share. Uses Sqids by default, with HashIds as an alternative.
|
|
17
31
|
email:
|
|
18
32
|
- stevegeek@gmail.com
|
|
19
33
|
executables: []
|
|
@@ -27,13 +41,16 @@ files:
|
|
|
27
41
|
- lib/encoded_id.rb
|
|
28
42
|
- lib/encoded_id/alphabet.rb
|
|
29
43
|
- lib/encoded_id/blocklist.rb
|
|
30
|
-
- lib/encoded_id/encoders/
|
|
31
|
-
- lib/encoded_id/encoders/
|
|
32
|
-
- lib/encoded_id/encoders/
|
|
33
|
-
- lib/encoded_id/encoders/
|
|
34
|
-
- lib/encoded_id/encoders/
|
|
44
|
+
- lib/encoded_id/encoders/base_configuration.rb
|
|
45
|
+
- lib/encoded_id/encoders/hashid.rb
|
|
46
|
+
- lib/encoded_id/encoders/hashid_configuration.rb
|
|
47
|
+
- lib/encoded_id/encoders/hashid_consistent_shuffle.rb
|
|
48
|
+
- lib/encoded_id/encoders/hashid_ordinal_alphabet_separator_guards.rb
|
|
49
|
+
- lib/encoded_id/encoders/hashid_salt.rb
|
|
35
50
|
- lib/encoded_id/encoders/my_sqids.rb
|
|
36
51
|
- lib/encoded_id/encoders/sqids.rb
|
|
52
|
+
- lib/encoded_id/encoders/sqids_configuration.rb
|
|
53
|
+
- lib/encoded_id/encoders/sqids_with_blocklist_mode.rb
|
|
37
54
|
- lib/encoded_id/hex_representation.rb
|
|
38
55
|
- lib/encoded_id/reversible_id.rb
|
|
39
56
|
- lib/encoded_id/version.rb
|
|
@@ -61,5 +78,5 @@ requirements: []
|
|
|
61
78
|
rubygems_version: 3.6.2
|
|
62
79
|
specification_version: 4
|
|
63
80
|
summary: EncodedId is a gem for creating reversible obfuscated IDs from numerical
|
|
64
|
-
IDs. It uses
|
|
81
|
+
IDs. It uses Sqids by default.
|
|
65
82
|
test_files: []
|