activemodel 8.0.2 → 8.1.0.beta1

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -107
  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_methods.rb +2 -2
  7. data/lib/active_model/attribute_mutation_tracker.rb +5 -5
  8. data/lib/active_model/attribute_set.rb +1 -1
  9. data/lib/active_model/attributes/normalization.rb +192 -0
  10. data/lib/active_model/dirty.rb +6 -6
  11. data/lib/active_model/error.rb +3 -2
  12. data/lib/active_model/errors.rb +1 -4
  13. data/lib/active_model/gem_version.rb +3 -3
  14. data/lib/active_model/lint.rb +7 -3
  15. data/lib/active_model/model.rb +2 -2
  16. data/lib/active_model/naming.rb +0 -1
  17. data/lib/active_model/nested_error.rb +1 -3
  18. data/lib/active_model/railtie.rb +7 -3
  19. data/lib/active_model/secure_password.rb +3 -1
  20. data/lib/active_model/type/big_integer.rb +21 -0
  21. data/lib/active_model/type/boolean.rb +1 -0
  22. data/lib/active_model/type/date.rb +1 -0
  23. data/lib/active_model/type/date_time.rb +8 -0
  24. data/lib/active_model/type/decimal.rb +1 -0
  25. data/lib/active_model/type/float.rb +1 -0
  26. data/lib/active_model/type/helpers/immutable.rb +13 -0
  27. data/lib/active_model/type/helpers.rb +1 -0
  28. data/lib/active_model/type/immutable_string.rb +2 -0
  29. data/lib/active_model/type/integer.rb +31 -19
  30. data/lib/active_model/type/string.rb +4 -0
  31. data/lib/active_model/type/value.rb +2 -2
  32. data/lib/active_model/validations/callbacks.rb +10 -0
  33. data/lib/active_model/validations.rb +4 -16
  34. data/lib/active_model.rb +6 -0
  35. metadata +10 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f47ebb694713a685204514dc5d04de997ca6949db638f45b354338698d650987
4
- data.tar.gz: 4183f64d69034c0cf755de5055ebae00308bc6b6c6d42c314f97356a9b906615
3
+ metadata.gz: d0477968c49c9249ac55371d34c8035ada03438da3b79f12d53f29179913018a
4
+ data.tar.gz: e9481d73df257802b3b69b46837b9806417f2244ecb672c397a2dc9944528799
5
5
  SHA512:
6
- metadata.gz: bfcb46dc147048dc714eadaa2b3389fac0b17294db99fd5aa7b29f37c7c3fdcc6cf5c3fdf472379bd00fc52a2c99bd9d6fe83d8abb5967e0f50a656959bd325d
7
- data.tar.gz: 99e6c0227a8e467a578e9c9e8e3c59cd4854550ebb549d57c3dea0bf517f2a87200f5d0c426fb076fb7946d83cf8a8b233662792e5711388305b204c8cd5013e
6
+ metadata.gz: 652d349baa6d7d2354878e7e70300036f6529ca8a0e5ef700eca8ed58e936b3d2fa18f293dc69aba2ba3ed7c54bec30631b23070969f7e9e57c18e663f289e15
7
+ data.tar.gz: c188746810b0ce0b3645cbc32d288c16864eed5c1ce1c1569a5f6d0aa93b4d761e68bfacb77956be90a3de7449f60d6467813c74e3115a5baddd618190013ea9
data/CHANGELOG.md CHANGED
@@ -1,120 +1,26 @@
1
- ## Rails 8.0.2 (March 12, 2025) ##
1
+ ## Rails 8.1.0.beta1 (September 04, 2025) ##
2
2
 
3
- * No changes.
3
+ * Add `except_on:` option for validation callbacks.
4
4
 
5
+ *Ben Sheldon*
5
6
 
6
- ## Rails 8.0.2 (March 12, 2025) ##
7
-
8
- * No changes.
9
-
10
-
11
- ## Rails 8.0.1 (December 13, 2024) ##
12
-
13
- * No changes.
14
-
15
-
16
- ## Rails 8.0.0.1 (December 10, 2024) ##
17
-
18
- * No changes.
19
-
20
-
21
- ## Rails 8.0.0 (November 07, 2024) ##
22
-
23
- * No changes.
24
-
25
-
26
- ## Rails 8.0.0.rc2 (October 30, 2024) ##
27
-
28
- * No changes.
29
-
30
-
31
- ## Rails 8.0.0.rc1 (October 19, 2024) ##
32
-
33
- * Add `:except_on` option for validations. Grants the ability to _skip_ validations in specified contexts.
34
-
35
- ```ruby
36
- class User < ApplicationRecord
37
- #...
38
- validates :birthday, presence: { except_on: :admin }
39
- #...
40
- end
41
-
42
- user = User.new(attributes except birthday)
43
- user.save(context: :admin)
44
- ```
45
-
46
- *Drew Bragg*
47
-
48
- ## Rails 8.0.0.beta1 (September 26, 2024) ##
49
-
50
- * Make `ActiveModel::Serialization#read_attribute_for_serialization` public
51
-
52
- *Sean Doyle*
53
-
54
- * Add a default token generator for password reset tokens when using `has_secure_password`.
55
-
56
- ```ruby
57
- class User < ApplicationRecord
58
- has_secure_password
59
- end
60
-
61
- user = User.create!(name: "david", password: "123", password_confirmation: "123")
62
- token = user.password_reset_token
63
- User.find_by_password_reset_token(token) # returns user
64
-
65
- # 16 minutes later...
66
- User.find_by_password_reset_token(token) # returns nil
67
-
68
- # raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
69
- User.find_by_password_reset_token!(token)
70
- ```
71
-
72
- *DHH*
73
-
74
- * Add a load hook `active_model_translation` for `ActiveModel::Translation`.
75
-
76
- *Shouichi Kamiya*
77
-
78
- * Add `raise_on_missing_translations` option to `ActiveModel::Translation`.
79
- When the option is set, `human_attribute_name` raises an error if a translation of the given attribute is missing.
80
-
81
- ```ruby
82
- # ActiveModel::Translation.raise_on_missing_translations = false
83
- Post.human_attribute_name("title")
84
- => "Title"
85
-
86
- # ActiveModel::Translation.raise_on_missing_translations = true
87
- Post.human_attribute_name("title")
88
- => Translation missing. Options considered were: (I18n::MissingTranslationData)
89
- - en.activerecord.attributes.post.title
90
- - en.attributes.title
91
-
92
- raise exception.respond_to?(:to_exception) ? exception.to_exception : exception
93
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
94
- ```
95
-
96
- *Shouichi Kamiya*
97
-
98
- * Introduce `ActiveModel::AttributeAssignment#attribute_writer_missing`
99
-
100
- Provide instances with an opportunity to gracefully handle assigning to an
101
- unknown attribute:
7
+ * Backport `ActiveRecord::Normalization` to `ActiveModel::Attributes::Normalization`
102
8
 
103
9
  ```ruby
104
- class Rectangle
105
- include ActiveModel::AttributeAssignment
10
+ class User
11
+ include ActiveModel::Attributes
12
+ include ActiveModel::Attributes::Normalization
106
13
 
107
- attr_accessor :length, :width
14
+ attribute :email, :string
108
15
 
109
- def attribute_writer_missing(name, value)
110
- Rails.logger.warn "Tried to assign to unknown attribute #{name}"
111
- end
16
+ normalizes :email, with: -> email { email.strip.downcase }
112
17
  end
113
18
 
114
- rectangle = Rectangle.new
115
- rectangle.assign_attributes(height: 10) # => Logs "Tried to assign to unknown attribute 'height'"
19
+ user = User.new
20
+ user.email = " CRUISE-CONTROL@EXAMPLE.COM\n"
21
+ user.email # => "cruise-control@example.com"
116
22
  ```
117
23
 
118
24
  *Sean Doyle*
119
25
 
120
- Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activemodel/CHANGELOG.md) for previous changes.
26
+ 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
@@ -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.transform_values(&: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
@@ -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.
@@ -252,7 +252,7 @@ module ActiveModel
252
252
 
253
253
  def init_attributes(other) # :nodoc:
254
254
  attrs = super
255
- if other.persisted? && self.class.respond_to?(:_default_attributes)
255
+ if self.class.respond_to?(:_default_attributes)
256
256
  self.class._default_attributes.map do |attr|
257
257
  attr.with_value_from_user(attrs.fetch_value(attr.name))
258
258
  end
@@ -296,22 +296,22 @@ module ActiveModel
296
296
  mutations_from_database.changed_attribute_names
297
297
  end
298
298
 
299
- # Dispatch target for {*_changed?}[rdoc-label:method-i-2A_changed-3F] attribute methods.
299
+ # Dispatch target for {*_changed?}[rdoc-ref:#*_changed?] attribute methods.
300
300
  def attribute_changed?(attr_name, **options)
301
301
  mutations_from_database.changed?(attr_name.to_s, **options)
302
302
  end
303
303
 
304
- # Dispatch target for {*_was}[rdoc-label:method-i-2A_was] attribute methods.
304
+ # Dispatch target for {*_was}[rdoc-ref:#*_was] attribute methods.
305
305
  def attribute_was(attr_name)
306
306
  mutations_from_database.original_value(attr_name.to_s)
307
307
  end
308
308
 
309
- # 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.
310
310
  def attribute_previously_changed?(attr_name, **options)
311
311
  mutations_before_last_save.changed?(attr_name.to_s, **options)
312
312
  end
313
313
 
314
- # 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.
315
315
  def attribute_previously_was(attr_name)
316
316
  mutations_before_last_save.original_value(attr_name.to_s)
317
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>.
@@ -8,9 +8,9 @@ module ActiveModel
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 8
11
- MINOR = 0
12
- TINY = 2
13
- PRE = nil
11
+ MINOR = 1
12
+ TINY = 0
13
+ PRE = "beta1"
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
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_model/error"
4
- require "forwardable"
5
4
 
6
5
  module ActiveModel
7
6
  class NestedError < Error
@@ -16,7 +15,6 @@ module ActiveModel
16
15
 
17
16
  attr_reader :inner_error
18
17
 
19
- extend Forwardable
20
- def_delegators :@inner_error, :message
18
+ delegate :message, to: :@inner_error
21
19
  end
22
20
  end
@@ -14,11 +14,15 @@ module ActiveModel
14
14
  end
15
15
 
16
16
  initializer "active_model.secure_password" do
17
- ActiveModel::SecurePassword.min_cost = Rails.env.test?
17
+ ActiveSupport.on_load(:active_model_secure_password) do
18
+ ActiveModel::SecurePassword.min_cost = Rails.env.test?
19
+ end
18
20
  end
19
21
 
20
- initializer "active_model.i18n_customize_full_message" do
21
- ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
22
+ initializer "active_model.i18n_customize_full_message" do |app|
23
+ ActiveSupport.on_load(:active_model_error) do
24
+ ActiveModel::Error.i18n_customize_full_message = app.config.active_model.i18n_customize_full_message || false
25
+ end
22
26
  end
23
27
  end
24
28
  end
@@ -155,7 +155,7 @@ module ActiveModel
155
155
  end
156
156
  end
157
157
 
158
- validates_confirmation_of attribute, allow_blank: true
158
+ validates_confirmation_of attribute, allow_nil: true
159
159
  end
160
160
 
161
161
  # Only generate tokens for records that are capable of doing so (Active Records, not vanilla Active Models)
@@ -228,4 +228,6 @@ module ActiveModel
228
228
  end
229
229
  end
230
230
  end
231
+
232
+ ActiveSupport.run_load_hooks(:active_model_secure_password, SecurePassword)
231
233
  end
@@ -23,10 +23,31 @@ module ActiveModel
23
23
  # All casting and serialization are performed in the same way as the
24
24
  # standard ActiveModel::Type::Integer type.
25
25
  class BigInteger < Integer
26
+ def serialize(value) # :nodoc:
27
+ case value
28
+ when ::Integer
29
+ # noop
30
+ when ::String
31
+ int = value.to_i
32
+ if int.zero? && value != "0"
33
+ return if non_numeric_string?(value)
34
+ end
35
+ value = int
36
+ else
37
+ value = super
38
+ end
39
+
40
+ value
41
+ end
42
+
26
43
  def serialize_cast_value(value) # :nodoc:
27
44
  value
28
45
  end
29
46
 
47
+ def serializable?(value, &)
48
+ true
49
+ end
50
+
30
51
  private
31
52
  def max_value
32
53
  ::Float::INFINITY
@@ -12,6 +12,7 @@ module ActiveModel
12
12
  # - Empty strings are coerced to +nil+.
13
13
  # - All other values will be coerced to +true+.
14
14
  class Boolean < Value
15
+ include Helpers::Immutable
15
16
  FALSE_VALUES = [
16
17
  false, 0,
17
18
  "0", :"0",
@@ -24,6 +24,7 @@ module ActiveModel
24
24
  # String values are parsed using the ISO 8601 date format. Any other values
25
25
  # are cast using their +to_date+ method, if it exists.
26
26
  class Date < Value
27
+ include Helpers::Immutable
27
28
  include Helpers::Timezone
28
29
  include Helpers::AcceptsMultiparameterTime.new
29
30
 
@@ -50,6 +50,14 @@ module ActiveModel
50
50
  :datetime
51
51
  end
52
52
 
53
+ def mutable? # :nodoc:
54
+ # Time#zone can be mutated by #utc or #localtime
55
+ # However when serializing the time zone will always
56
+ # be coerced and even if the zone was mutated Time instances
57
+ # remain equal, so we don't need to implement `#changed_in_place?`
58
+ true
59
+ end
60
+
53
61
  private
54
62
  def cast_value(value)
55
63
  return apply_seconds_precision(value) unless value.is_a?(::String)
@@ -43,6 +43,7 @@ module ActiveModel
43
43
  # attribute :weight, :decimal, precision: 24
44
44
  # end
45
45
  class Decimal < Value
46
+ include Helpers::Immutable
46
47
  include Helpers::Numeric
47
48
  BIGDECIMAL_PRECISION = 18
48
49
 
@@ -34,6 +34,7 @@ module ActiveModel
34
34
  # - <tt>"-Infinity"</tt> is cast to <tt>-Float::INFINITY</tt>.
35
35
  # - <tt>"NaN"</tt> is cast to +Float::NAN+.
36
36
  class Float < Value
37
+ include Helpers::Immutable
37
38
  include Helpers::Numeric
38
39
 
39
40
  def type
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ module Helpers # :nodoc: all
6
+ module Immutable
7
+ def mutable? # :nodoc:
8
+ false
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -3,5 +3,6 @@
3
3
  require "active_model/type/helpers/accepts_multiparameter_time"
4
4
  require "active_model/type/helpers/numeric"
5
5
  require "active_model/type/helpers/mutable"
6
+ require "active_model/type/helpers/immutable"
6
7
  require "active_model/type/helpers/time_value"
7
8
  require "active_model/type/helpers/timezone"
@@ -35,6 +35,8 @@ module ActiveModel
35
35
  #
36
36
  # person.active # => "aye"
37
37
  class ImmutableString < Value
38
+ include Helpers::Immutable
39
+
38
40
  def initialize(**args)
39
41
  @true = -(args.delete(:true)&.to_s || "t")
40
42
  @false = -(args.delete(:false)&.to_s || "f")
@@ -42,6 +42,7 @@ module ActiveModel
42
42
  # attribute :age, :integer, limit: 6
43
43
  # end
44
44
  class Integer < Value
45
+ include Helpers::Immutable
45
46
  include Helpers::Numeric
46
47
 
47
48
  # Column storage size in bytes.
@@ -50,7 +51,8 @@ module ActiveModel
50
51
 
51
52
  def initialize(**)
52
53
  super
53
- @range = min_value...max_value
54
+ @max = max_value
55
+ @min = min_value
54
56
  end
55
57
 
56
58
  def type
@@ -63,40 +65,50 @@ module ActiveModel
63
65
  end
64
66
 
65
67
  def serialize(value)
66
- return if value.is_a?(::String) && non_numeric_string?(value)
67
- ensure_in_range(super)
68
+ case value
69
+ when ::Integer
70
+ # noop
71
+ when ::String
72
+ int = value.to_i
73
+ if int.zero? && value != "0"
74
+ return if non_numeric_string?(value)
75
+ end
76
+ value = int
77
+ else
78
+ value = super
79
+ end
80
+
81
+ if out_of_range?(value)
82
+ raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
83
+ end
84
+
85
+ value
68
86
  end
69
87
 
70
88
  def serialize_cast_value(value) # :nodoc:
71
- ensure_in_range(value)
89
+ if out_of_range?(value)
90
+ raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
91
+ end
92
+
93
+ value
72
94
  end
73
95
 
74
96
  def serializable?(value)
75
97
  cast_value = cast(value)
76
- in_range?(cast_value) || begin
77
- yield cast_value if block_given?
78
- false
79
- end
98
+ return true unless out_of_range?(cast_value)
99
+ yield cast_value if block_given?
100
+ false
80
101
  end
81
102
 
82
103
  private
83
- attr_reader :range
84
-
85
- def in_range?(value)
86
- !value || range.member?(value)
104
+ def out_of_range?(value)
105
+ value && (@max <= value || @min > value)
87
106
  end
88
107
 
89
108
  def cast_value(value)
90
109
  value.to_i rescue nil
91
110
  end
92
111
 
93
- def ensure_in_range(value)
94
- unless in_range?(value)
95
- raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
96
- end
97
- value
98
- end
99
-
100
112
  def max_value
101
113
  1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
102
114
  end
@@ -19,6 +19,10 @@ module ActiveModel
19
19
  end
20
20
  end
21
21
 
22
+ def mutable? # :nodoc:
23
+ true
24
+ end
25
+
22
26
  def to_immutable_string
23
27
  ImmutableString.new(
24
28
  true: @true,
@@ -25,7 +25,7 @@ module ActiveModel
25
25
  # by the database. For example a boolean type can return +true+ if the
26
26
  # value parameter is a Ruby boolean, but may return +false+ if the value
27
27
  # parameter is some other object.
28
- def serializable?(value, &_)
28
+ def serializable?(value, &)
29
29
  true
30
30
  end
31
31
 
@@ -138,7 +138,7 @@ module ActiveModel
138
138
  end
139
139
 
140
140
  def mutable? # :nodoc:
141
- false
141
+ true
142
142
  end
143
143
 
144
144
  def as_json(*)
@@ -106,6 +106,16 @@ module ActiveModel
106
106
  *options[:if]
107
107
  ]
108
108
  end
109
+
110
+ if options.key?(:except_on)
111
+ options[:except_on] = Array(options[:except_on])
112
+ options[:unless] = [
113
+ ->(o) {
114
+ options[:except_on].intersect?(Array(o.validation_context))
115
+ },
116
+ *options[:unless]
117
+ ]
118
+ end
109
119
  end
110
120
  end
111
121
 
@@ -63,7 +63,8 @@ module ActiveModel
63
63
  # end
64
64
  # end
65
65
  #
66
- # Options:
66
+ # ==== Options
67
+ #
67
68
  # * <tt>:on</tt> - Specifies the contexts where this validation is active.
68
69
  # Runs in all validation contexts by default +nil+. You can pass a symbol
69
70
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
@@ -134,7 +135,8 @@ module ActiveModel
134
135
  # Note that the return value of validation methods is not relevant.
135
136
  # It's not possible to halt the validate callback chain.
136
137
  #
137
- # Options:
138
+ # ==== Options
139
+ #
138
140
  # * <tt>:on</tt> - Specifies the contexts where this validation is active.
139
141
  # Runs in all validation contexts by default +nil+. You can pass a symbol
140
142
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
@@ -437,20 +439,6 @@ module ActiveModel
437
439
  alias :read_attribute_for_validation :send
438
440
 
439
441
  # Returns the context when running validations.
440
- #
441
- # This is useful when running validations except a certain context (opposite to the +on+ option).
442
- #
443
- # class Person
444
- # include ActiveModel::Validations
445
- #
446
- # attr_accessor :name
447
- # validates :name, presence: true, if: -> { validation_context != :custom }
448
- # end
449
- #
450
- # person = Person.new
451
- # person.valid? #=> false
452
- # person.valid?(:new) #=> false
453
- # person.valid?(:custom) #=> true
454
442
  def validation_context
455
443
  context_for_validation.context
456
444
  end
data/lib/active_model.rb CHANGED
@@ -56,6 +56,12 @@ module ActiveModel
56
56
  autoload :Validations
57
57
  autoload :Validator
58
58
 
59
+ module Attributes
60
+ extend ActiveSupport::Autoload
61
+
62
+ autoload :Normalization
63
+ end
64
+
59
65
  eager_autoload do
60
66
  autoload :Errors
61
67
  autoload :Error
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activemodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.2
4
+ version: 8.1.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 8.0.2
18
+ version: 8.1.0.beta1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 8.0.2
25
+ version: 8.1.0.beta1
26
26
  description: A toolkit for building modeling frameworks like Active Record. Rich support
27
27
  for attributes, callbacks, validations, serialization, internationalization, and
28
28
  testing.
@@ -47,6 +47,7 @@ files:
47
47
  - lib/active_model/attribute_set/builder.rb
48
48
  - lib/active_model/attribute_set/yaml_encoder.rb
49
49
  - lib/active_model/attributes.rb
50
+ - lib/active_model/attributes/normalization.rb
50
51
  - lib/active_model/callbacks.rb
51
52
  - lib/active_model/conversion.rb
52
53
  - lib/active_model/deprecator.rb
@@ -75,6 +76,7 @@ files:
75
76
  - lib/active_model/type/float.rb
76
77
  - lib/active_model/type/helpers.rb
77
78
  - lib/active_model/type/helpers/accepts_multiparameter_time.rb
79
+ - lib/active_model/type/helpers/immutable.rb
78
80
  - lib/active_model/type/helpers/mutable.rb
79
81
  - lib/active_model/type/helpers/numeric.rb
80
82
  - lib/active_model/type/helpers/time_value.rb
@@ -111,10 +113,10 @@ licenses:
111
113
  - MIT
112
114
  metadata:
113
115
  bug_tracker_uri: https://github.com/rails/rails/issues
114
- changelog_uri: https://github.com/rails/rails/blob/v8.0.2/activemodel/CHANGELOG.md
115
- documentation_uri: https://api.rubyonrails.org/v8.0.2/
116
+ changelog_uri: https://github.com/rails/rails/blob/v8.1.0.beta1/activemodel/CHANGELOG.md
117
+ documentation_uri: https://api.rubyonrails.org/v8.1.0.beta1/
116
118
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
117
- source_code_uri: https://github.com/rails/rails/tree/v8.0.2/activemodel
119
+ source_code_uri: https://github.com/rails/rails/tree/v8.1.0.beta1/activemodel
118
120
  rubygems_mfa_required: 'true'
119
121
  rdoc_options: []
120
122
  require_paths:
@@ -130,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
132
  - !ruby/object:Gem::Version
131
133
  version: '0'
132
134
  requirements: []
133
- rubygems_version: 3.6.2
135
+ rubygems_version: 3.6.9
134
136
  specification_version: 4
135
137
  summary: A toolkit for building modeling frameworks (part of Rails).
136
138
  test_files: []