encoded_id-rails 1.0.0.rc1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -18
- data/LICENSE.txt +1 -1
- data/README.md +76 -479
- data/context/encoded_id-rails.md +433 -0
- data/context/encoded_id.md +283 -0
- data/lib/encoded_id/rails/active_record_finders.rb +52 -0
- data/lib/encoded_id/rails/annotated_id.rb +8 -0
- data/lib/encoded_id/rails/annotated_id_parser.rb +8 -1
- data/lib/encoded_id/rails/coder.rb +20 -2
- data/lib/encoded_id/rails/configuration.rb +44 -4
- data/lib/encoded_id/rails/encoder_methods.rb +9 -1
- data/lib/encoded_id/rails/finder_methods.rb +10 -0
- data/lib/encoded_id/rails/model.rb +25 -2
- data/lib/encoded_id/rails/path_param.rb +7 -0
- data/lib/encoded_id/rails/persists.rb +52 -8
- data/lib/encoded_id/rails/query_methods.rb +20 -4
- data/lib/encoded_id/rails/railtie.rb +13 -0
- data/lib/encoded_id/rails/salt.rb +7 -0
- data/lib/encoded_id/rails/slugged_id.rb +8 -0
- data/lib/encoded_id/rails/slugged_id_parser.rb +8 -1
- data/lib/encoded_id/rails/slugged_path_param.rb +7 -0
- data/lib/encoded_id/rails.rb +9 -6
- data/lib/generators/encoded_id/rails/templates/encoded_id.rb +22 -2
- metadata +13 -23
- data/.devcontainer/Dockerfile +0 -17
- data/.devcontainer/compose.yml +0 -10
- data/.devcontainer/devcontainer.json +0 -12
- data/.standard.yml +0 -3
- data/Appraisals +0 -9
- data/Gemfile +0 -24
- data/Rakefile +0 -20
- data/Steepfile +0 -4
- data/gemfiles/.bundle/config +0 -2
- data/gemfiles/rails_7.2.gemfile +0 -19
- data/gemfiles/rails_8.0.gemfile +0 -19
- data/lib/encoded_id/rails/version.rb +0 -7
- data/rbs_collection.yaml +0 -24
- 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
|
|
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
|
-
|
|
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,14 +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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
11
34
|
|
|
35
|
+
# @rbs () -> void
|
|
12
36
|
def initialize
|
|
13
37
|
@character_group_size = 4
|
|
14
38
|
@group_separator = "-"
|
|
@@ -19,10 +43,21 @@ module EncodedId
|
|
|
19
43
|
@annotation_method_name = :annotation_for_encoded_id
|
|
20
44
|
@annotated_id_separator = "_"
|
|
21
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
|
|
22
56
|
end
|
|
23
57
|
|
|
24
58
|
# Perform validation vs alphabet on these assignments
|
|
25
59
|
|
|
60
|
+
# @rbs (String value) -> String
|
|
26
61
|
def group_separator=(value)
|
|
27
62
|
unless valid_separator?(value, alphabet)
|
|
28
63
|
raise ArgumentError, "Group separator characters must not be part of the alphabet"
|
|
@@ -30,6 +65,7 @@ module EncodedId
|
|
|
30
65
|
@group_separator = value
|
|
31
66
|
end
|
|
32
67
|
|
|
68
|
+
# @rbs (String value) -> String
|
|
33
69
|
def slugged_id_separator=(value)
|
|
34
70
|
if value.blank? || value == group_separator || !valid_separator?(value, alphabet)
|
|
35
71
|
raise ArgumentError, "Slugged ID separator characters must not be part of the alphabet or the same as the group separator"
|
|
@@ -37,6 +73,7 @@ module EncodedId
|
|
|
37
73
|
@slugged_id_separator = value
|
|
38
74
|
end
|
|
39
75
|
|
|
76
|
+
# @rbs (String value) -> String
|
|
40
77
|
def annotated_id_separator=(value)
|
|
41
78
|
if value.blank? || value == group_separator || !valid_separator?(value, alphabet)
|
|
42
79
|
raise ArgumentError, "Annotated ID separator characters must not be part of the alphabet or the same as the group separator"
|
|
@@ -44,6 +81,9 @@ module EncodedId
|
|
|
44
81
|
@annotated_id_separator = value
|
|
45
82
|
end
|
|
46
83
|
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# @rbs (String separator, ::EncodedId::Alphabet characters) -> bool
|
|
47
87
|
def valid_separator?(separator, characters)
|
|
48
88
|
separator.chars.none? { |v| characters.include?(v) }
|
|
49
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,11 +1,28 @@
|
|
|
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)
|
|
@@ -17,8 +34,9 @@ module EncodedId
|
|
|
17
34
|
end
|
|
18
35
|
end
|
|
19
36
|
|
|
20
|
-
attr_accessor :encoded_id_memoized_with_id
|
|
37
|
+
attr_accessor :encoded_id_memoized_with_id #: untyped
|
|
21
38
|
|
|
39
|
+
# @rbs () -> void
|
|
22
40
|
def clear_encoded_id_cache!
|
|
23
41
|
[:@encoded_id_hash, :@encoded_id, :@slugged_encoded_id].each do |var|
|
|
24
42
|
remove_instance_variable(var) if instance_variable_defined?(var)
|
|
@@ -26,10 +44,12 @@ module EncodedId
|
|
|
26
44
|
self.encoded_id_memoized_with_id = nil
|
|
27
45
|
end
|
|
28
46
|
|
|
47
|
+
# @rbs () -> void
|
|
29
48
|
def check_and_clear_memoization
|
|
30
49
|
clear_encoded_id_cache! if encoded_id_memoized_with_id && encoded_id_memoized_with_id != id
|
|
31
50
|
end
|
|
32
51
|
|
|
52
|
+
# @rbs () -> String?
|
|
33
53
|
def encoded_id_hash
|
|
34
54
|
return unless id
|
|
35
55
|
check_and_clear_memoization
|
|
@@ -39,6 +59,7 @@ module EncodedId
|
|
|
39
59
|
@encoded_id_hash = self.class.encode_encoded_id(id)
|
|
40
60
|
end
|
|
41
61
|
|
|
62
|
+
# @rbs () -> String?
|
|
42
63
|
def encoded_id
|
|
43
64
|
return unless id
|
|
44
65
|
check_and_clear_memoization
|
|
@@ -53,6 +74,7 @@ module EncodedId
|
|
|
53
74
|
@encoded_id = EncodedId::Rails::AnnotatedId.new(id_part: encoded, annotation: send(annotated_by.to_s), separator: separator).annotated_id
|
|
54
75
|
end
|
|
55
76
|
|
|
77
|
+
# @rbs () -> String?
|
|
56
78
|
def slugged_encoded_id
|
|
57
79
|
return unless id
|
|
58
80
|
check_and_clear_memoization
|
|
@@ -67,6 +89,7 @@ module EncodedId
|
|
|
67
89
|
@slugged_encoded_id = EncodedId::Rails::SluggedId.new(id_part: encoded, slug_part: send(with.to_s), separator: separator).slugged_id
|
|
68
90
|
end
|
|
69
91
|
|
|
92
|
+
# @rbs (?untyped? options) -> untyped
|
|
70
93
|
def reload(options = nil)
|
|
71
94
|
result = super
|
|
72
95
|
clear_encoded_id_cache!
|
|
@@ -80,7 +103,7 @@ module EncodedId
|
|
|
80
103
|
name.underscore
|
|
81
104
|
end
|
|
82
105
|
|
|
83
|
-
# 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.
|
|
84
107
|
# You either override this method per model, pass an alternate method name to
|
|
85
108
|
# #slugged_encoded_id or setup an alias to another model method in your ApplicationRecord class
|
|
86
109
|
def name_for_encoded_id_slug
|
|
@@ -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
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
module EncodedId
|
|
4
6
|
module Rails
|
|
5
7
|
module Persists
|
|
8
|
+
# @rbs (untyped base) -> void
|
|
6
9
|
def self.included(base)
|
|
7
10
|
base.extend ClassMethods
|
|
8
11
|
|
|
@@ -19,13 +22,38 @@ module EncodedId
|
|
|
19
22
|
end
|
|
20
23
|
|
|
21
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
|
|
22
31
|
def encode_normalized_encoded_id(id)
|
|
23
32
|
encode_encoded_id(id, character_group_size: nil)
|
|
24
33
|
end
|
|
25
34
|
end
|
|
26
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
|
+
|
|
27
54
|
# On duplication we need to reset the encoded ID to nil as this new record will have a new ID.
|
|
28
55
|
# We need to 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,55 @@ module EncodedId
|
|
|
35
63
|
copy
|
|
36
64
|
end
|
|
37
65
|
|
|
38
|
-
|
|
66
|
+
# @rbs () -> Integer
|
|
67
|
+
def resolved_id
|
|
39
68
|
validate_id_for_encoded_id!
|
|
40
69
|
|
|
41
|
-
|
|
70
|
+
id #: Integer
|
|
42
71
|
end
|
|
43
72
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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}"
|
|
54
92
|
end
|
|
55
93
|
|
|
56
|
-
|
|
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}"
|
|
57
97
|
end
|
|
58
98
|
|
|
99
|
+
# @rbs () -> bool
|
|
59
100
|
def should_update_normalized_encoded_id?
|
|
60
101
|
id_changed? || (normalized_encoded_id.blank? && persisted?)
|
|
61
102
|
end
|
|
62
103
|
|
|
104
|
+
# @rbs () -> void
|
|
63
105
|
def validate_id_for_encoded_id!
|
|
64
106
|
raise StandardError, "You cannot set the normalized ID of a record which is not persisted" if id.blank?
|
|
65
107
|
end
|
|
66
108
|
|
|
109
|
+
# @rbs () -> void
|
|
67
110
|
def prevent_update_of_normalized_encoded_id!
|
|
68
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!"
|
|
69
112
|
end
|
|
70
113
|
|
|
114
|
+
# @rbs () -> void
|
|
71
115
|
def prevent_update_of_prefixed_encoded_id!
|
|
72
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!"
|
|
73
117
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
@@ -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. " \
|
|
@@ -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 SluggedId
|
|
10
|
+
# @rbs @slug_part: String
|
|
11
|
+
# @rbs @id_part: String
|
|
12
|
+
# @rbs @separator: String
|
|
13
|
+
|
|
14
|
+
# @rbs (slug_part: String, id_part: String, ?separator: String) -> void
|
|
8
15
|
def initialize(slug_part:, id_part:, separator: "--")
|
|
9
16
|
@slug_part = slug_part
|
|
10
17
|
@id_part = id_part
|
|
11
18
|
@separator = separator
|
|
12
19
|
end
|
|
13
20
|
|
|
21
|
+
# @rbs return: String
|
|
14
22
|
def slugged_id
|
|
15
23
|
unless @id_part.present? && @slug_part.present?
|
|
16
24
|
raise ::StandardError, "The model does not return a valid ID and/or slug"
|
|
@@ -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 SluggedIdParser
|
|
8
|
+
# @rbs @slug: String?
|
|
9
|
+
# @rbs @id: String
|
|
10
|
+
|
|
11
|
+
# @rbs (String slugged_id, ?separator: String) -> void
|
|
6
12
|
def initialize(slugged_id, separator: "--")
|
|
7
13
|
if separator && slugged_id.include?(separator)
|
|
8
14
|
parts = slugged_id.split(separator)
|
|
@@ -13,7 +19,8 @@ module EncodedId
|
|
|
13
19
|
end
|
|
14
20
|
end
|
|
15
21
|
|
|
16
|
-
attr_reader :slug
|
|
22
|
+
attr_reader :slug #: String?
|
|
23
|
+
attr_reader :id #: String
|
|
17
24
|
end
|
|
18
25
|
end
|
|
19
26
|
end
|