activemodel 7.1.5.2 → 8.1.2.1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -281
  3. data/README.rdoc +10 -10
  4. data/lib/active_model/attribute/user_provided_default.rb +10 -0
  5. data/lib/active_model/attribute.rb +10 -2
  6. data/lib/active_model/attribute_assignment.rb +26 -3
  7. data/lib/active_model/attribute_methods.rb +42 -43
  8. data/lib/active_model/attribute_mutation_tracker.rb +5 -5
  9. data/lib/active_model/attribute_registration.rb +62 -22
  10. data/lib/active_model/attribute_set.rb +1 -1
  11. data/lib/active_model/attributes/normalization.rb +192 -0
  12. data/lib/active_model/attributes.rb +13 -0
  13. data/lib/active_model/callbacks.rb +1 -1
  14. data/lib/active_model/conversion.rb +1 -1
  15. data/lib/active_model/dirty.rb +16 -9
  16. data/lib/active_model/error.rb +3 -2
  17. data/lib/active_model/errors.rb +1 -4
  18. data/lib/active_model/gem_version.rb +3 -3
  19. data/lib/active_model/lint.rb +7 -3
  20. data/lib/active_model/model.rb +2 -2
  21. data/lib/active_model/naming.rb +0 -1
  22. data/lib/active_model/nested_error.rb +1 -3
  23. data/lib/active_model/railtie.rb +8 -4
  24. data/lib/active_model/secure_password.rb +62 -5
  25. data/lib/active_model/serialization.rb +21 -21
  26. data/lib/active_model/translation.rb +22 -5
  27. data/lib/active_model/type/big_integer.rb +21 -0
  28. data/lib/active_model/type/boolean.rb +1 -0
  29. data/lib/active_model/type/date.rb +1 -0
  30. data/lib/active_model/type/date_time.rb +8 -0
  31. data/lib/active_model/type/decimal.rb +1 -0
  32. data/lib/active_model/type/float.rb +9 -8
  33. data/lib/active_model/type/helpers/immutable.rb +13 -0
  34. data/lib/active_model/type/helpers/time_value.rb +12 -15
  35. data/lib/active_model/type/helpers/timezone.rb +5 -1
  36. data/lib/active_model/type/helpers.rb +1 -0
  37. data/lib/active_model/type/immutable_string.rb +2 -0
  38. data/lib/active_model/type/integer.rb +31 -19
  39. data/lib/active_model/type/registry.rb +2 -3
  40. data/lib/active_model/type/string.rb +4 -0
  41. data/lib/active_model/type/value.rb +4 -4
  42. data/lib/active_model/validations/acceptance.rb +1 -1
  43. data/lib/active_model/validations/callbacks.rb +11 -1
  44. data/lib/active_model/validations/comparison.rb +1 -1
  45. data/lib/active_model/validations/validates.rb +8 -3
  46. data/lib/active_model/validations.rb +57 -32
  47. data/lib/active_model.rb +6 -0
  48. metadata +10 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e89137dfa40b068e71b94112ef2591344ea839e3e077c3b1c97cb15bf8248119
4
- data.tar.gz: 7bc83d36358978fd5f9ed4d54f58d2e80eac9a05fd5b5464130beec1fc8eb745
3
+ metadata.gz: e7d64f9c0fada4afc52f06c6b1e86cea5506c3c0e5bbb62007240efaa9635f5d
4
+ data.tar.gz: 73f7293af65afdd7e75dc8403f0625787c6e0e911ac629e689bd47576b995192
5
5
  SHA512:
6
- metadata.gz: 5774c02cbdd72532211828a0410af3bb5215ac937e8868809257206c4798a76051a484cfa7a0d3e19476ad6339c1076ae1b5d2055ce78b3fdde4641d9cef9086
7
- data.tar.gz: 615a9ed964889e21a90432b6c08411f03df07836a66de01bf9079b44979e2779032667c568873569a436e40b030b62d9c7c7aed7ac20fe74f4c905c8eaa8d5da
6
+ metadata.gz: 7bebff0c62141aa5ae3aa8983b261dc35634d2f3ae2266504a7f0bfc6184558bc2860d487ae452ffab1ed548fcf84e9442eebec4682cec2ec69bde83bd672370
7
+ data.tar.gz: 13f68620adf0b69c15818c6e290f589e263d33cdf608bb82c50928cc63c2a20ee27fb7b8f6df543e501ecb16e10dea02a409f7759a54db3e197d59bc50e2c3ff
data/CHANGELOG.md CHANGED
@@ -1,311 +1,51 @@
1
- ## Rails 7.1.5.2 (August 13, 2025) ##
1
+ ## Rails 8.1.2.1 (March 23, 2026) ##
2
2
 
3
3
  * No changes.
4
4
 
5
5
 
6
- ## Rails 7.1.5.1 (December 10, 2024) ##
6
+ ## Rails 8.1.2 (January 08, 2026) ##
7
7
 
8
8
  * No changes.
9
9
 
10
10
 
11
- ## Rails 7.1.5 (October 30, 2024) ##
12
-
13
- * Fix regression in `alias_attribute` to work with user defined methods.
14
-
15
- `alias_attribute` would wrongly assume the attribute accessor was generated by Active Model.
16
-
17
- ```ruby
18
- class Person
19
- include ActiveModel::AttributeMethods
20
-
21
- define_attribute_methods :name
22
- attr_accessor :name
23
-
24
- alias_attribute :full_name, :name
25
- end
26
-
27
- person.full_name # => NoMethodError: undefined method `attribute' for an instance of Person
28
- ```
29
-
30
- *Jean Boussier*
31
-
32
-
33
- ## Rails 7.1.4.2 (October 23, 2024) ##
34
-
35
- * No changes.
36
-
37
-
38
- ## Rails 7.1.4.1 (October 15, 2024) ##
39
-
40
- * No changes.
41
-
42
-
43
- ## Rails 7.1.4 (August 22, 2024) ##
11
+ ## Rails 8.1.1 (October 28, 2025) ##
44
12
 
45
13
  * No changes.
46
14
 
47
15
 
48
- ## Rails 7.1.3.4 (June 04, 2024) ##
49
-
50
- * No changes.
51
-
52
-
53
- ## Rails 7.1.3.3 (May 16, 2024) ##
54
-
55
- * No changes.
56
-
57
-
58
- ## Rails 7.1.3.2 (February 21, 2024) ##
59
-
60
- * No changes.
61
-
62
-
63
- ## Rails 7.1.3.1 (February 21, 2024) ##
64
-
65
- * No changes.
66
-
67
-
68
- ## Rails 7.1.3 (January 16, 2024) ##
69
-
70
- * No changes.
71
-
72
-
73
- ## Rails 7.1.2 (November 10, 2023) ##
74
-
75
- * Make `==(other)` method of AttributeSet safe.
76
-
77
- *Dmitry Pogrebnoy*
78
-
79
-
80
- ## Rails 7.1.1 (October 11, 2023) ##
81
-
82
- * No changes.
83
-
84
-
85
- ## Rails 7.1.0 (October 05, 2023) ##
86
-
87
- * No changes.
88
-
89
-
90
- ## Rails 7.1.0.rc2 (October 01, 2023) ##
91
-
92
- * No changes.
93
-
94
-
95
- ## Rails 7.1.0.rc1 (September 27, 2023) ##
96
-
97
- * Remove change in the typography of user facing error messages.
98
- For example, “can’t be blank” is again “can't be blank”.
99
-
100
- *Rafael Mendonça França*
101
-
102
-
103
- ## Rails 7.1.0.beta1 (September 13, 2023) ##
104
-
105
- * Support composite identifiers in `to_key`
106
-
107
- `to_key` avoids wrapping `#id` value into an `Array` if `#id` already an array
108
-
109
- *Nikita Vasilevsky*
110
-
111
- * Add `ActiveModel::Conversion.param_delimiter` to configure delimiter being used in `to_param`
112
-
113
- *Nikita Vasilevsky*
114
-
115
- * `undefine_attribute_methods` undefines alias attribute methods along with attribute methods.
116
-
117
- *Nikita Vasilevsky*
16
+ ## Rails 8.1.0 (October 22, 2025) ##
118
17
 
119
- * Error.full_message now strips ":base" from the message.
18
+ * Add `reset_token: { expires_in: ... }` option to `has_secure_password`.
120
19
 
121
- *zzak*
122
-
123
- * Add a load hook for `ActiveModel::Model` (named `active_model`) to match the load hook for
124
- `ActiveRecord::Base` and allow for overriding aspects of the `ActiveModel::Model` class.
125
-
126
- *Lewis Buckley*
127
-
128
- * Improve password length validation in ActiveModel::SecurePassword to consider byte size for BCrypt
129
- compatibility.
130
-
131
- The previous password length validation only considered the character count, which may not
132
- accurately reflect the 72-byte size limit imposed by BCrypt. This change updates the validation
133
- to consider both character count and byte size while keeping the character length validation in place.
134
-
135
- ```ruby
136
- user = User.new(password: "a" * 73) # 73 characters
137
- user.valid? # => false
138
- user.errors[:password] # => ["is too long"]
139
-
140
-
141
- user = User.new(password: "あ" * 25) # 25 characters, 75 bytes
142
- user.valid? # => false
143
- user.errors[:password] # => ["is too long"]
144
- ```
145
-
146
- *ChatGPT*, *Guillermo Iguaran*
147
-
148
- * `has_secure_password` now generates an `#{attribute}_salt` method that returns the salt
149
- used to compute the password digest. The salt will change whenever the password is changed,
150
- so it can be used to create single-use password reset tokens with `generates_token_for`:
151
-
152
- ```ruby
153
- class User < ActiveRecord::Base
154
- has_secure_password
155
-
156
- generates_token_for :password_reset, expires_in: 15.minutes do
157
- password_salt&.last(10)
158
- end
159
- end
160
- ```
161
-
162
- *Lázaro Nixon*
163
-
164
- * Improve typography of user facing error messages. In English contractions,
165
- the Unicode APOSTROPHE (`U+0027`) is now RIGHT SINGLE QUOTATION MARK
166
- (`U+2019`). For example, "can't be blank" is now "can’t be blank".
167
-
168
- *Jon Dufresne*
169
-
170
- * Add class to `ActiveModel::MissingAttributeError` error message.
171
-
172
- Show which class is missing the attribute in the error message:
20
+ Allows configuring the expiry duration of password reset tokens (default remains 15 minutes for backwards compatibility).
173
21
 
174
22
  ```ruby
175
- user = User.first
176
- user.pets.select(:id).first.user_id
177
- # => ActiveModel::MissingAttributeError: missing attribute 'user_id' for Pet
23
+ has_secure_password reset_token: { expires_in: 1.hour }
178
24
  ```
179
25
 
180
- *Petrik de Heus*
181
-
182
- * Raise `NoMethodError` in `ActiveModel::Type::Value#as_json` to avoid unpredictable
183
- results.
26
+ *Jevin Sew*, *Abeid Ahmed*
184
27
 
185
- *Vasiliy Ermolovich*
28
+ * Add `except_on:` option for validation callbacks.
186
29
 
187
- * Custom attribute types that inherit from Active Model built-in types and do
188
- not override the `serialize` method will now benefit from an optimization
189
- when serializing attribute values for the database.
30
+ *Ben Sheldon*
190
31
 
191
- For example, with a custom type like the following:
32
+ * Backport `ActiveRecord::Normalization` to `ActiveModel::Attributes::Normalization`
192
33
 
193
34
  ```ruby
194
- class DowncasedString < ActiveModel::Type::String
195
- def cast(value)
196
- super&.downcase
197
- end
198
- end
35
+ class User
36
+ include ActiveModel::Attributes
37
+ include ActiveModel::Attributes::Normalization
199
38
 
200
- ActiveRecord::Type.register(:downcased_string, DowncasedString)
39
+ attribute :email, :string
201
40
 
202
- class User < ActiveRecord::Base
203
- attribute :email, :downcased_string
41
+ normalizes :email, with: -> email { email.strip.downcase }
204
42
  end
205
43
 
206
- user = User.new(email: "FooBar@example.com")
44
+ user = User.new
45
+ user.email = " CRUISE-CONTROL@EXAMPLE.COM\n"
46
+ user.email # => "cruise-control@example.com"
207
47
  ```
208
48
 
209
- Serializing the `email` attribute for the database will be roughly twice as
210
- fast. More expensive `cast` operations will likely see greater improvements.
211
-
212
- *Jonathan Hefner*
213
-
214
- * `has_secure_password` now supports password challenges via a
215
- `password_challenge` accessor and validation.
216
-
217
- A password challenge is a safeguard to verify that the current user is
218
- actually the password owner. It can be used when changing sensitive model
219
- fields, such as the password itself. It is different than a password
220
- confirmation, which is used to prevent password typos.
221
-
222
- When `password_challenge` is set, the validation checks that the value's
223
- digest matches the *currently persisted* `password_digest` (i.e.
224
- `password_digest_was`).
225
-
226
- This allows a password challenge to be done as part of a typical `update`
227
- call, just like a password confirmation. It also allows a password
228
- challenge error to be handled in the same way as other validation errors.
229
-
230
- For example, in the controller, instead of:
231
-
232
- ```ruby
233
- password_params = params.require(:password).permit(
234
- :password_challenge,
235
- :password,
236
- :password_confirmation,
237
- )
238
-
239
- password_challenge = password_params.delete(:password_challenge)
240
- @password_challenge_failed = !current_user.authenticate(password_challenge)
241
-
242
- if !@password_challenge_failed && current_user.update(password_params)
243
- # ...
244
- end
245
- ```
246
-
247
- You can now write:
248
-
249
- ```ruby
250
- password_params = params.require(:password).permit(
251
- :password_challenge,
252
- :password,
253
- :password_confirmation,
254
- ).with_defaults(password_challenge: "")
255
-
256
- if current_user.update(password_params)
257
- # ...
258
- end
259
- ```
260
-
261
- And, in the view, instead of checking `@password_challenge_failed`, you can
262
- render an error for the `password_challenge` field just as you would for
263
- other form fields, including utilizing `config.action_view.field_error_proc`.
264
-
265
- *Jonathan Hefner*
266
-
267
- * Support infinite ranges for `LengthValidator`s `:in`/`:within` options
268
-
269
- ```ruby
270
- validates_length_of :first_name, in: ..30
271
- ```
272
-
273
- *fatkodima*
274
-
275
- * Add support for beginless ranges to inclusivity/exclusivity validators:
276
-
277
- ```ruby
278
- validates_inclusion_of :birth_date, in: -> { (..Date.today) }
279
- ```
280
-
281
- ```ruby
282
- validates_exclusion_of :birth_date, in: -> { (..Date.today) }
283
- ```
284
-
285
- *Bo Jeanes*
286
-
287
- * Make validators accept lambdas without record argument
288
-
289
- ```ruby
290
- # Before
291
- validates_comparison_of :birth_date, less_than_or_equal_to: ->(_record) { Date.today }
292
-
293
- # After
294
- validates_comparison_of :birth_date, less_than_or_equal_to: -> { Date.today }
295
- ```
296
-
297
- *fatkodima*
298
-
299
- * Fix casting long strings to `Date`, `Time` or `DateTime`
300
-
301
- *fatkodima*
302
-
303
- * Use different cache namespace for proxy calls
304
-
305
- Models can currently have different attribute bodies for the same method
306
- names, leading to conflicts. Adding a new namespace `:active_model_proxy`
307
- fixes the issue.
308
-
309
- *Chris Salzberg*
49
+ *Sean Doyle*
310
50
 
311
- Please check [7-0-stable](https://github.com/rails/rails/blob/7-0-stable/activemodel/CHANGELOG.md) for previous changes.
51
+ Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/activemodel/CHANGELOG.md) for previous changes.
data/README.rdoc CHANGED
@@ -56,7 +56,7 @@ behavior out of the box:
56
56
  person.clear_name
57
57
  person.clear_age
58
58
 
59
- {Learn more}[link:classes/ActiveModel/AttributeMethods.html]
59
+ {Learn more}[https://api.rubyonrails.org/classes/ActiveModel/AttributeMethods.html]
60
60
 
61
61
  * Callbacks for certain operations
62
62
 
@@ -74,7 +74,7 @@ behavior out of the box:
74
74
  This generates +before_create+, +around_create+ and +after_create+
75
75
  class methods that wrap your create method.
76
76
 
77
- {Learn more}[link:classes/ActiveModel/Callbacks.html]
77
+ {Learn more}[https://api.rubyonrails.org/classes/ActiveModel/Callbacks.html]
78
78
 
79
79
  * Tracking value changes
80
80
 
@@ -110,7 +110,7 @@ behavior out of the box:
110
110
  person.save
111
111
  person.previous_changes # => {'name' => ['bob, 'robert']}
112
112
 
113
- {Learn more}[link:classes/ActiveModel/Dirty.html]
113
+ {Learn more}[https://api.rubyonrails.org/classes/ActiveModel/Dirty.html]
114
114
 
115
115
  * Adding +errors+ interface to objects
116
116
 
@@ -141,7 +141,7 @@ behavior out of the box:
141
141
  person.errors.full_messages
142
142
  # => ["Name cannot be nil"]
143
143
 
144
- {Learn more}[link:classes/ActiveModel/Errors.html]
144
+ {Learn more}[https://api.rubyonrails.org/classes/ActiveModel/Errors.html]
145
145
 
146
146
  * Model name introspection
147
147
 
@@ -152,7 +152,7 @@ behavior out of the box:
152
152
  NamedPerson.model_name.name # => "NamedPerson"
153
153
  NamedPerson.model_name.human # => "Named person"
154
154
 
155
- {Learn more}[link:classes/ActiveModel/Naming.html]
155
+ {Learn more}[https://api.rubyonrails.org/classes/ActiveModel/Naming.html]
156
156
 
157
157
  * Making objects serializable
158
158
 
@@ -179,7 +179,7 @@ behavior out of the box:
179
179
  s = SerialPerson.new
180
180
  s.to_json # => "{\"name\":null}"
181
181
 
182
- {Learn more}[link:classes/ActiveModel/Serialization.html]
182
+ {Learn more}[https://api.rubyonrails.org/classes/ActiveModel/Serialization.html]
183
183
 
184
184
  * Internationalization (i18n) support
185
185
 
@@ -190,7 +190,7 @@ behavior out of the box:
190
190
  Person.human_attribute_name('my_attribute')
191
191
  # => "My attribute"
192
192
 
193
- {Learn more}[link:classes/ActiveModel/Translation.html]
193
+ {Learn more}[https://api.rubyonrails.org/classes/ActiveModel/Translation.html]
194
194
 
195
195
  * Validation support
196
196
 
@@ -208,7 +208,7 @@ behavior out of the box:
208
208
  person.first_name = 'zoolander'
209
209
  person.valid? # => false
210
210
 
211
- {Learn more}[link:classes/ActiveModel/Validations.html]
211
+ {Learn more}[https://api.rubyonrails.org/classes/ActiveModel/Validations.html]
212
212
 
213
213
  * Custom validators
214
214
 
@@ -230,7 +230,7 @@ behavior out of the box:
230
230
  p.name = "Bob"
231
231
  p.valid? # => true
232
232
 
233
- {Learn more}[link:classes/ActiveModel/Validator.html]
233
+ {Learn more}[https://api.rubyonrails.org/classes/ActiveModel/Validator.html]
234
234
 
235
235
 
236
236
  == Download and installation
@@ -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
@@ -153,7 +161,7 @@ module ActiveModel
153
161
  alias :assigned? :original_attribute
154
162
 
155
163
  def initialize_dup(other)
156
- if defined?(@value) && @value.duplicable?
164
+ if @value&.duplicable?
157
165
  @value = @value.dup
158
166
  end
159
167
  end
@@ -36,19 +36,42 @@ 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
45
66
 
46
67
  def _assign_attribute(k, v)
47
68
  setter = :"#{k}="
69
+ public_send(setter, v)
70
+ rescue NoMethodError
48
71
  if respond_to?(setter)
49
- public_send(setter, v)
72
+ raise
50
73
  else
51
- raise UnknownAttributeError.new(self, k.to_s)
74
+ attribute_writer_missing(k.to_s, v)
52
75
  end
53
76
  end
54
77
  end