encoded_id-rails 1.0.0.beta3 → 1.0.0.rc6

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -18
  3. data/LICENSE.txt +1 -1
  4. data/README.md +77 -368
  5. data/context/encoded_id-rails.md +433 -0
  6. data/context/encoded_id.md +283 -0
  7. data/lib/encoded_id/rails/active_record_finders.rb +52 -0
  8. data/lib/encoded_id/rails/annotated_id.rb +8 -0
  9. data/lib/encoded_id/rails/annotated_id_parser.rb +8 -1
  10. data/lib/encoded_id/rails/coder.rb +20 -2
  11. data/lib/encoded_id/rails/configuration.rb +45 -3
  12. data/lib/encoded_id/rails/encoder_methods.rb +9 -1
  13. data/lib/encoded_id/rails/finder_methods.rb +10 -0
  14. data/lib/encoded_id/rails/model.rb +65 -8
  15. data/lib/encoded_id/rails/path_param.rb +7 -0
  16. data/lib/encoded_id/rails/persists.rb +120 -0
  17. data/lib/encoded_id/rails/query_methods.rb +20 -4
  18. data/lib/encoded_id/rails/railtie.rb +13 -0
  19. data/lib/encoded_id/rails/salt.rb +7 -0
  20. data/lib/encoded_id/rails/slugged_id.rb +8 -0
  21. data/lib/encoded_id/rails/slugged_id_parser.rb +8 -1
  22. data/lib/encoded_id/rails/slugged_path_param.rb +7 -0
  23. data/lib/encoded_id/rails.rb +10 -6
  24. data/lib/generators/encoded_id/rails/USAGE +4 -0
  25. data/lib/generators/encoded_id/rails/add_columns_generator.rb +45 -0
  26. data/lib/generators/encoded_id/rails/templates/add_encoded_id_columns_migration.rb.erb +16 -0
  27. data/lib/generators/encoded_id/rails/templates/encoded_id.rb +28 -0
  28. metadata +27 -52
  29. data/.standard.yml +0 -3
  30. data/Appraisals +0 -14
  31. data/Gemfile +0 -18
  32. data/Rakefile +0 -14
  33. data/Steepfile +0 -4
  34. data/gemfiles/.bundle/config +0 -2
  35. data/gemfiles/rails_6.1.gemfile +0 -16
  36. data/gemfiles/rails_6.1.gemfile.lock +0 -130
  37. data/gemfiles/rails_7.0.gemfile +0 -16
  38. data/gemfiles/rails_7.0.gemfile.lock +0 -128
  39. data/gemfiles/rails_7.1.gemfile +0 -16
  40. data/gemfiles/rails_7.1.gemfile.lock +0 -140
  41. data/lib/encoded_id/rails/version.rb +0 -7
  42. data/rbs_collection.yaml +0 -24
  43. data/sig/encoded_id/rails.rbs +0 -141
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module EncodedId
6
+ module Rails
7
+ # This module overrides standard ActiveRecord finder methods to automatically decode encoded IDs.
8
+ # Important: This module should NOT be used with models that use string-based primary keys (e.g., UUIDs)
9
+ # as it will cause conflicts between string IDs and encoded IDs.
10
+ module ActiveRecordFinders
11
+ extend ActiveSupport::Concern
12
+
13
+ # @rbs!
14
+ # include ::ActiveRecord::FinderMethods
15
+ # extend ::ActiveRecord::QueryMethods
16
+
17
+ included do
18
+ if columns_hash["id"]&.type == :string
19
+ ::Rails.logger.warn("EncodedId::Rails::ActiveRecordFinders has been included in #{name}, but this model uses string-based IDs. This may cause conflicts with encoded ID handling.")
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ # @rbs (*untyped args) -> untyped
25
+ def find(*args)
26
+ return super unless args.size == 1 && args.first.is_a?(String)
27
+
28
+ decoded_ids = decode_encoded_id(args.first)
29
+
30
+ if decoded_ids.blank?
31
+ raise ::ActiveRecord::RecordNotFound
32
+ elsif decoded_ids.size == 1
33
+ super(decoded_ids.first)
34
+ else
35
+ super(decoded_ids)
36
+ end
37
+ end
38
+
39
+ # Override find_by_id to handle encoded IDs
40
+ # @rbs (untyped id) -> untyped
41
+ def find_by_id(id)
42
+ if id.is_a?(String)
43
+ decoded_ids = decode_encoded_id(id)
44
+ return nil if decoded_ids.blank?
45
+ return super(decoded_ids.first)
46
+ end
47
+ super
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,16 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  require "cgi"
4
6
 
5
7
  module EncodedId
6
8
  module Rails
7
9
  class AnnotatedId
10
+ # @rbs @annotation: String
11
+ # @rbs @id_part: String
12
+ # @rbs @separator: String
13
+
14
+ # @rbs (annotation: String, id_part: String, ?separator: String) -> void
8
15
  def initialize(annotation:, id_part:, separator: "_")
9
16
  @annotation = annotation
10
17
  @id_part = id_part
11
18
  @separator = separator
12
19
  end
13
20
 
21
+ # @rbs return: String
14
22
  def annotated_id
15
23
  unless @id_part.present? && @annotation.present?
16
24
  raise ::StandardError, "The model does not provide a valid ID and/or annotation"
@@ -1,8 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
5
7
  class AnnotatedIdParser
8
+ # @rbs @annotation: String?
9
+ # @rbs @id: String
10
+
11
+ # @rbs (String annotated_id, ?separator: String) -> void
6
12
  def initialize(annotated_id, separator: "_")
7
13
  if separator && annotated_id.include?(separator)
8
14
  parts = annotated_id.split(separator)
@@ -13,7 +19,8 @@ module EncodedId
13
19
  end
14
20
  end
15
21
 
16
- attr_reader :annotation, :id
22
+ attr_reader :annotation #: String?
23
+ attr_reader :id #: String
17
24
  end
18
25
  end
19
26
  end
@@ -1,20 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
5
7
  class Coder
6
- def initialize(salt:, id_length:, character_group_size:, separator:, alphabet:)
8
+ # @rbs @salt: String
9
+ # @rbs @id_length: Integer
10
+ # @rbs @character_group_size: Integer
11
+ # @rbs @separator: String
12
+ # @rbs @alphabet: ::EncodedId::Alphabet
13
+ # @rbs @encoder: (Symbol | ::EncodedId::Encoders::Base)
14
+ # @rbs @blocklist: ::EncodedId::Blocklist?
15
+
16
+ # @rbs (salt: String, id_length: Integer, character_group_size: Integer, separator: String, alphabet: ::EncodedId::Alphabet, ?encoder: Symbol?, ?blocklist: ::EncodedId::Blocklist?) -> void
17
+ def initialize(salt:, id_length:, character_group_size:, separator:, alphabet:, encoder: nil, blocklist: nil)
7
18
  @salt = salt
8
19
  @id_length = id_length
9
20
  @character_group_size = character_group_size
10
21
  @separator = separator
11
22
  @alphabet = alphabet
23
+ @encoder = encoder || EncodedId::Rails.configuration.encoder
24
+ @blocklist = blocklist || EncodedId::Rails.configuration.blocklist
12
25
  end
13
26
 
27
+ # @rbs (Integer | Array[Integer] id) -> String
14
28
  def encode(id)
15
29
  coder.encode(id)
16
30
  end
17
31
 
32
+ # @rbs (String encoded_id) -> Array[Integer]?
18
33
  def decode(encoded_id)
19
34
  coder.decode(encoded_id)
20
35
  rescue EncodedId::EncodedIdFormatError, EncodedId::InvalidInputError
@@ -23,13 +38,16 @@ module EncodedId
23
38
 
24
39
  private
25
40
 
41
+ # @rbs return: ::EncodedId::ReversibleId
26
42
  def coder
27
43
  ::EncodedId::ReversibleId.new(
28
44
  salt: @salt,
29
45
  length: @id_length,
30
46
  split_at: @character_group_size,
31
47
  split_with: @separator,
32
- alphabet: @alphabet
48
+ alphabet: @alphabet,
49
+ encoder: @encoder,
50
+ blocklist: @blocklist
33
51
  )
34
52
  end
35
53
  end
@@ -1,13 +1,38 @@
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_reader :group_separator, :slugged_id_separator, :annotated_id_separator
9
+ # @rbs @salt: String
10
+ # @rbs @character_group_size: Integer
11
+ # @rbs @alphabet: ::EncodedId::Alphabet
12
+ # @rbs @id_length: Integer
13
+ # @rbs @slug_value_method_name: Symbol
14
+ # @rbs @annotation_method_name: Symbol
15
+ # @rbs @model_to_param_returns_encoded_id: bool
16
+ # @rbs @blocklist: ::EncodedId::Blocklist
17
+ # @rbs @group_separator: String
18
+ # @rbs @slugged_id_separator: String
19
+ # @rbs @annotated_id_separator: String
20
+ # @rbs @encoder: Symbol
21
+
22
+ attr_accessor :salt #: String
23
+ attr_accessor :character_group_size #: Integer
24
+ attr_accessor :alphabet #: ::EncodedId::Alphabet
25
+ attr_accessor :id_length #: Integer
26
+ attr_accessor :slug_value_method_name #: Symbol
27
+ attr_accessor :annotation_method_name #: Symbol
28
+ attr_accessor :model_to_param_returns_encoded_id #: bool
29
+ attr_accessor :blocklist #: ::EncodedId::Blocklist
30
+ attr_reader :group_separator #: String
31
+ attr_reader :slugged_id_separator #: String
32
+ attr_reader :annotated_id_separator #: String
33
+ attr_reader :encoder #: Symbol
10
34
 
35
+ # @rbs () -> void
11
36
  def initialize
12
37
  @character_group_size = 4
13
38
  @group_separator = "-"
@@ -17,10 +42,22 @@ module EncodedId
17
42
  @slugged_id_separator = "--"
18
43
  @annotation_method_name = :annotation_for_encoded_id
19
44
  @annotated_id_separator = "_"
45
+ @model_to_param_returns_encoded_id = false
46
+ @encoder = :hashids
47
+ @blocklist = ::EncodedId::Blocklist.empty
48
+ end
49
+
50
+ # @rbs (Symbol value) -> Symbol
51
+ def encoder=(value)
52
+ unless ::EncodedId::ReversibleId::VALID_ENCODERS.include?(value)
53
+ raise ArgumentError, "Encoder must be one of: #{::EncodedId::ReversibleId::VALID_ENCODERS.join(", ")}"
54
+ end
55
+ @encoder = value
20
56
  end
21
57
 
22
58
  # Perform validation vs alphabet on these assignments
23
59
 
60
+ # @rbs (String value) -> String
24
61
  def group_separator=(value)
25
62
  unless valid_separator?(value, alphabet)
26
63
  raise ArgumentError, "Group separator characters must not be part of the alphabet"
@@ -28,6 +65,7 @@ module EncodedId
28
65
  @group_separator = value
29
66
  end
30
67
 
68
+ # @rbs (String value) -> String
31
69
  def slugged_id_separator=(value)
32
70
  if value.blank? || value == group_separator || !valid_separator?(value, alphabet)
33
71
  raise ArgumentError, "Slugged ID separator characters must not be part of the alphabet or the same as the group separator"
@@ -35,6 +73,7 @@ module EncodedId
35
73
  @slugged_id_separator = value
36
74
  end
37
75
 
76
+ # @rbs (String value) -> String
38
77
  def annotated_id_separator=(value)
39
78
  if value.blank? || value == group_separator || !valid_separator?(value, alphabet)
40
79
  raise ArgumentError, "Annotated ID separator characters must not be part of the alphabet or the same as the group separator"
@@ -42,6 +81,9 @@ module EncodedId
42
81
  @annotated_id_separator = value
43
82
  end
44
83
 
84
+ private
85
+
86
+ # @rbs (String separator, ::EncodedId::Alphabet characters) -> bool
45
87
  def valid_separator?(separator, characters)
46
88
  separator.chars.none? { |v| characters.include?(v) }
47
89
  end
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
5
7
  module EncoderMethods
8
+ # @rbs (Array[Integer] | Integer ids, ?Hash[Symbol, untyped] options) -> String
6
9
  def encode_encoded_id(ids, options = {})
7
10
  raise StandardError, "You must pass an ID or array of IDs" if ids.blank?
8
11
  encoded_id_coder(options).encode(ids)
9
12
  end
10
13
 
14
+ # @rbs (String slugged_encoded_id, ?Hash[Symbol, untyped] options) -> Array[Integer]?
11
15
  def decode_encoded_id(slugged_encoded_id, options = {})
12
16
  return if slugged_encoded_id.blank?
13
17
  raise StandardError, "You must pass a string encoded ID" unless slugged_encoded_id.is_a?(String)
@@ -18,11 +22,13 @@ module EncodedId
18
22
  end
19
23
 
20
24
  # This can be overridden in the model to provide a custom salt
25
+ # @rbs return: String
21
26
  def encoded_id_salt
22
27
  # @type self: Class
23
28
  EncodedId::Rails::Salt.new(self, EncodedId::Rails.configuration.salt).generate!
24
29
  end
25
30
 
31
+ # @rbs (?Hash[Symbol, untyped] options) -> EncodedId::Rails::Coder
26
32
  def encoded_id_coder(options = {})
27
33
  config = EncodedId::Rails.configuration
28
34
  EncodedId::Rails::Coder.new(
@@ -30,7 +36,9 @@ module EncodedId
30
36
  id_length: options[:id_length] || config.id_length,
31
37
  character_group_size: options.key?(:character_group_size) ? options[:character_group_size] : config.character_group_size,
32
38
  alphabet: options[:alphabet] || config.alphabet,
33
- separator: options.key?(:separator) ? options[:separator] : config.group_separator
39
+ separator: options.key?(:separator) ? options[:separator] : config.group_separator,
40
+ encoder: options[:encoder] || config.encoder,
41
+ blocklist: options[:blocklist] || config.blocklist
34
42
  )
35
43
  end
36
44
  end
@@ -1,9 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
5
7
  module FinderMethods
8
+ # @rbs!
9
+ # include ::EncodedId::Rails::EncoderMethods
10
+ # include ActiveRecordFinders
11
+
6
12
  # Find by encoded ID and optionally ensure record ID is the same as constraint (can be slugged)
13
+ # @rbs (String encoded_id, ?with_id: Symbol?) -> untyped
7
14
  def find_by_encoded_id(encoded_id, with_id: nil)
8
15
  decoded_id = decode_encoded_id(encoded_id)
9
16
  return if decoded_id.nil? || decoded_id.blank?
@@ -13,6 +20,7 @@ module EncodedId
13
20
  record
14
21
  end
15
22
 
23
+ # @rbs (String encoded_id, ?with_id: Symbol?) -> untyped
16
24
  def find_by_encoded_id!(encoded_id, with_id: nil)
17
25
  decoded_id = decode_encoded_id(encoded_id)
18
26
  raise ActiveRecord::RecordNotFound if decoded_id.nil? || decoded_id.blank?
@@ -23,12 +31,14 @@ module EncodedId
23
31
  record
24
32
  end
25
33
 
34
+ # @rbs (String encoded_id) -> Array[untyped]?
26
35
  def find_all_by_encoded_id(encoded_id)
27
36
  decoded_ids = decode_encoded_id(encoded_id)
28
37
  return if decoded_ids.blank?
29
38
  where(id: decoded_ids).to_a
30
39
  end
31
40
 
41
+ # @rbs (String encoded_id) -> Array[untyped]
32
42
  def find_all_by_encoded_id!(encoded_id)
33
43
  decoded_ids = decode_encoded_id(encoded_id)
34
44
  raise ActiveRecord::RecordNotFound if decoded_ids.nil? || decoded_ids.blank?
@@ -1,43 +1,101 @@
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
8
10
  module Model
11
+ # @rbs!
12
+ # extend ::EncodedId::Rails::EncoderMethods
13
+ # extend ::EncodedId::Rails::FinderMethods
14
+ # extend ::EncodedId::Rails::QueryMethods
15
+ #
16
+ #
17
+ # # Model attributes that must exist, plus related AR methods
18
+ # def id: () -> Integer?
19
+
20
+ # @rbs @encoded_id_hash: String?
21
+ # @rbs @encoded_id: String?
22
+ # @rbs @slugged_encoded_id: String?
23
+ # @rbs @encoded_id_memoized_with_id: untyped
24
+
25
+ # @rbs (untyped base) -> void
9
26
  def self.included(base)
10
27
  base.extend(EncoderMethods)
11
28
  base.extend(FinderMethods)
12
29
  base.extend(QueryMethods)
30
+
31
+ # Automatically include PathParam if configured to do so
32
+ if EncodedId::Rails.configuration.model_to_param_returns_encoded_id
33
+ base.include(EncodedId::Rails::PathParam)
34
+ end
35
+ end
36
+
37
+ attr_accessor :encoded_id_memoized_with_id #: untyped
38
+
39
+ # @rbs () -> void
40
+ def clear_encoded_id_cache!
41
+ [:@encoded_id_hash, :@encoded_id, :@slugged_encoded_id].each do |var|
42
+ remove_instance_variable(var) if instance_variable_defined?(var)
43
+ end
44
+ self.encoded_id_memoized_with_id = nil
13
45
  end
14
46
 
47
+ # @rbs () -> void
48
+ def check_and_clear_memoization
49
+ clear_encoded_id_cache! if encoded_id_memoized_with_id && encoded_id_memoized_with_id != id
50
+ end
51
+
52
+ # @rbs () -> String?
15
53
  def encoded_id_hash
16
54
  return unless id
17
- return @encoded_id_hash if defined?(@encoded_id_hash) && !id_changed?
18
- self.class.encode_encoded_id(id)
55
+ check_and_clear_memoization
56
+ return @encoded_id_hash if defined?(@encoded_id_hash)
57
+
58
+ self.encoded_id_memoized_with_id = id
59
+ @encoded_id_hash = self.class.encode_encoded_id(id)
19
60
  end
20
61
 
62
+ # @rbs () -> String?
21
63
  def encoded_id
22
64
  return unless id
23
- return @encoded_id if defined?(@encoded_id) && !id_changed?
65
+ check_and_clear_memoization
66
+ return @encoded_id if defined?(@encoded_id)
67
+
24
68
  encoded = encoded_id_hash
25
69
  annotated_by = EncodedId::Rails.configuration.annotation_method_name
26
70
  return @encoded_id = encoded unless annotated_by && encoded
71
+
27
72
  separator = EncodedId::Rails.configuration.annotated_id_separator
73
+ self.encoded_id_memoized_with_id = id
28
74
  @encoded_id = EncodedId::Rails::AnnotatedId.new(id_part: encoded, annotation: send(annotated_by.to_s), separator: separator).annotated_id
29
75
  end
30
76
 
77
+ # @rbs () -> String?
31
78
  def slugged_encoded_id
32
79
  return unless id
33
- return @slugged_encoded_id if defined?(@slugged_encoded_id) && !id_changed?
80
+ check_and_clear_memoization
81
+ return @slugged_encoded_id if defined?(@slugged_encoded_id)
82
+
34
83
  with = EncodedId::Rails.configuration.slug_value_method_name
35
84
  separator = EncodedId::Rails.configuration.slugged_id_separator
36
85
  encoded = encoded_id
37
86
  return unless encoded
87
+
88
+ self.encoded_id_memoized_with_id = id
38
89
  @slugged_encoded_id = EncodedId::Rails::SluggedId.new(id_part: encoded, slug_part: send(with.to_s), separator: separator).slugged_id
39
90
  end
40
91
 
92
+ # @rbs (?untyped? options) -> untyped
93
+ def reload(options = nil)
94
+ result = super
95
+ clear_encoded_id_cache!
96
+ result
97
+ end
98
+
41
99
  # By default the annotation is the model name (it will be parameterized)
42
100
  def annotation_for_encoded_id
43
101
  name = self.class.name
@@ -45,18 +103,17 @@ module EncodedId
45
103
  name.underscore
46
104
  end
47
105
 
48
- # By default trying to generate a slug without defining how will raise.
106
+ # By default, trying to generate a slug without defining how will raise.
49
107
  # You either override this method per model, pass an alternate method name to
50
108
  # #slugged_encoded_id or setup an alias to another model method in your ApplicationRecord class
51
109
  def name_for_encoded_id_slug
52
110
  raise StandardError, "You must define a method to generate the slug for the encoded ID of #{self.class.name}"
53
111
  end
54
112
 
55
- # When duplicating an ActiveRecord object, we want to reset the memoized encoded_id
113
+ # When duplicating an ActiveRecord object, we want to reset the memoized encoded IDs
56
114
  def dup
57
115
  super.tap do |new_record|
58
- new_record.send(:remove_instance_variable, :@encoded_id) if new_record.instance_variable_defined?(:@encoded_id)
59
- new_record.send(:remove_instance_variable, :@slugged_encoded_id) if new_record.instance_variable_defined?(:@slugged_encoded_id)
116
+ new_record.clear_encoded_id_cache!
60
117
  end
61
118
  end
62
119
  end
@@ -1,11 +1,18 @@
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
8
10
  module PathParam
11
+ # Method provided by model
12
+ # @rbs!
13
+ # def encoded_id: () -> String?
14
+
15
+ # @rbs () -> String
9
16
  def to_param
10
17
  encoded_id || raise(StandardError, "Cannot create path param for #{self.class.name} without an encoded id")
11
18
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module EncodedId
6
+ module Rails
7
+ module Persists
8
+ # @rbs (untyped base) -> void
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+
12
+ base.validates :normalized_encoded_id, uniqueness: true, allow_nil: true
13
+ base.validates :prefixed_encoded_id, uniqueness: true, allow_nil: true
14
+
15
+ base.before_validation :prevent_update_of_normalized_encoded_id!, if: :normalized_encoded_id_changed?
16
+ base.before_validation :prevent_update_of_prefixed_encoded_id!, if: :prefixed_encoded_id_changed?
17
+
18
+ base.after_create :set_normalized_encoded_id!
19
+ base.before_save :update_normalized_encoded_id!, if: :should_update_normalized_encoded_id?
20
+
21
+ base.after_commit :check_encoded_id_persisted!, on: [:create, :update]
22
+ end
23
+
24
+ module ClassMethods
25
+ # Encoder methods come from ::EncodedId::Rails::Model but thats not working with this pattern of defining class
26
+ # methods.
27
+ # @rbs!
28
+ # include ::EncodedId::Rails::EncoderMethods
29
+
30
+ # @rbs (Integer id) -> String
31
+ def encode_normalized_encoded_id(id)
32
+ encode_encoded_id(id, character_group_size: nil)
33
+ end
34
+ end
35
+
36
+ # Method provided by model
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
+
54
+ # On duplication we need to reset the encoded ID to nil as this new record will have a new ID.
55
+ # We need to also prevent these changes from marking the record as dirty.
56
+ # @rbs () -> untyped
57
+ def dup
58
+ copy = super
59
+ copy.prefixed_encoded_id = nil
60
+ copy.clear_prefixed_encoded_id_change
61
+ copy.normalized_encoded_id = nil
62
+ copy.clear_normalized_encoded_id_change
63
+ copy
64
+ end
65
+
66
+ # @rbs () -> Integer
67
+ def resolved_id
68
+ validate_id_for_encoded_id!
69
+
70
+ id #: Integer
71
+ end
72
+
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
77
+
78
+ # @rbs () -> void
79
+ def update_normalized_encoded_id!
80
+ self.normalized_encoded_id = self.class.encode_normalized_encoded_id(resolved_id)
81
+ self.prefixed_encoded_id = encoded_id
82
+ end
83
+
84
+ # @rbs () -> void
85
+ def check_encoded_id_persisted!
86
+ validate_id_for_encoded_id!
87
+
88
+ encoded_from_current_id = self.class.encode_normalized_encoded_id(resolved_id)
89
+
90
+ if normalized_encoded_id != encoded_from_current_id
91
+ raise StandardError, "The persisted encoded ID #{normalized_encoded_id} for #{self.class.name} is not the same as currently computing #{encoded_from_current_id}"
92
+ end
93
+
94
+ return if prefixed_encoded_id == encoded_id
95
+
96
+ 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}"
97
+ end
98
+
99
+ # @rbs () -> bool
100
+ def should_update_normalized_encoded_id?
101
+ id_changed? || (normalized_encoded_id.blank? && persisted?)
102
+ end
103
+
104
+ # @rbs () -> void
105
+ def validate_id_for_encoded_id!
106
+ raise StandardError, "You cannot set the normalized ID of a record which is not persisted" if id.blank?
107
+ end
108
+
109
+ # @rbs () -> void
110
+ def prevent_update_of_normalized_encoded_id!
111
+ 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!"
112
+ end
113
+
114
+ # @rbs () -> void
115
+ def prevent_update_of_prefixed_encoded_id!
116
+ 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!"
117
+ end
118
+ end
119
+ end
120
+ end
@@ -1,12 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
5
7
  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)
8
+ # Methods provided by other mixins/ActiveRecord
9
+ # @rbs!
10
+ # def decode_encoded_id: (String) -> Array[Integer]?
11
+ # def where: (**untyped) -> untyped
12
+
13
+ # @rbs (*String slugged_encoded_ids) -> untyped
14
+ def where_encoded_id(*slugged_encoded_ids)
15
+ slugged_encoded_ids = slugged_encoded_ids.flatten
16
+
17
+ raise ::ActiveRecord::RecordNotFound if slugged_encoded_ids.empty?
18
+
19
+ decoded_ids = slugged_encoded_ids.flat_map do |slugged_encoded_id|
20
+ decode_encoded_id(slugged_encoded_id).tap do |decoded_id|
21
+ raise ::ActiveRecord::RecordNotFound if decoded_id.nil?
22
+ end
23
+ end
24
+
25
+ where(id: decoded_ids)
10
26
  end
11
27
  end
12
28
  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,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  module EncodedId
4
6
  module Rails
5
7
  class Salt
8
+ # @rbs @klass: Class
9
+ # @rbs @salt: String
10
+
11
+ # @rbs (Class klass, String salt) -> void
6
12
  def initialize(klass, salt)
7
13
  @klass = klass
8
14
  @salt = salt
9
15
  end
10
16
 
17
+ # @rbs return: String
11
18
  def generate!
12
19
  unless @klass.respond_to?(:name) && @klass.name.present?
13
20
  raise ::StandardError, "The class must have a `name` to ensure encode id uniqueness. " \