activemodel 7.2.2.1 → 8.1.2

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 +25 -45
  3. data/README.rdoc +1 -1
  4. data/lib/active_model/attribute/user_provided_default.rb +10 -0
  5. data/lib/active_model/attribute.rb +9 -1
  6. data/lib/active_model/attribute_assignment.rb +23 -2
  7. data/lib/active_model/attribute_methods.rb +5 -5
  8. data/lib/active_model/attribute_mutation_tracker.rb +5 -5
  9. data/lib/active_model/attribute_set.rb +1 -1
  10. data/lib/active_model/attributes/normalization.rb +192 -0
  11. data/lib/active_model/conversion.rb +1 -1
  12. data/lib/active_model/dirty.rb +16 -9
  13. data/lib/active_model/error.rb +3 -2
  14. data/lib/active_model/errors.rb +1 -4
  15. data/lib/active_model/gem_version.rb +3 -3
  16. data/lib/active_model/lint.rb +7 -3
  17. data/lib/active_model/model.rb +2 -2
  18. data/lib/active_model/naming.rb +0 -1
  19. data/lib/active_model/nested_error.rb +1 -3
  20. data/lib/active_model/railtie.rb +8 -4
  21. data/lib/active_model/secure_password.rb +61 -4
  22. data/lib/active_model/serialization.rb +21 -21
  23. data/lib/active_model/translation.rb +22 -5
  24. data/lib/active_model/type/big_integer.rb +21 -0
  25. data/lib/active_model/type/boolean.rb +1 -0
  26. data/lib/active_model/type/date.rb +1 -0
  27. data/lib/active_model/type/date_time.rb +8 -0
  28. data/lib/active_model/type/decimal.rb +1 -0
  29. data/lib/active_model/type/float.rb +9 -8
  30. data/lib/active_model/type/helpers/immutable.rb +13 -0
  31. data/lib/active_model/type/helpers/time_value.rb +20 -44
  32. data/lib/active_model/type/helpers.rb +1 -0
  33. data/lib/active_model/type/immutable_string.rb +2 -0
  34. data/lib/active_model/type/integer.rb +31 -19
  35. data/lib/active_model/type/string.rb +4 -0
  36. data/lib/active_model/type/value.rb +4 -4
  37. data/lib/active_model/validations/acceptance.rb +1 -1
  38. data/lib/active_model/validations/callbacks.rb +10 -0
  39. data/lib/active_model/validations/validates.rb +7 -2
  40. data/lib/active_model/validations.rb +57 -32
  41. data/lib/active_model.rb +6 -0
  42. metadata +11 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7abaeb86e5e7dc3786831b348d5f8deb514f1e3f2b70de2c2c7e7cd79a4c1511
4
- data.tar.gz: ecb06547a6de150912b0c81b5296cd68258958f342c9029fb7633a879710e122
3
+ metadata.gz: 4f299734cfee7396d03d330f92d0be6f335017395937c52f6e6a88ebc894c1b8
4
+ data.tar.gz: d9ed23ff71d39c9ad9dc0df555413ed5440baef7240d1dc12d6ddc35cdc1c76c
5
5
  SHA512:
6
- metadata.gz: e2dbf2434ce2471c32f69df40b9ecbec73df015c1100db563bd74ceca953c6feebd94c674ab2fdef7777580f79b9aea1525d976a3ff40aabbc7efa4148b1ac5f
7
- data.tar.gz: 0c3605f04d15a6d7b7d97546183d8620b80f38d5d15c5b8e452d862721e9776d5249a3c6cd7f3b0c8b3269800db39bff939d93af92990b9680391afa5ae3e3e4
6
+ metadata.gz: 9586704ac01c836de27bfc1fdc50deee46f921af37590022fba6c13af2c703c7d4973cb0d50c16b45687afe1bb36eb7c6ea5fb6c04992b93a42dcc3f8f80637d
7
+ data.tar.gz: 428ad04107455dbb8e62a341192f8f4a093a4d17afca052623d25305ae7e109b21fa0dab8f2aa7581dc9ec1ebfe58bfad1e0008387df1b2cc044f95772895297
data/CHANGELOG.md CHANGED
@@ -1,66 +1,46 @@
1
- ## Rails 7.2.2.1 (December 10, 2024) ##
1
+ ## Rails 8.1.2 (January 08, 2026) ##
2
2
 
3
3
  * No changes.
4
4
 
5
5
 
6
- ## Rails 7.2.2 (October 30, 2024) ##
7
-
8
- * Fix regression in `alias_attribute` to work with user defined methods.
9
-
10
- `alias_attribute` would wrongly assume the attribute accessor was generated by Active Model.
11
-
12
- ```ruby
13
- class Person
14
- include ActiveModel::AttributeMethods
15
-
16
- define_attribute_methods :name
17
- attr_accessor :name
18
-
19
- alias_attribute :full_name, :name
20
- end
21
-
22
- person.full_name # => NoMethodError: undefined method `attribute' for an instance of Person
23
- ```
24
-
25
- *Jean Boussier*
26
-
27
-
28
- ## Rails 7.2.1.2 (October 23, 2024) ##
6
+ ## Rails 8.1.1 (October 28, 2025) ##
29
7
 
30
8
  * No changes.
31
9
 
32
10
 
33
- ## Rails 7.2.1.1 (October 15, 2024) ##
11
+ ## Rails 8.1.0 (October 22, 2025) ##
34
12
 
35
- * No changes.
13
+ * Add `reset_token: { expires_in: ... }` option to `has_secure_password`.
36
14
 
15
+ Allows configuring the expiry duration of password reset tokens (default remains 15 minutes for backwards compatibility).
37
16
 
38
- ## Rails 7.2.1 (August 22, 2024) ##
39
-
40
- * No changes.
17
+ ```ruby
18
+ has_secure_password reset_token: { expires_in: 1.hour }
19
+ ```
41
20
 
21
+ *Jevin Sew*, *Abeid Ahmed*
42
22
 
43
- ## Rails 7.2.0 (August 09, 2024) ##
23
+ * Add `except_on:` option for validation callbacks.
44
24
 
45
- * Fix a bug where type casting of string to `Time` and `DateTime` doesn't
46
- calculate minus minute value in TZ offset correctly.
25
+ *Ben Sheldon*
47
26
 
48
- *Akira Matsuda*
27
+ * Backport `ActiveRecord::Normalization` to `ActiveModel::Attributes::Normalization`
49
28
 
50
- * Port the `type_for_attribute` method to Active Model. Classes that include
51
- `ActiveModel::Attributes` will now provide this method. This method behaves
52
- the same for Active Model as it does for Active Record.
29
+ ```ruby
30
+ class User
31
+ include ActiveModel::Attributes
32
+ include ActiveModel::Attributes::Normalization
53
33
 
54
- ```ruby
55
- class MyModel
56
- include ActiveModel::Attributes
34
+ attribute :email, :string
57
35
 
58
- attribute :my_attribute, :integer
59
- end
36
+ normalizes :email, with: -> email { email.strip.downcase }
37
+ end
60
38
 
61
- MyModel.type_for_attribute(:my_attribute) # => #<ActiveModel::Type::Integer ...>
62
- ```
39
+ user = User.new
40
+ user.email = " CRUISE-CONTROL@EXAMPLE.COM\n"
41
+ user.email # => "cruise-control@example.com"
42
+ ```
63
43
 
64
- *Jonathan Hefner*
44
+ *Sean Doyle*
65
45
 
66
- Please check [7-1-stable](https://github.com/rails/rails/blob/7-1-stable/activemodel/CHANGELOG.md) for previous changes.
46
+ Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/activemodel/CHANGELOG.md) for previous changes.
data/README.rdoc CHANGED
@@ -261,6 +261,6 @@ Bug reports for the Ruby on \Rails project can be filed here:
261
261
 
262
262
  * https://github.com/rails/rails/issues
263
263
 
264
- Feature requests should be discussed on the rails-core mailing list here:
264
+ Feature requests should be discussed on the rubyonrails-core forum here:
265
265
 
266
266
  * https://discuss.rubyonrails.org/c/rubyonrails-core
@@ -26,6 +26,16 @@ module ActiveModel
26
26
  self.class.new(name, user_provided_value, type, original_attribute)
27
27
  end
28
28
 
29
+ def dup_or_share # :nodoc:
30
+ # Can't elide dup when the default is a Proc
31
+ # See Attribute#dup_or_share
32
+ if @user_provided_value.is_a?(Proc)
33
+ dup
34
+ else
35
+ super
36
+ end
37
+ end
38
+
29
39
  def marshal_dump
30
40
  result = [
31
41
  name,
@@ -38,7 +38,7 @@ module ActiveModel
38
38
  @value = value unless value.nil?
39
39
  end
40
40
 
41
- def value
41
+ def value(&)
42
42
  # `defined?` is cheaper than `||=` when we get back falsy values
43
43
  @value = type_cast(value_before_type_cast) unless defined?(@value)
44
44
  @value
@@ -96,6 +96,14 @@ module ActiveModel
96
96
  end
97
97
  end
98
98
 
99
+ def dup_or_share # :nodoc:
100
+ if @type.mutable?
101
+ dup
102
+ else
103
+ self # If the underlying type is immutable we can get away with not duping
104
+ end
105
+ end
106
+
99
107
  def type_cast(*)
100
108
  raise NotImplementedError
101
109
  end
@@ -36,9 +36,30 @@ module ActiveModel
36
36
 
37
37
  alias attributes= assign_attributes
38
38
 
39
+ # Like `BasicObject#method_missing`, `#attribute_writer_missing` is invoked
40
+ # when `#assign_attributes` is passed an unknown attribute name.
41
+ #
42
+ # By default, `#attribute_writer_missing` raises an UnknownAttributeError.
43
+ #
44
+ # class Rectangle
45
+ # include ActiveModel::AttributeAssignment
46
+ #
47
+ # attr_accessor :length, :width
48
+ #
49
+ # def attribute_writer_missing(name, value)
50
+ # Rails.logger.warn "Tried to assign to unknown attribute #{name}"
51
+ # end
52
+ # end
53
+ #
54
+ # rectangle = Rectangle.new
55
+ # rectangle.assign_attributes(height: 10) # => Logs "Tried to assign to unknown attribute 'height'"
56
+ def attribute_writer_missing(name, value)
57
+ raise UnknownAttributeError.new(self, name)
58
+ end
59
+
39
60
  private
40
61
  def _assign_attributes(attributes)
41
- attributes.each do |k, v|
62
+ attributes.each_pair do |k, v|
42
63
  _assign_attribute(k, v)
43
64
  end
44
65
  end
@@ -50,7 +71,7 @@ module ActiveModel
50
71
  if respond_to?(setter)
51
72
  raise
52
73
  else
53
- raise UnknownAttributeError.new(self, k.to_s)
74
+ attribute_writer_missing(k.to_s, v)
54
75
  end
55
76
  end
56
77
  end
@@ -214,7 +214,7 @@ module ActiveModel
214
214
  end
215
215
  end
216
216
 
217
- def generate_alias_attribute_methods(code_generator, new_name, old_name)
217
+ def generate_alias_attribute_methods(code_generator, new_name, old_name) # :nodoc:
218
218
  ActiveSupport::CodeGenerator.batch(code_generator, __FILE__, __LINE__) do |owner|
219
219
  attribute_method_patterns.each do |pattern|
220
220
  alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
@@ -321,8 +321,8 @@ module ActiveModel
321
321
  canonical_method_name = pattern.method_name(attr_name)
322
322
  public_method_name = pattern.method_name(as)
323
323
 
324
- # If defining a regular attribute method, we don't override methods that are explictly
325
- # defined in parrent classes.
324
+ # If defining a regular attribute method, we don't override methods that are explicitly
325
+ # defined in parent classes.
326
326
  if instance_method_already_implemented?(public_method_name)
327
327
  # However, for `alias_attribute`, we always define the method.
328
328
  # We check for override second because `instance_method_already_implemented?`
@@ -373,7 +373,7 @@ module ActiveModel
373
373
  # person.name_short? # => NoMethodError
374
374
  # person.first_name # => NoMethodError
375
375
  def undefine_attribute_methods
376
- generated_attribute_methods.module_eval do
376
+ @generated_attribute_methods&.module_eval do
377
377
  undef_method(*instance_methods)
378
378
  end
379
379
  attribute_method_patterns_cache.clear
@@ -402,7 +402,7 @@ module ActiveModel
402
402
  end
403
403
 
404
404
  def instance_method_already_implemented?(method_name)
405
- generated_attribute_methods.method_defined?(method_name)
405
+ @generated_attribute_methods&.method_defined?(method_name)
406
406
  end
407
407
 
408
408
  # The methods +method_missing+ and +respond_to?+ of this module are
@@ -5,7 +5,7 @@ require "active_support/core_ext/object/duplicable"
5
5
 
6
6
  module ActiveModel
7
7
  class AttributeMutationTracker # :nodoc:
8
- OPTION_NOT_GIVEN = Object.new
8
+ OPTION_NOT_GIVEN = Object.new.freeze
9
9
 
10
10
  def initialize(attributes)
11
11
  @attributes = attributes
@@ -16,17 +16,17 @@ module ActiveModel
16
16
  end
17
17
 
18
18
  def changed_values
19
- attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
19
+ attr_names.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |attr_name, result|
20
20
  if changed?(attr_name)
21
- result[attr_name] = original_value(attr_name)
21
+ result.store(attr_name, original_value(attr_name), convert_value: false)
22
22
  end
23
23
  end
24
24
  end
25
25
 
26
26
  def changes
27
- attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
27
+ attr_names.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |attr_name, result|
28
28
  if change = change_to_attribute(attr_name)
29
- result.merge!(attr_name => change)
29
+ result.store(attr_name, change, convert_value: false)
30
30
  end
31
31
  end
32
32
  end
@@ -71,7 +71,7 @@ module ActiveModel
71
71
  end
72
72
 
73
73
  def deep_dup
74
- AttributeSet.new(attributes.deep_dup)
74
+ AttributeSet.new(attributes.transform_values(&:dup_or_share))
75
75
  end
76
76
 
77
77
  def initialize_dup(_)
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Attributes
5
+ module Normalization
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include ActiveModel::Dirty
10
+ include ActiveModel::Validations::Callbacks
11
+
12
+ class_attribute :normalized_attributes, default: Set.new
13
+
14
+ before_validation :normalize_changed_in_place_attributes
15
+ end
16
+
17
+ # Normalizes a specified attribute using its declared normalizations.
18
+ #
19
+ # ==== Examples
20
+ #
21
+ # class User
22
+ # include ActiveModel::Attributes
23
+ # include ActiveModel::Attributes::Normalization
24
+ #
25
+ # attribute :email, :string
26
+ #
27
+ # normalizes :email, with: -> email { email.strip.downcase }
28
+ # end
29
+ #
30
+ # legacy_user = User.load_from_legacy_data(...)
31
+ # legacy_user.email # => " CRUISE-CONTROL@EXAMPLE.COM\n"
32
+ # legacy_user.normalize_attribute(:email)
33
+ # legacy_user.email # => "cruise-control@example.com"
34
+ #
35
+ # ==== Behavior with Active Record
36
+ #
37
+ # To prevent confusion, normalization will not be applied
38
+ # when the attribute is fetched from the database. This means that if a
39
+ # record was persisted before the normalization was declared, the record's
40
+ # attribute will not be normalized until either it is assigned a new
41
+ # value, or it is explicitly migrated via Normalization#normalize_attribute.
42
+ #
43
+ # Be aware that if your app was created before Rails 7.1, and your app
44
+ # marshals instances of the targeted model (for example, when caching),
45
+ # then you should set ActiveRecord.marshalling_format_version to +7.1+ or
46
+ # higher via either <tt>config.load_defaults 7.1</tt> or
47
+ # <tt>config.active_record.marshalling_format_version = 7.1</tt>.
48
+ # Otherwise, +Marshal+ may attempt to serialize the normalization +Proc+
49
+ # and raise +TypeError+.
50
+ #
51
+ # class User < ActiveRecord::Base
52
+ # normalizes :email, with: -> email { email.strip.downcase }
53
+ # normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
54
+ # end
55
+ #
56
+ # user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
57
+ # user.email # => "cruise-control@example.com"
58
+ #
59
+ # user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")
60
+ # user.email # => "cruise-control@example.com"
61
+ # user.email_before_type_cast # => "cruise-control@example.com"
62
+ #
63
+ # User.where(email: "\tCRUISE-CONTROL@EXAMPLE.COM ").count # => 1
64
+ # User.where(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]).count # => 0
65
+ #
66
+ # User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") # => true
67
+ # User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false
68
+ #
69
+ # User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
70
+ def normalize_attribute(name)
71
+ # Treat the value as a new, unnormalized value.
72
+ send(:"#{name}=", send(name))
73
+ end
74
+
75
+ module ClassMethods
76
+ # Declares a normalization for one or more attributes. The normalization
77
+ # is applied when the attribute is assigned or validated.
78
+ #
79
+ # Because the normalization may be applied multiple times, it should be
80
+ # _idempotent_. In other words, applying the normalization more than once
81
+ # should have the same result as applying it only once.
82
+ #
83
+ # By default, the normalization will not be applied to +nil+ values. This
84
+ # behavior can be changed with the +:apply_to_nil+ option.
85
+ #
86
+ # ==== Options
87
+ #
88
+ # * +:with+ - Any callable object that accepts the attribute's value as
89
+ # its sole argument, and returns it normalized.
90
+ # * +:apply_to_nil+ - Whether to apply the normalization to +nil+ values.
91
+ # Defaults to +false+.
92
+ #
93
+ # ==== Examples
94
+ #
95
+ # class User
96
+ # include ActiveModel::Attributes
97
+ # include ActiveModel::Attributes::Normalization
98
+ #
99
+ # attribute :email, :string
100
+ # attribute :phone, :string
101
+ #
102
+ # normalizes :email, with: -> email { email.strip.downcase }
103
+ # normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
104
+ # end
105
+ #
106
+ # user = User.new
107
+ # user.email = " CRUISE-CONTROL@EXAMPLE.COM\n"
108
+ # user.email # => "cruise-control@example.com"
109
+ #
110
+ # User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
111
+ def normalizes(*names, with:, apply_to_nil: false)
112
+ decorate_attributes(names) do |name, cast_type|
113
+ NormalizedValueType.new(cast_type: cast_type, normalizer: with, normalize_nil: apply_to_nil)
114
+ end
115
+
116
+ self.normalized_attributes += names.map(&:to_sym)
117
+ end
118
+
119
+ # Normalizes a given +value+ using normalizations declared for +name+.
120
+ #
121
+ # ==== Examples
122
+ #
123
+ # class User
124
+ # include ActiveModel::Attributes
125
+ # include ActiveModel::Attributes::Normalization
126
+ #
127
+ # attribute :email, :string
128
+ #
129
+ # normalizes :email, with: -> email { email.strip.downcase }
130
+ # end
131
+ #
132
+ # User.normalize_value_for(:email, " CRUISE-CONTROL@EXAMPLE.COM\n")
133
+ # # => "cruise-control@example.com"
134
+ def normalize_value_for(name, value)
135
+ type_for_attribute(name).cast(value)
136
+ end
137
+ end
138
+
139
+ private
140
+ def normalize_changed_in_place_attributes
141
+ self.class.normalized_attributes.each do |name|
142
+ normalize_attribute(name) if attribute_changed_in_place?(name)
143
+ end
144
+ end
145
+
146
+ class NormalizedValueType < DelegateClass(ActiveModel::Type::Value) # :nodoc:
147
+ include ActiveModel::Type::SerializeCastValue
148
+
149
+ attr_reader :cast_type, :normalizer, :normalize_nil
150
+ alias :normalize_nil? :normalize_nil
151
+
152
+ def initialize(cast_type:, normalizer:, normalize_nil:)
153
+ @cast_type = cast_type
154
+ @normalizer = normalizer
155
+ @normalize_nil = normalize_nil
156
+ super(cast_type)
157
+ end
158
+
159
+ def cast(value)
160
+ normalize(super(value))
161
+ end
162
+
163
+ def serialize(value)
164
+ serialize_cast_value(cast(value))
165
+ end
166
+
167
+ def serialize_cast_value(value)
168
+ ActiveModel::Type::SerializeCastValue.serialize(cast_type, value)
169
+ end
170
+
171
+ def ==(other)
172
+ self.class == other.class &&
173
+ normalize_nil? == other.normalize_nil? &&
174
+ normalizer == other.normalizer &&
175
+ cast_type == other.cast_type
176
+ end
177
+ alias eql? ==
178
+
179
+ def hash
180
+ [self.class, cast_type, normalizer, normalize_nil?].hash
181
+ end
182
+
183
+ define_method(:inspect, Kernel.instance_method(:inspect))
184
+
185
+ private
186
+ def normalize(value)
187
+ normalizer.call(value) unless value.nil? && !normalize_nil?
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -3,7 +3,7 @@
3
3
  module ActiveModel
4
4
  # = Active \Model \Conversion
5
5
  #
6
- # Handles default conversions: to_model, to_key, to_param, and to_partial_path.
6
+ # Handles default conversions: #to_model, #to_key, #to_param, and #to_partial_path.
7
7
  #
8
8
  # Let's take for example this non-persisted object.
9
9
  #
@@ -108,7 +108,7 @@ module ActiveModel
108
108
  # person.changes # => {"name" => ["Bill", "Bob"]}
109
109
  #
110
110
  # If an attribute is modified in-place then make use of
111
- # {*_will_change!}[rdoc-label:method-i-2A_will_change-21] to mark that the attribute is changing.
111
+ # {*_will_change!}[rdoc-ref:#*_will_change!] to mark that the attribute is changing.
112
112
  # Otherwise \Active \Model can't track changes to in-place attributes. Note
113
113
  # that Active Record can detect in-place modifications automatically. You do
114
114
  # not need to call <tt>*_will_change!</tt> on Active Record models.
@@ -247,16 +247,23 @@ module ActiveModel
247
247
 
248
248
  def initialize_dup(other) # :nodoc:
249
249
  super
250
+ @mutations_from_database = nil
251
+ end
252
+
253
+ def init_attributes(other) # :nodoc:
254
+ attrs = super
250
255
  if self.class.respond_to?(:_default_attributes)
251
- @attributes = self.class._default_attributes.map do |attr|
252
- attr.with_value_from_user(@attributes.fetch_value(attr.name))
256
+ self.class._default_attributes.map do |attr|
257
+ attr.with_value_from_user(attrs.fetch_value(attr.name))
253
258
  end
259
+ else
260
+ attrs
254
261
  end
255
- @mutations_from_database = nil
256
262
  end
257
263
 
258
264
  def as_json(options = {}) # :nodoc:
259
- options[:except] = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
265
+ except = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
266
+ options = options.merge except: except
260
267
  super(options)
261
268
  end
262
269
 
@@ -289,22 +296,22 @@ module ActiveModel
289
296
  mutations_from_database.changed_attribute_names
290
297
  end
291
298
 
292
- # Dispatch target for {*_changed?}[rdoc-label:method-i-2A_changed-3F] attribute methods.
299
+ # Dispatch target for {*_changed?}[rdoc-ref:#*_changed?] attribute methods.
293
300
  def attribute_changed?(attr_name, **options)
294
301
  mutations_from_database.changed?(attr_name.to_s, **options)
295
302
  end
296
303
 
297
- # Dispatch target for {*_was}[rdoc-label:method-i-2A_was] attribute methods.
304
+ # Dispatch target for {*_was}[rdoc-ref:#*_was] attribute methods.
298
305
  def attribute_was(attr_name)
299
306
  mutations_from_database.original_value(attr_name.to_s)
300
307
  end
301
308
 
302
- # Dispatch target for {*_previously_changed?}[rdoc-label:method-i-2A_previously_changed-3F] attribute methods.
309
+ # Dispatch target for {*_previously_changed?}[rdoc-ref:#*_previously_changed?] attribute methods.
303
310
  def attribute_previously_changed?(attr_name, **options)
304
311
  mutations_before_last_save.changed?(attr_name.to_s, **options)
305
312
  end
306
313
 
307
- # Dispatch target for {*_previously_was}[rdoc-label:method-i-2A_previously_was] attribute methods.
314
+ # Dispatch target for {*_previously_was}[rdoc-ref:#*_previously_was] attribute methods.
308
315
  def attribute_previously_was(attr_name)
309
316
  mutations_before_last_save.original_value(attr_name.to_s)
310
317
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/class/attribute"
4
3
 
5
4
  module ActiveModel
6
5
  # = Active \Model \Error
@@ -83,7 +82,7 @@ module ActiveModel
83
82
  defaults << :"#{i18n_scope}.errors.messages.#{type}"
84
83
 
85
84
  catch(:exception) do
86
- translation = I18n.translate(defaults.first, **options.merge(default: defaults.drop(1), throw: true))
85
+ translation = I18n.translate(defaults.first, **options, default: defaults.drop(1), throw: true)
87
86
  return translation unless translation.nil?
88
87
  end unless options[:message]
89
88
  else
@@ -205,4 +204,6 @@ module ActiveModel
205
204
  [@base, @attribute, @raw_type, @options.except(*CALLBACKS_OPTIONS)]
206
205
  end
207
206
  end
207
+
208
+ ActiveSupport.run_load_hooks(:active_model_error, Error)
208
209
  end
@@ -5,7 +5,6 @@ require "active_support/core_ext/string/inflections"
5
5
  require "active_support/core_ext/object/deep_dup"
6
6
  require "active_model/error"
7
7
  require "active_model/nested_error"
8
- require "forwardable"
9
8
 
10
9
  module ActiveModel
11
10
  # = Active \Model \Errors
@@ -61,8 +60,6 @@ module ActiveModel
61
60
  class Errors
62
61
  include Enumerable
63
62
 
64
- extend Forwardable
65
-
66
63
  ##
67
64
  # :method: each
68
65
  #
@@ -100,7 +97,7 @@ module ActiveModel
100
97
  #
101
98
  # Returns number of errors.
102
99
 
103
- def_delegators :@errors, :each, :clear, :empty?, :size, :uniq!
100
+ delegate :each, :clear, :empty?, :size, :uniq!, to: :@errors
104
101
 
105
102
  # The actual array of +Error+ objects
106
103
  # This method is aliased to <tt>objects</tt>.
@@ -7,10 +7,10 @@ module ActiveModel
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 7
11
- MINOR = 2
10
+ MAJOR = 8
11
+ MINOR = 1
12
12
  TINY = 2
13
- PRE = "1"
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -30,7 +30,7 @@ module ActiveModel
30
30
  # of the model, and is used to a generate unique DOM id for the object.
31
31
  def test_to_key
32
32
  assert_respond_to model, :to_key
33
- def model.persisted?() false end
33
+ def_method(model, :persisted?) { false }
34
34
  assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
35
35
  end
36
36
 
@@ -45,8 +45,8 @@ module ActiveModel
45
45
  # any of the possible implementation strategies on the implementer.
46
46
  def test_to_param
47
47
  assert_respond_to model, :to_param
48
- def model.to_key() [1] end
49
- def model.persisted?() false end
48
+ def_method(model, :to_key) { [1] }
49
+ def_method(model, :persisted?) { false }
50
50
  assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
51
51
  end
52
52
 
@@ -105,6 +105,10 @@ module ActiveModel
105
105
  end
106
106
 
107
107
  private
108
+ def def_method(receiver, name, &block)
109
+ ::Object.instance_method(:define_singleton_method).bind_call(receiver, name, &block)
110
+ end
111
+
108
112
  def model
109
113
  assert_respond_to @model, :to_model
110
114
  @model.to_model
@@ -54,7 +54,7 @@ module ActiveModel
54
54
  #
55
55
  # person = Person.new(id: 1, name: "bob")
56
56
  # person.slice(:id, :name)
57
- # => { "id" => 1, "name" => "bob" }
57
+ # # => { "id" => 1, "name" => "bob" }
58
58
  #
59
59
  #--
60
60
  # Implemented by ActiveModel::Access#slice.
@@ -68,7 +68,7 @@ module ActiveModel
68
68
  #
69
69
  # person = Person.new(id: 1, name: "bob")
70
70
  # person.values_at(:id, :name)
71
- # => [1, "bob"]
71
+ # # => [1, "bob"]
72
72
  #
73
73
  #--
74
74
  # Implemented by ActiveModel::Access#values_at.
@@ -3,7 +3,6 @@
3
3
  require "active_support/core_ext/hash/except"
4
4
  require "active_support/core_ext/module/introspection"
5
5
  require "active_support/core_ext/module/redefine_method"
6
- require "active_support/core_ext/module/delegation"
7
6
 
8
7
  module ActiveModel
9
8
  class Name