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.
@@ -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| i = n.to_i; i if i.between?(0, MAX_INT) }
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
- # Convert string to array of character codepoints for processing
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
- class Sqids < Base
8
- # @rbs @sqids: untyped
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 (String salt, ?Integer min_hash_length, ?Alphabet alphabet, ?Blocklist blocklist) -> void
11
- def initialize(salt, min_hash_length = 0, alphabet = Alphabet.alphanum, blocklist = Blocklist.empty)
12
- super
13
- @sqids = ::MySqids.new(
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 => e
21
- raise InvalidInputError, "unable to create sqids instance: #{e.message}"
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? { |n| Integer(n) } # raises if conversion fails
27
- return "" if numbers.empty? || numbers.any? { |n| n < 0 }
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, i|
100
- group_id = i / @hex_digit_encoding_group_size
101
- groups[group_id] ||= []
102
- groups[group_id].unshift(char)
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 { |c| c.join.to_i(16) }
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
- # @rbs VALID_ENCODERS: Array[Symbol]
14
- VALID_ENCODERS = [:hashids, :sqids].freeze
15
- # @rbs DEFAULT_ENCODER: Symbol
16
- DEFAULT_ENCODER = :hashids
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
- # Accessors for introspection
44
- attr_reader :salt #: String
45
- attr_reader :length #: Integer
46
- attr_reader :alphabet #: Alphabet
47
- attr_reader :split_at #: Integer?
48
- attr_reader :split_with #: String?
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 :max_length #: Integer?
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: true)
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 => e
79
- raise EncodedIdFormatError, e.message
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: true)
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 (Symbol | Encoders::Base encoder) -> Encoders::Base
152
- def create_encoder(encoder)
153
- # If an encoder instance was provided, return it directly
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 |i|
199
- insert_pos = i * at + insert_offset
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
- return str unless alphabet.equivalences
131
+ equivalences = @config.alphabet.equivalences
132
+ return str unless equivalences
216
133
 
217
- alphabet.equivalences.reduce(str) do |cleaned, ceq|
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
- return false if max_length.nil?
142
+ max_len = @config.max_length
143
+ return false unless max_len
226
144
 
227
- str.length > max_length
145
+ str.length > max_len
228
146
  end
229
147
  end
230
148
  end
@@ -3,6 +3,5 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  module EncodedId
6
- # @rbs VERSION: String
7
- VERSION = "1.0.0.rc6"
6
+ VERSION = "1.0.0"
8
7
  end
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/base"
12
- require_relative "encoded_id/encoders/hash_id_salt"
13
- require_relative "encoded_id/encoders/hash_id_consistent_shuffle"
14
- require_relative "encoded_id/encoders/hash_id_ordinal_alphabet_separator_guards"
15
- require_relative "encoded_id/encoders/hash_id"
16
-
17
- # Only load Sqids encoder if the gem is available
18
- begin
19
- require "sqids"
20
- require_relative "encoded_id/encoders/my_sqids"
21
- require_relative "encoded_id/encoders/sqids"
22
- rescue LoadError
23
- # Sqids gem not available, encoder will not be loaded
24
- end
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.rc6
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-17 00:00:00.000000000 Z
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. Dependency free.
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/base.rb
31
- - lib/encoded_id/encoders/hash_id.rb
32
- - lib/encoded_id/encoders/hash_id_consistent_shuffle.rb
33
- - lib/encoded_id/encoders/hash_id_ordinal_alphabet_separator_guards.rb
34
- - lib/encoded_id/encoders/hash_id_salt.rb
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 an implementation of Hash IDs under the hood.
81
+ IDs. It uses Sqids by default.
65
82
  test_files: []