encoded_id-rails 1.0.0.rc1 → 1.0.0.rc7

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -18
  3. data/LICENSE.txt +1 -1
  4. data/README.md +81 -473
  5. data/context/encoded_id-rails.md +651 -0
  6. data/context/encoded_id.md +437 -0
  7. data/lib/encoded_id/rails/active_record_finders.rb +54 -0
  8. data/lib/encoded_id/rails/annotated_id.rb +14 -9
  9. data/lib/encoded_id/rails/annotated_id_parser.rb +9 -1
  10. data/lib/encoded_id/rails/coder.rb +55 -10
  11. data/lib/encoded_id/rails/composite_id_base.rb +39 -0
  12. data/lib/encoded_id/rails/configuration.rb +66 -10
  13. data/lib/encoded_id/rails/encoder_methods.rb +30 -7
  14. data/lib/encoded_id/rails/finder_methods.rb +11 -0
  15. data/lib/encoded_id/rails/model.rb +60 -7
  16. data/lib/encoded_id/rails/path_param.rb +8 -0
  17. data/lib/encoded_id/rails/persists.rb +55 -9
  18. data/lib/encoded_id/rails/query_methods.rb +21 -4
  19. data/lib/encoded_id/rails/railtie.rb +13 -0
  20. data/lib/encoded_id/rails/salt.rb +8 -0
  21. data/lib/encoded_id/rails/slugged_id.rb +14 -9
  22. data/lib/encoded_id/rails/slugged_id_parser.rb +9 -1
  23. data/lib/encoded_id/rails/slugged_path_param.rb +8 -0
  24. data/lib/encoded_id/rails.rb +11 -6
  25. data/lib/generators/encoded_id/rails/install_generator.rb +36 -2
  26. data/lib/generators/encoded_id/rails/templates/{encoded_id.rb → hashids_encoded_id.rb} +49 -5
  27. data/lib/generators/encoded_id/rails/templates/sqids_encoded_id.rb +116 -0
  28. metadata +16 -24
  29. data/.devcontainer/Dockerfile +0 -17
  30. data/.devcontainer/compose.yml +0 -10
  31. data/.devcontainer/devcontainer.json +0 -12
  32. data/.standard.yml +0 -3
  33. data/Appraisals +0 -9
  34. data/Gemfile +0 -24
  35. data/Rakefile +0 -20
  36. data/Steepfile +0 -4
  37. data/gemfiles/.bundle/config +0 -2
  38. data/gemfiles/rails_7.2.gemfile +0 -19
  39. data/gemfiles/rails_8.0.gemfile +0 -19
  40. data/lib/encoded_id/rails/version.rb +0 -7
  41. data/rbs_collection.yaml +0 -24
  42. data/sig/encoded_id/rails.rbs +0 -141
@@ -1,14 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
5
7
  # Configuration class for initializer
6
8
  class Configuration
7
- attr_accessor :salt, :character_group_size, :alphabet, :id_length
8
- attr_accessor :slug_value_method_name, :annotation_method_name
9
- attr_accessor :model_to_param_returns_encoded_id
10
- attr_reader :group_separator, :slugged_id_separator, :annotated_id_separator
9
+ VALID_ENCODERS = [:hashids, :sqids].freeze
10
+ DEFAULT_ENCODER = :sqids
11
+
12
+ # @rbs @salt: String?
13
+ # @rbs @character_group_size: Integer
14
+ # @rbs @alphabet: ::EncodedId::Alphabet
15
+ # @rbs @id_length: Integer
16
+ # @rbs @slug_value_method_name: Symbol
17
+ # @rbs @annotation_method_name: Symbol
18
+ # @rbs @model_to_param_returns_encoded_id: bool
19
+ # @rbs @blocklist: ::EncodedId::Blocklist
20
+ # @rbs @blocklist_mode: Symbol
21
+ # @rbs @blocklist_max_length: Integer
22
+ # @rbs @group_separator: String
23
+ # @rbs @slugged_id_separator: String
24
+ # @rbs @annotated_id_separator: String
25
+ # @rbs @encoder: Symbol
26
+ # @rbs @downcase_on_decode: bool
11
27
 
28
+ attr_accessor :salt #: String?
29
+ attr_accessor :character_group_size #: Integer
30
+ attr_accessor :alphabet #: ::EncodedId::Alphabet
31
+ attr_accessor :id_length #: Integer
32
+ attr_accessor :slug_value_method_name #: Symbol
33
+ attr_accessor :annotation_method_name #: Symbol
34
+ attr_accessor :model_to_param_returns_encoded_id #: bool
35
+ attr_accessor :blocklist #: ::EncodedId::Blocklist
36
+ attr_accessor :blocklist_mode #: Symbol
37
+ attr_accessor :blocklist_max_length #: Integer
38
+ attr_accessor :downcase_on_decode #: bool
39
+ attr_reader :group_separator #: String
40
+ attr_reader :slugged_id_separator #: String
41
+ attr_reader :annotated_id_separator #: String
42
+ attr_reader :encoder #: Symbol
43
+
44
+ # @rbs () -> void
12
45
  def initialize
13
46
  @character_group_size = 4
14
47
  @group_separator = "-"
@@ -19,10 +52,25 @@ module EncodedId
19
52
  @annotation_method_name = :annotation_for_encoded_id
20
53
  @annotated_id_separator = "_"
21
54
  @model_to_param_returns_encoded_id = false
55
+ @encoder = DEFAULT_ENCODER
56
+ @blocklist = ::EncodedId::Blocklist.empty
57
+ @blocklist_mode = :length_threshold
58
+ @blocklist_max_length = 32
59
+ @downcase_on_decode = false
60
+ @salt = nil
61
+ end
62
+
63
+ # @rbs (Symbol value) -> Symbol
64
+ def encoder=(value)
65
+ unless VALID_ENCODERS.include?(value)
66
+ raise ArgumentError, "Encoder must be one of: #{VALID_ENCODERS.join(", ")}"
67
+ end
68
+ @encoder = value
22
69
  end
23
70
 
24
71
  # Perform validation vs alphabet on these assignments
25
72
 
73
+ # @rbs (String value) -> String
26
74
  def group_separator=(value)
27
75
  unless valid_separator?(value, alphabet)
28
76
  raise ArgumentError, "Group separator characters must not be part of the alphabet"
@@ -30,22 +78,30 @@ module EncodedId
30
78
  @group_separator = value
31
79
  end
32
80
 
81
+ # @rbs (String value) -> String
33
82
  def slugged_id_separator=(value)
34
- if value.blank? || value == group_separator || !valid_separator?(value, alphabet)
35
- raise ArgumentError, "Slugged ID separator characters must not be part of the alphabet or the same as the group separator"
36
- end
83
+ validate_id_separator!(value, "Slugged ID")
37
84
  @slugged_id_separator = value
38
85
  end
39
86
 
87
+ # @rbs (String value) -> String
40
88
  def annotated_id_separator=(value)
89
+ validate_id_separator!(value, "Annotated ID")
90
+ @annotated_id_separator = value
91
+ end
92
+
93
+ private
94
+
95
+ # @rbs (String value, String separator_name) -> void
96
+ def validate_id_separator!(value, separator_name)
41
97
  if value.blank? || value == group_separator || !valid_separator?(value, alphabet)
42
- raise ArgumentError, "Annotated ID separator characters must not be part of the alphabet or the same as the group separator"
98
+ raise ArgumentError, "#{separator_name} separator characters must not be part of the alphabet or the same as the group separator"
43
99
  end
44
- @annotated_id_separator = value
45
100
  end
46
101
 
102
+ # @rbs (String separator, ::EncodedId::Alphabet characters) -> bool
47
103
  def valid_separator?(separator, characters)
48
- separator.chars.none? { |v| characters.include?(v) }
104
+ separator.chars.none? { characters.include?(_1) }
49
105
  end
50
106
  end
51
107
  end
@@ -1,36 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
7
+ # Provides methods for encoding and decoding IDs, extended into ActiveRecord models.
5
8
  module EncoderMethods
9
+ # @rbs!
10
+ # interface _EncodedIdModel
11
+ # def encoded_id_options: () -> Hash[Symbol, untyped]
12
+ # end
13
+ # @rbs (Array[Integer] | Integer ids, ?Hash[Symbol, untyped] options) -> String
6
14
  def encode_encoded_id(ids, options = {})
7
15
  raise StandardError, "You must pass an ID or array of IDs" if ids.blank?
8
16
  encoded_id_coder(options).encode(ids)
9
17
  end
10
18
 
19
+ # @rbs (String slugged_encoded_id, ?Hash[Symbol, untyped] options) -> Array[Integer]?
11
20
  def decode_encoded_id(slugged_encoded_id, options = {})
12
21
  return if slugged_encoded_id.blank?
13
22
  raise StandardError, "You must pass a string encoded ID" unless slugged_encoded_id.is_a?(String)
14
- annotated_encoded_id = SluggedIdParser.new(slugged_encoded_id, separator: EncodedId::Rails.configuration.slugged_id_separator).id
15
- encoded_id = AnnotatedIdParser.new(annotated_encoded_id, separator: EncodedId::Rails.configuration.annotated_id_separator).id
23
+ config = EncodedId::Rails.configuration
24
+ annotated_encoded_id = SluggedIdParser.new(slugged_encoded_id, separator: config.slugged_id_separator).id
25
+ encoded_id = AnnotatedIdParser.new(annotated_encoded_id, separator: config.annotated_id_separator).id
16
26
  return if !encoded_id || encoded_id.blank?
17
27
  encoded_id_coder(options).decode(encoded_id)
18
28
  end
19
29
 
20
30
  # This can be overridden in the model to provide a custom salt
31
+ # @rbs return: String
21
32
  def encoded_id_salt
22
33
  # @type self: Class
23
34
  EncodedId::Rails::Salt.new(self, EncodedId::Rails.configuration.salt).generate!
24
35
  end
25
36
 
37
+ # @rbs (?Hash[Symbol, untyped] options) -> EncodedId::Rails::Coder
38
+ # | (_EncodedIdModel self, ?Hash[Symbol, untyped] options) -> EncodedId::Rails::Coder
26
39
  def encoded_id_coder(options = {})
27
40
  config = EncodedId::Rails.configuration
41
+ # Merge model-level options with call-time options (call-time options take precedence)
42
+ # @type var model_options: Hash[Symbol, untyped]
43
+ model_options = respond_to?(:encoded_id_options) ? encoded_id_options : {} #: Hash[Symbol, untyped]
44
+ merged_options = model_options.merge(options)
45
+
28
46
  EncodedId::Rails::Coder.new(
29
- salt: options[:salt] || encoded_id_salt,
30
- id_length: options[:id_length] || config.id_length,
31
- character_group_size: options.key?(:character_group_size) ? options[:character_group_size] : config.character_group_size,
32
- alphabet: options[:alphabet] || config.alphabet,
33
- separator: options.key?(:separator) ? options[:separator] : config.group_separator
47
+ salt: merged_options[:salt] || encoded_id_salt,
48
+ id_length: merged_options[:id_length] || config.id_length,
49
+ character_group_size: merged_options.key?(:character_group_size) ? merged_options[:character_group_size] : config.character_group_size,
50
+ alphabet: merged_options[:alphabet] || config.alphabet,
51
+ separator: merged_options.key?(:separator) ? merged_options[:separator] : config.group_separator,
52
+ encoder: merged_options[:encoder] || config.encoder,
53
+ blocklist: merged_options[:blocklist] || config.blocklist,
54
+ blocklist_mode: merged_options[:blocklist_mode] || config.blocklist_mode,
55
+ blocklist_max_length: merged_options[:blocklist_max_length] || config.blocklist_max_length,
56
+ downcase_on_decode: merged_options.key?(:downcase_on_decode) ? merged_options[:downcase_on_decode] : config.downcase_on_decode
34
57
  )
35
58
  end
36
59
  end
@@ -1,9 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
7
+ # Provides finder methods for locating records by their encoded IDs.
5
8
  module FinderMethods
9
+ # @rbs!
10
+ # include ::EncodedId::Rails::EncoderMethods
11
+ # include ActiveRecordFinders
12
+
6
13
  # Find by encoded ID and optionally ensure record ID is the same as constraint (can be slugged)
14
+ # @rbs (String encoded_id, ?with_id: Symbol?) -> untyped
7
15
  def find_by_encoded_id(encoded_id, with_id: nil)
8
16
  decoded_id = decode_encoded_id(encoded_id)
9
17
  return if decoded_id.nil? || decoded_id.blank?
@@ -13,6 +21,7 @@ module EncodedId
13
21
  record
14
22
  end
15
23
 
24
+ # @rbs (String encoded_id, ?with_id: Symbol?) -> untyped
16
25
  def find_by_encoded_id!(encoded_id, with_id: nil)
17
26
  decoded_id = decode_encoded_id(encoded_id)
18
27
  raise ActiveRecord::RecordNotFound if decoded_id.nil? || decoded_id.blank?
@@ -23,12 +32,14 @@ module EncodedId
23
32
  record
24
33
  end
25
34
 
35
+ # @rbs (String encoded_id) -> Array[untyped]?
26
36
  def find_all_by_encoded_id(encoded_id)
27
37
  decoded_ids = decode_encoded_id(encoded_id)
28
38
  return if decoded_ids.blank?
29
39
  where(id: decoded_ids).to_a
30
40
  end
31
41
 
42
+ # @rbs (String encoded_id) -> Array[untyped]
32
43
  def find_all_by_encoded_id!(encoded_id)
33
44
  decoded_ids = decode_encoded_id(encoded_id)
34
45
  raise ActiveRecord::RecordNotFound if decoded_ids.nil? || decoded_ids.blank?
@@ -1,24 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  require "active_record"
4
6
  require "encoded_id"
5
7
 
6
8
  module EncodedId
7
9
  module Rails
10
+ # Main module for adding encoded ID functionality to ActiveRecord models.
8
11
  module Model
12
+ # @rbs!
13
+ # extend ::EncodedId::Rails::EncoderMethods
14
+ # extend ::EncodedId::Rails::FinderMethods
15
+ # extend ::EncodedId::Rails::QueryMethods
16
+ #
17
+ #
18
+ # # Model attributes that must exist, plus related AR methods
19
+ # def id: () -> Integer?
20
+
21
+ # @rbs @encoded_id_hash: String?
22
+ # @rbs @encoded_id: String?
23
+ # @rbs @slugged_encoded_id: String?
24
+ # @rbs @encoded_id_memoized_with_id: untyped
25
+
26
+ # @rbs (untyped base) -> void
9
27
  def self.included(base)
10
28
  base.extend(EncoderMethods)
11
29
  base.extend(FinderMethods)
12
30
  base.extend(QueryMethods)
31
+ base.extend(ClassMethods)
13
32
 
14
- # Automatically include PathParam if configured to do so
33
+ # Conditionally include PathParam based on global configuration
15
34
  if EncodedId::Rails.configuration.model_to_param_returns_encoded_id
16
35
  base.include(EncodedId::Rails::PathParam)
17
36
  end
18
37
  end
19
38
 
20
- attr_accessor :encoded_id_memoized_with_id
39
+ module ClassMethods
40
+ # Configure encoder options for this specific model
41
+ # @example
42
+ # class MyModel < ApplicationRecord
43
+ # include EncodedId::Rails::Model
44
+ # encoded_id_config encoder: :hashids
45
+ # end
46
+ #
47
+ # @rbs (**untyped options) -> void
48
+ def encoded_id_config(**options)
49
+ @encoded_id_options = options
50
+ end
51
+
52
+ # Walks up the inheritance chain to find options if not set on this class
53
+ # @rbs () -> Hash[Symbol, untyped]
54
+ def encoded_id_options
55
+ return @encoded_id_options if defined?(@encoded_id_options) && @encoded_id_options
56
+
57
+ if superclass.respond_to?(:encoded_id_options)
58
+ superclass.encoded_id_options
59
+ else
60
+ {}
61
+ end
62
+ end
63
+ end
64
+
65
+ attr_accessor :encoded_id_memoized_with_id #: untyped
21
66
 
67
+ # @rbs () -> void
22
68
  def clear_encoded_id_cache!
23
69
  [:@encoded_id_hash, :@encoded_id, :@slugged_encoded_id].each do |var|
24
70
  remove_instance_variable(var) if instance_variable_defined?(var)
@@ -26,10 +72,12 @@ module EncodedId
26
72
  self.encoded_id_memoized_with_id = nil
27
73
  end
28
74
 
75
+ # @rbs () -> void
29
76
  def check_and_clear_memoization
30
77
  clear_encoded_id_cache! if encoded_id_memoized_with_id && encoded_id_memoized_with_id != id
31
78
  end
32
79
 
80
+ # @rbs () -> String?
33
81
  def encoded_id_hash
34
82
  return unless id
35
83
  check_and_clear_memoization
@@ -39,27 +87,31 @@ module EncodedId
39
87
  @encoded_id_hash = self.class.encode_encoded_id(id)
40
88
  end
41
89
 
90
+ # @rbs () -> String?
42
91
  def encoded_id
43
92
  return unless id
44
93
  check_and_clear_memoization
45
94
  return @encoded_id if defined?(@encoded_id)
46
95
 
47
96
  encoded = encoded_id_hash
48
- annotated_by = EncodedId::Rails.configuration.annotation_method_name
97
+ config = EncodedId::Rails.configuration
98
+ annotated_by = config.annotation_method_name
49
99
  return @encoded_id = encoded unless annotated_by && encoded
50
100
 
51
- separator = EncodedId::Rails.configuration.annotated_id_separator
101
+ separator = config.annotated_id_separator
52
102
  self.encoded_id_memoized_with_id = id
53
103
  @encoded_id = EncodedId::Rails::AnnotatedId.new(id_part: encoded, annotation: send(annotated_by.to_s), separator: separator).annotated_id
54
104
  end
55
105
 
106
+ # @rbs () -> String?
56
107
  def slugged_encoded_id
57
108
  return unless id
58
109
  check_and_clear_memoization
59
110
  return @slugged_encoded_id if defined?(@slugged_encoded_id)
60
111
 
61
- with = EncodedId::Rails.configuration.slug_value_method_name
62
- separator = EncodedId::Rails.configuration.slugged_id_separator
112
+ config = EncodedId::Rails.configuration
113
+ with = config.slug_value_method_name
114
+ separator = config.slugged_id_separator
63
115
  encoded = encoded_id
64
116
  return unless encoded
65
117
 
@@ -67,6 +119,7 @@ module EncodedId
67
119
  @slugged_encoded_id = EncodedId::Rails::SluggedId.new(id_part: encoded, slug_part: send(with.to_s), separator: separator).slugged_id
68
120
  end
69
121
 
122
+ # @rbs (?untyped? options) -> untyped
70
123
  def reload(options = nil)
71
124
  result = super
72
125
  clear_encoded_id_cache!
@@ -80,7 +133,7 @@ module EncodedId
80
133
  name.underscore
81
134
  end
82
135
 
83
- # By default trying to generate a slug without defining how will raise.
136
+ # By default, trying to generate a slug without defining how will raise.
84
137
  # You either override this method per model, pass an alternate method name to
85
138
  # #slugged_encoded_id or setup an alias to another model method in your ApplicationRecord class
86
139
  def name_for_encoded_id_slug
@@ -1,11 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  require "active_record"
4
6
  require "encoded_id"
5
7
 
6
8
  module EncodedId
7
9
  module Rails
10
+ # Overrides to_param to return the encoded ID for use in URLs.
8
11
  module PathParam
12
+ # Method provided by model
13
+ # @rbs!
14
+ # def encoded_id: () -> String?
15
+
16
+ # @rbs () -> String
9
17
  def to_param
10
18
  encoded_id || raise(StandardError, "Cannot create path param for #{self.class.name} without an encoded id")
11
19
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
7
+ # Provides persistence of encoded IDs to database columns with validation and callbacks.
5
8
  module Persists
9
+ # @rbs (untyped base) -> void
6
10
  def self.included(base)
7
11
  base.extend ClassMethods
8
12
 
@@ -19,13 +23,37 @@ module EncodedId
19
23
  end
20
24
 
21
25
  module ClassMethods
26
+ # Encoder methods come from ::EncodedId::Rails::Model but thats not working with this pattern of defining class
27
+ # methods.
28
+ # @rbs!
29
+ # include ::EncodedId::Rails::EncoderMethods
30
+
31
+ # @rbs (Integer id) -> String
22
32
  def encode_normalized_encoded_id(id)
23
33
  encode_encoded_id(id, character_group_size: nil)
24
34
  end
25
35
  end
26
36
 
37
+ # @rbs!
38
+ # include ::ActiveRecord::Persistence
39
+ #
40
+ # include ::EncodedId::Rails::Model
41
+ #
42
+ # extend ClassMethods
43
+ #
44
+ # # Model attributes that must exist, plus related AR methods
45
+ # def id: () -> Integer?
46
+ # def id_changed?: () -> bool
47
+ # def prefixed_encoded_id: () -> String?
48
+ # def prefixed_encoded_id=: (String?) -> String?
49
+ # def normalized_encoded_id: () -> String?
50
+ # def normalized_encoded_id=: (String?) -> String?
51
+ # def clear_prefixed_encoded_id_change: () -> void
52
+ # def clear_normalized_encoded_id_change: () -> void
53
+
27
54
  # On duplication we need to reset the encoded ID to nil as this new record will have a new ID.
28
- # We need to also prevent these changes from marking the record as dirty.
55
+ # We also prevent these changes from marking the record as dirty.
56
+ # @rbs () -> untyped
29
57
  def dup
30
58
  copy = super
31
59
  copy.prefixed_encoded_id = nil
@@ -35,39 +63,57 @@ module EncodedId
35
63
  copy
36
64
  end
37
65
 
38
- def set_normalized_encoded_id!
66
+ # @rbs () -> Integer
67
+ def resolved_id
39
68
  validate_id_for_encoded_id!
40
69
 
41
- update_columns(normalized_encoded_id: self.class.encode_normalized_encoded_id(id), prefixed_encoded_id: encoded_id)
70
+ id #: Integer
42
71
  end
43
72
 
44
- def update_normalized_encoded_id!
45
- validate_id_for_encoded_id!
73
+ # @rbs () -> void
74
+ def set_normalized_encoded_id!
75
+ update_columns(normalized_encoded_id: self.class.encode_normalized_encoded_id(resolved_id), prefixed_encoded_id: encoded_id)
76
+ end
46
77
 
47
- self.normalized_encoded_id = self.class.encode_normalized_encoded_id(id)
78
+ # @rbs () -> void
79
+ def update_normalized_encoded_id!
80
+ self.normalized_encoded_id = self.class.encode_normalized_encoded_id(resolved_id)
48
81
  self.prefixed_encoded_id = encoded_id
49
82
  end
50
83
 
84
+ # @rbs () -> void
51
85
  def check_encoded_id_persisted!
52
- if normalized_encoded_id != self.class.encode_normalized_encoded_id(id)
53
- raise StandardError, "The persisted encoded ID #{normalized_encoded_id} for #{self.class.name} is not the same as currently computing #{self.class.encode_normalized_encoded_id(id)}"
86
+ validate_id_for_encoded_id!
87
+
88
+ klass = self.class
89
+ klass_name = klass.name
90
+ encoded_from_current_id = klass.encode_normalized_encoded_id(resolved_id)
91
+
92
+ if normalized_encoded_id != encoded_from_current_id
93
+ raise StandardError, "The persisted encoded ID #{normalized_encoded_id} for #{klass_name} is not the same as currently computing #{encoded_from_current_id}"
54
94
  end
55
95
 
56
- raise StandardError, "The persisted prefixed encoded ID (for #{self.class.name} with id: #{id}, normalized_encoded_id: #{normalized_encoded_id}) is not correct: it is #{prefixed_encoded_id} instead of #{encoded_id}" if prefixed_encoded_id != encoded_id
96
+ return if prefixed_encoded_id == encoded_id
97
+
98
+ raise StandardError, "The persisted prefixed encoded ID (for #{klass_name} with id: #{id}, normalized_encoded_id: #{normalized_encoded_id}) is not correct: it is #{prefixed_encoded_id} instead of #{encoded_id}"
57
99
  end
58
100
 
101
+ # @rbs () -> bool
59
102
  def should_update_normalized_encoded_id?
60
103
  id_changed? || (normalized_encoded_id.blank? && persisted?)
61
104
  end
62
105
 
106
+ # @rbs () -> void
63
107
  def validate_id_for_encoded_id!
64
108
  raise StandardError, "You cannot set the normalized ID of a record which is not persisted" if id.blank?
65
109
  end
66
110
 
111
+ # @rbs () -> void
67
112
  def prevent_update_of_normalized_encoded_id!
68
113
  raise ActiveRecord::ReadonlyAttributeError, "You cannot update the normalized encoded ID '#{normalized_encoded_id}' of a record #{self.class.name} #{id}, if you need to refresh it use set_normalized_encoded_id!"
69
114
  end
70
115
 
116
+ # @rbs () -> void
71
117
  def prevent_update_of_prefixed_encoded_id!
72
118
  raise ActiveRecord::ReadonlyAttributeError, "You cannot update the prefixed encoded ID '#{prefixed_encoded_id}' of a record #{self.class.name} #{id}, if you need to refresh it use set_normalized_encoded_id!"
73
119
  end
@@ -1,12 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
7
+ # Provides query methods for finding records using encoded IDs in where clauses.
5
8
  module QueryMethods
6
- def where_encoded_id(slugged_encoded_id)
7
- decoded_id = decode_encoded_id(slugged_encoded_id)
8
- raise ActiveRecord::RecordNotFound if decoded_id.nil?
9
- where(id: decoded_id)
9
+ # Methods provided by other mixins/ActiveRecord
10
+ # @rbs!
11
+ # def decode_encoded_id: (String) -> Array[Integer]?
12
+ # def where: (**untyped) -> untyped
13
+
14
+ # @rbs (*String slugged_encoded_ids) -> untyped
15
+ def where_encoded_id(*slugged_encoded_ids)
16
+ slugged_encoded_ids = slugged_encoded_ids.flatten
17
+
18
+ raise ::ActiveRecord::RecordNotFound if slugged_encoded_ids.empty?
19
+
20
+ decoded_ids = slugged_encoded_ids.flat_map do |slugged_encoded_id|
21
+ decode_encoded_id(slugged_encoded_id).tap do |decoded_id|
22
+ raise ::ActiveRecord::RecordNotFound if decoded_id.nil?
23
+ end
24
+ end
25
+
26
+ where(id: decoded_ids)
10
27
  end
11
28
  end
12
29
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module EncodedId
6
+ module Rails
7
+ # Railtie for Rails integration
8
+ class Railtie < ::Rails::Railtie
9
+ # initializer "encoded_id.rails.initialize" do
10
+ # end
11
+ end
12
+ end
13
+ end
@@ -1,13 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
7
+ # Generates a unique salt for encoding IDs based on the model class name.
5
8
  class Salt
9
+ # @rbs @klass: Class
10
+ # @rbs @salt: String
11
+
12
+ # @rbs (Class klass, String salt) -> void
6
13
  def initialize(klass, salt)
7
14
  @klass = klass
8
15
  @salt = salt
9
16
  end
10
17
 
18
+ # @rbs return: String
11
19
  def generate!
12
20
  unless @klass.respond_to?(:name) && @klass.name.present?
13
21
  raise ::StandardError, "The class must have a `name` to ensure encode id uniqueness. " \
@@ -1,21 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cgi"
3
+ # rbs_inline: enabled
4
4
 
5
5
  module EncodedId
6
6
  module Rails
7
- class SluggedId
7
+ # Represents an encoded ID with a slug prefix (e.g., "my-post--ABC123").
8
+ class SluggedId < CompositeIdBase
9
+ # @rbs (slug_part: String, id_part: String, ?separator: String) -> void
8
10
  def initialize(slug_part:, id_part:, separator: "--")
9
- @slug_part = slug_part
10
- @id_part = id_part
11
- @separator = separator
11
+ super(first_part: slug_part, id_part: id_part, separator: separator)
12
12
  end
13
13
 
14
+ # @rbs return: String
14
15
  def slugged_id
15
- unless @id_part.present? && @slug_part.present?
16
- raise ::StandardError, "The model does not return a valid ID and/or slug"
17
- end
18
- "#{@slug_part.to_s.parameterize}#{CGI.escape(@separator)}#{@id_part}"
16
+ build_composite_id
17
+ end
18
+
19
+ private
20
+
21
+ # @rbs return: String
22
+ def invalid_id_error_message
23
+ "The model does not return a valid ID and/or slug"
19
24
  end
20
25
  end
21
26
  end
@@ -1,8 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
7
+ # Parses a slugged ID into its slug and ID components.
5
8
  class SluggedIdParser
9
+ # @rbs @slug: String?
10
+ # @rbs @id: String
11
+
12
+ # @rbs (String slugged_id, ?separator: String) -> void
6
13
  def initialize(slugged_id, separator: "--")
7
14
  if separator && slugged_id.include?(separator)
8
15
  parts = slugged_id.split(separator)
@@ -13,7 +20,8 @@ module EncodedId
13
20
  end
14
21
  end
15
22
 
16
- attr_reader :slug, :id
23
+ attr_reader :slug #: String?
24
+ attr_reader :id #: String
17
25
  end
18
26
  end
19
27
  end
@@ -1,11 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  require "active_record"
4
6
  require "encoded_id"
5
7
 
6
8
  module EncodedId
7
9
  module Rails
10
+ # Overrides to_param to return the slugged encoded ID for use in URLs.
8
11
  module SluggedPathParam
12
+ # Method provided by model
13
+ # @rbs!
14
+ # def slugged_encoded_id: () -> String?
15
+
16
+ # @rbs () -> String
9
17
  def to_param
10
18
  slugged_encoded_id || raise(StandardError, "Cannot create path param for #{self.class.name} without an encoded id")
11
19
  end