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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +97 -18
- data/LICENSE.txt +1 -1
- data/README.md +81 -473
- data/context/encoded_id-rails.md +651 -0
- data/context/encoded_id.md +437 -0
- data/lib/encoded_id/rails/active_record_finders.rb +54 -0
- data/lib/encoded_id/rails/annotated_id.rb +14 -9
- data/lib/encoded_id/rails/annotated_id_parser.rb +9 -1
- data/lib/encoded_id/rails/coder.rb +55 -10
- data/lib/encoded_id/rails/composite_id_base.rb +39 -0
- data/lib/encoded_id/rails/configuration.rb +66 -10
- data/lib/encoded_id/rails/encoder_methods.rb +30 -7
- data/lib/encoded_id/rails/finder_methods.rb +11 -0
- data/lib/encoded_id/rails/model.rb +60 -7
- data/lib/encoded_id/rails/path_param.rb +8 -0
- data/lib/encoded_id/rails/persists.rb +55 -9
- data/lib/encoded_id/rails/query_methods.rb +21 -4
- data/lib/encoded_id/rails/railtie.rb +13 -0
- data/lib/encoded_id/rails/salt.rb +8 -0
- data/lib/encoded_id/rails/slugged_id.rb +14 -9
- data/lib/encoded_id/rails/slugged_id_parser.rb +9 -1
- data/lib/encoded_id/rails/slugged_path_param.rb +8 -0
- data/lib/encoded_id/rails.rb +11 -6
- data/lib/generators/encoded_id/rails/install_generator.rb +36 -2
- data/lib/generators/encoded_id/rails/templates/{encoded_id.rb → hashids_encoded_id.rb} +49 -5
- data/lib/generators/encoded_id/rails/templates/sqids_encoded_id.rb +116 -0
- metadata +16 -24
- 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
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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, "
|
|
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? {
|
|
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
|
-
|
|
15
|
-
|
|
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:
|
|
30
|
-
id_length:
|
|
31
|
-
character_group_size:
|
|
32
|
-
alphabet:
|
|
33
|
-
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
@@ -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
|
-
|
|
3
|
+
# rbs_inline: enabled
|
|
4
4
|
|
|
5
5
|
module EncodedId
|
|
6
6
|
module Rails
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|