activemodel 7.1.5.1 → 7.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f79873d7c2a9772e4c9ca33cf468880b923a818c11b874eb7c8c40954a1e652
4
- data.tar.gz: a264d11a6a4a6783d08513145436a78440847774f64ab8bfe5436e0e0c56224b
3
+ metadata.gz: cd11eb90ab99961854be28823adafd875c43c0ee7d7e6067b2af752a1f183f88
4
+ data.tar.gz: 690c5e435225163f5ebee8eadeb5ebdcb6b9269140be69471e187d4bce24b7fa
5
5
  SHA512:
6
- metadata.gz: 7019a3654dabd54a630605e8b032450f170fe5aa8f3fa0ec626f80546dbf9c09cea143bed32356608319b059127ede72aae6e08c5a280dfae9021e98f027640f
7
- data.tar.gz: 72936b14496e7b9465b6cdaf7894b80b502bb275e58f7e4b9e52d67ccfd671ee1a4f952ea3da25b504147d81a645755a4cb16dde63f4ecbc68b6787636201b0b
6
+ metadata.gz: c0c5710770bf4a62c45d71ccc25629f73c39f45c9c38f013abd7947c63e72c4e35ec0bfe85da0b32a4ab3c62892aadea62b78ca64e04666015a318339d544c00
7
+ data.tar.gz: e1db829ae10154c73749425d12c1749df1cbbb9f6485c68badac84f2bd945d91ae9c0022eb0d5e886c11edced5e13476be20bf49605e2b318cb2d1dd3e58d9a8
data/CHANGELOG.md CHANGED
@@ -1,306 +1,88 @@
1
- ## Rails 7.1.5.1 (December 10, 2024) ##
1
+ ## Rails 7.2.3 (October 28, 2025) ##
2
2
 
3
- * No changes.
4
-
5
-
6
- ## Rails 7.1.5 (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
3
+ * Fix `has_secure_password` to perform confirmation validation of the password even when blank.
15
4
 
16
- define_attribute_methods :name
17
- attr_accessor :name
5
+ The validation was incorrectly skipped when the password only contained whitespace characters.
18
6
 
19
- alias_attribute :full_name, :name
20
- end
7
+ *Fabio Sangiovanni*
21
8
 
22
- person.full_name # => NoMethodError: undefined method `attribute' for an instance of Person
23
- ```
24
-
25
- *Jean Boussier*
26
-
27
-
28
- ## Rails 7.1.4.2 (October 23, 2024) ##
29
-
30
- * No changes.
31
-
32
-
33
- ## Rails 7.1.4.1 (October 15, 2024) ##
34
-
35
- * No changes.
36
-
37
-
38
- ## Rails 7.1.4 (August 22, 2024) ##
39
-
40
- * No changes.
9
+ * Handle missing attributes for `ActiveModel::Translation#human_attribute_name`.
41
10
 
11
+ *zzak*
42
12
 
43
- ## Rails 7.1.3.4 (June 04, 2024) ##
13
+ * Fix `ActiveModel::AttributeAssignment#assign_attributes` to accept objects without `each`.
44
14
 
45
- * No changes.
15
+ *Kouhei Yanagita*
46
16
 
47
17
 
48
- ## Rails 7.1.3.3 (May 16, 2024) ##
18
+ ## Rails 7.2.2.2 (August 13, 2025) ##
49
19
 
50
20
  * No changes.
51
21
 
52
22
 
53
- ## Rails 7.1.3.2 (February 21, 2024) ##
23
+ ## Rails 7.2.2.1 (December 10, 2024) ##
54
24
 
55
25
  * No changes.
56
26
 
57
27
 
58
- ## Rails 7.1.3.1 (February 21, 2024) ##
59
-
60
- * No changes.
28
+ ## Rails 7.2.2 (October 30, 2024) ##
61
29
 
30
+ * Fix regression in `alias_attribute` to work with user defined methods.
62
31
 
63
- ## Rails 7.1.3 (January 16, 2024) ##
32
+ `alias_attribute` would wrongly assume the attribute accessor was generated by Active Model.
64
33
 
65
- * No changes.
34
+ ```ruby
35
+ class Person
36
+ include ActiveModel::AttributeMethods
66
37
 
38
+ define_attribute_methods :name
39
+ attr_accessor :name
67
40
 
68
- ## Rails 7.1.2 (November 10, 2023) ##
41
+ alias_attribute :full_name, :name
42
+ end
69
43
 
70
- * Make `==(other)` method of AttributeSet safe.
44
+ person.full_name # => NoMethodError: undefined method `attribute' for an instance of Person
45
+ ```
71
46
 
72
- *Dmitry Pogrebnoy*
47
+ *Jean Boussier*
73
48
 
74
49
 
75
- ## Rails 7.1.1 (October 11, 2023) ##
50
+ ## Rails 7.2.1.2 (October 23, 2024) ##
76
51
 
77
52
  * No changes.
78
53
 
79
54
 
80
- ## Rails 7.1.0 (October 05, 2023) ##
55
+ ## Rails 7.2.1.1 (October 15, 2024) ##
81
56
 
82
57
  * No changes.
83
58
 
84
59
 
85
- ## Rails 7.1.0.rc2 (October 01, 2023) ##
60
+ ## Rails 7.2.1 (August 22, 2024) ##
86
61
 
87
62
  * No changes.
88
63
 
89
64
 
90
- ## Rails 7.1.0.rc1 (September 27, 2023) ##
91
-
92
- * Remove change in the typography of user facing error messages.
93
- For example, “can’t be blank” is again “can't be blank”.
94
-
95
- *Rafael Mendonça França*
96
-
97
-
98
- ## Rails 7.1.0.beta1 (September 13, 2023) ##
99
-
100
- * Support composite identifiers in `to_key`
101
-
102
- `to_key` avoids wrapping `#id` value into an `Array` if `#id` already an array
103
-
104
- *Nikita Vasilevsky*
105
-
106
- * Add `ActiveModel::Conversion.param_delimiter` to configure delimiter being used in `to_param`
107
-
108
- *Nikita Vasilevsky*
109
-
110
- * `undefine_attribute_methods` undefines alias attribute methods along with attribute methods.
111
-
112
- *Nikita Vasilevsky*
113
-
114
- * Error.full_message now strips ":base" from the message.
115
-
116
- *zzak*
117
-
118
- * Add a load hook for `ActiveModel::Model` (named `active_model`) to match the load hook for
119
- `ActiveRecord::Base` and allow for overriding aspects of the `ActiveModel::Model` class.
120
-
121
- *Lewis Buckley*
122
-
123
- * Improve password length validation in ActiveModel::SecurePassword to consider byte size for BCrypt
124
- compatibility.
125
-
126
- The previous password length validation only considered the character count, which may not
127
- accurately reflect the 72-byte size limit imposed by BCrypt. This change updates the validation
128
- to consider both character count and byte size while keeping the character length validation in place.
129
-
130
- ```ruby
131
- user = User.new(password: "a" * 73) # 73 characters
132
- user.valid? # => false
133
- user.errors[:password] # => ["is too long"]
134
-
135
-
136
- user = User.new(password: "あ" * 25) # 25 characters, 75 bytes
137
- user.valid? # => false
138
- user.errors[:password] # => ["is too long"]
139
- ```
140
-
141
- *ChatGPT*, *Guillermo Iguaran*
142
-
143
- * `has_secure_password` now generates an `#{attribute}_salt` method that returns the salt
144
- used to compute the password digest. The salt will change whenever the password is changed,
145
- so it can be used to create single-use password reset tokens with `generates_token_for`:
146
-
147
- ```ruby
148
- class User < ActiveRecord::Base
149
- has_secure_password
150
-
151
- generates_token_for :password_reset, expires_in: 15.minutes do
152
- password_salt&.last(10)
153
- end
154
- end
155
- ```
156
-
157
- *Lázaro Nixon*
158
-
159
- * Improve typography of user facing error messages. In English contractions,
160
- the Unicode APOSTROPHE (`U+0027`) is now RIGHT SINGLE QUOTATION MARK
161
- (`U+2019`). For example, "can't be blank" is now "can’t be blank".
162
-
163
- *Jon Dufresne*
164
-
165
- * Add class to `ActiveModel::MissingAttributeError` error message.
166
-
167
- Show which class is missing the attribute in the error message:
168
-
169
- ```ruby
170
- user = User.first
171
- user.pets.select(:id).first.user_id
172
- # => ActiveModel::MissingAttributeError: missing attribute 'user_id' for Pet
173
- ```
65
+ ## Rails 7.2.0 (August 09, 2024) ##
174
66
 
175
- *Petrik de Heus*
67
+ * Fix a bug where type casting of string to `Time` and `DateTime` doesn't
68
+ calculate minus minute value in TZ offset correctly.
176
69
 
177
- * Raise `NoMethodError` in `ActiveModel::Type::Value#as_json` to avoid unpredictable
178
- results.
70
+ *Akira Matsuda*
179
71
 
180
- *Vasiliy Ermolovich*
72
+ * Port the `type_for_attribute` method to Active Model. Classes that include
73
+ `ActiveModel::Attributes` will now provide this method. This method behaves
74
+ the same for Active Model as it does for Active Record.
181
75
 
182
- * Custom attribute types that inherit from Active Model built-in types and do
183
- not override the `serialize` method will now benefit from an optimization
184
- when serializing attribute values for the database.
76
+ ```ruby
77
+ class MyModel
78
+ include ActiveModel::Attributes
185
79
 
186
- For example, with a custom type like the following:
187
-
188
- ```ruby
189
- class DowncasedString < ActiveModel::Type::String
190
- def cast(value)
191
- super&.downcase
80
+ attribute :my_attribute, :integer
192
81
  end
193
- end
194
82
 
195
- ActiveRecord::Type.register(:downcased_string, DowncasedString)
196
-
197
- class User < ActiveRecord::Base
198
- attribute :email, :downcased_string
199
- end
200
-
201
- user = User.new(email: "FooBar@example.com")
202
- ```
203
-
204
- Serializing the `email` attribute for the database will be roughly twice as
205
- fast. More expensive `cast` operations will likely see greater improvements.
83
+ MyModel.type_for_attribute(:my_attribute) # => #<ActiveModel::Type::Integer ...>
84
+ ```
206
85
 
207
86
  *Jonathan Hefner*
208
87
 
209
- * `has_secure_password` now supports password challenges via a
210
- `password_challenge` accessor and validation.
211
-
212
- A password challenge is a safeguard to verify that the current user is
213
- actually the password owner. It can be used when changing sensitive model
214
- fields, such as the password itself. It is different than a password
215
- confirmation, which is used to prevent password typos.
216
-
217
- When `password_challenge` is set, the validation checks that the value's
218
- digest matches the *currently persisted* `password_digest` (i.e.
219
- `password_digest_was`).
220
-
221
- This allows a password challenge to be done as part of a typical `update`
222
- call, just like a password confirmation. It also allows a password
223
- challenge error to be handled in the same way as other validation errors.
224
-
225
- For example, in the controller, instead of:
226
-
227
- ```ruby
228
- password_params = params.require(:password).permit(
229
- :password_challenge,
230
- :password,
231
- :password_confirmation,
232
- )
233
-
234
- password_challenge = password_params.delete(:password_challenge)
235
- @password_challenge_failed = !current_user.authenticate(password_challenge)
236
-
237
- if !@password_challenge_failed && current_user.update(password_params)
238
- # ...
239
- end
240
- ```
241
-
242
- You can now write:
243
-
244
- ```ruby
245
- password_params = params.require(:password).permit(
246
- :password_challenge,
247
- :password,
248
- :password_confirmation,
249
- ).with_defaults(password_challenge: "")
250
-
251
- if current_user.update(password_params)
252
- # ...
253
- end
254
- ```
255
-
256
- And, in the view, instead of checking `@password_challenge_failed`, you can
257
- render an error for the `password_challenge` field just as you would for
258
- other form fields, including utilizing `config.action_view.field_error_proc`.
259
-
260
- *Jonathan Hefner*
261
-
262
- * Support infinite ranges for `LengthValidator`s `:in`/`:within` options
263
-
264
- ```ruby
265
- validates_length_of :first_name, in: ..30
266
- ```
267
-
268
- *fatkodima*
269
-
270
- * Add support for beginless ranges to inclusivity/exclusivity validators:
271
-
272
- ```ruby
273
- validates_inclusion_of :birth_date, in: -> { (..Date.today) }
274
- ```
275
-
276
- ```ruby
277
- validates_exclusion_of :birth_date, in: -> { (..Date.today) }
278
- ```
279
-
280
- *Bo Jeanes*
281
-
282
- * Make validators accept lambdas without record argument
283
-
284
- ```ruby
285
- # Before
286
- validates_comparison_of :birth_date, less_than_or_equal_to: ->(_record) { Date.today }
287
-
288
- # After
289
- validates_comparison_of :birth_date, less_than_or_equal_to: -> { Date.today }
290
- ```
291
-
292
- *fatkodima*
293
-
294
- * Fix casting long strings to `Date`, `Time` or `DateTime`
295
-
296
- *fatkodima*
297
-
298
- * Use different cache namespace for proxy calls
299
-
300
- Models can currently have different attribute bodies for the same method
301
- names, leading to conflicts. Adding a new namespace `:active_model_proxy`
302
- fixes the issue.
303
-
304
- *Chris Salzberg*
305
-
306
- Please check [7-0-stable](https://github.com/rails/rails/blob/7-0-stable/activemodel/CHANGELOG.md) for previous changes.
88
+ Please check [7-1-stable](https://github.com/rails/rails/blob/7-1-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
@@ -153,7 +153,7 @@ module ActiveModel
153
153
  alias :assigned? :original_attribute
154
154
 
155
155
  def initialize_dup(other)
156
- if defined?(@value) && @value.duplicable?
156
+ if @value&.duplicable?
157
157
  @value = @value.dup
158
158
  end
159
159
  end
@@ -38,15 +38,17 @@ module ActiveModel
38
38
 
39
39
  private
40
40
  def _assign_attributes(attributes)
41
- attributes.each do |k, v|
41
+ attributes.each_pair do |k, v|
42
42
  _assign_attribute(k, v)
43
43
  end
44
44
  end
45
45
 
46
46
  def _assign_attribute(k, v)
47
47
  setter = :"#{k}="
48
+ public_send(setter, v)
49
+ rescue NoMethodError
48
50
  if respond_to?(setter)
49
- public_send(setter, v)
51
+ raise
50
52
  else
51
53
  raise UnknownAttributeError.new(self, k.to_s)
52
54
  end
@@ -66,7 +66,6 @@ module ActiveModel
66
66
 
67
67
  NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
68
68
  CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
69
- FORWARD_PARAMETERS = "*args"
70
69
 
71
70
  included do
72
71
  class_attribute :attribute_aliases, instance_writer: false, default: {}
@@ -228,28 +227,13 @@ module ActiveModel
228
227
  method_name = pattern.method_name(new_name).to_s
229
228
  target_name = pattern.method_name(old_name).to_s
230
229
  parameters = pattern.parameters
231
- mangled_name = target_name
232
230
 
233
- unless NAME_COMPILABLE_REGEXP.match?(target_name)
234
- mangled_name = "__temp__#{target_name.unpack1("h*")}"
235
- end
231
+ mangled_name = build_mangled_name(target_name)
236
232
 
237
- code_generator.define_cached_method(mangled_name, as: method_name, namespace: :alias_attribute) do |batch|
238
- body = if CALL_COMPILABLE_REGEXP.match?(target_name)
239
- "self.#{target_name}(#{parameters || ''})"
240
- else
241
- call_args = [":'#{target_name}'"]
242
- call_args << parameters if parameters
243
- "send(#{call_args.join(", ")})"
244
- end
233
+ call_args = []
234
+ call_args << parameters if parameters
245
235
 
246
- modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
247
-
248
- batch <<
249
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
250
- body <<
251
- "end"
252
- end
236
+ define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute, as: method_name)
253
237
  end
254
238
 
255
239
  # Is +new_name+ an alias?
@@ -347,6 +331,7 @@ module ActiveModel
347
331
  end
348
332
 
349
333
  generate_method = "define_method_#{pattern.proxy_target}"
334
+
350
335
  if respond_to?(generate_method, true)
351
336
  send(generate_method, attr_name.to_s, owner: owner, as: as)
352
337
  else
@@ -357,7 +342,7 @@ module ActiveModel
357
342
  pattern.parameters,
358
343
  attr_name.to_s,
359
344
  namespace: :active_model_proxy,
360
- as: public_method_name,
345
+ as: public_method_name
361
346
  )
362
347
  end
363
348
  end
@@ -403,6 +388,8 @@ module ActiveModel
403
388
  super
404
389
  base.class_eval do
405
390
  @attribute_method_patterns_cache = nil
391
+ @aliases_by_attribute_name = nil
392
+ @generated_attribute_methods = nil
406
393
  end
407
394
  end
408
395
 
@@ -441,27 +428,41 @@ module ActiveModel
441
428
  # using the given `extra` args. This falls back on `send`
442
429
  # if the called name cannot be compiled.
443
430
  def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
444
- mangled_name = name
445
- unless NAME_COMPILABLE_REGEXP.match?(name)
446
- mangled_name = "__temp__#{name.unpack1("h*")}"
447
- end
431
+ mangled_name = build_mangled_name(name)
448
432
 
449
433
  call_args.map!(&:inspect)
450
434
  call_args << parameters if parameters
435
+
436
+ # We have to use a different namespace for every target method, because
437
+ # if someone defines an attribute that look like an attribute method we could clash, e.g.
438
+ # attribute :title_was
439
+ # attribute :title
451
440
  namespace = :"#{namespace}_#{proxy_target}"
452
441
 
442
+ define_call(code_generator, name, proxy_target, mangled_name, parameters, call_args, namespace: namespace, as: as)
443
+ end
444
+
445
+ def build_mangled_name(name)
446
+ mangled_name = name
447
+
448
+ unless NAME_COMPILABLE_REGEXP.match?(name)
449
+ mangled_name = :"__temp__#{name.unpack1("h*")}"
450
+ end
451
+
452
+ mangled_name
453
+ end
454
+
455
+ def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:, as:)
453
456
  code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
454
- body = if CALL_COMPILABLE_REGEXP.match?(proxy_target)
455
- "self.#{proxy_target}(#{call_args.join(", ")})"
457
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
458
+ "self.#{target_name}(#{call_args.join(", ")})"
456
459
  else
457
- call_args.unshift(":'#{proxy_target}'")
460
+ call_args.unshift(":'#{target_name}'")
458
461
  "send(#{call_args.join(", ")})"
459
462
  end
460
463
 
461
- modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
462
-
463
464
  batch <<
464
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
465
+ "def #{mangled_name}(#{parameters || ''})" <<
465
466
  body <<
466
467
  "end"
467
468
  end
@@ -475,7 +476,7 @@ module ActiveModel
475
476
  def initialize(prefix: "", suffix: "", parameters: nil)
476
477
  @prefix = prefix
477
478
  @suffix = suffix
478
- @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
479
+ @parameters = parameters.nil? ? "..." : parameters
479
480
  @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
480
481
  @proxy_target = "#{@prefix}attribute#{@suffix}"
481
482
  @method_name = "#{prefix}%s#{suffix}"
@@ -503,24 +504,22 @@ module ActiveModel
503
504
  # It's also possible to instantiate related objects, so a <tt>Client</tt>
504
505
  # class belonging to the +clients+ table with a +master_id+ foreign key
505
506
  # can instantiate master through <tt>Client#master</tt>.
506
- def method_missing(method, *args, &block)
507
+ def method_missing(method, ...)
507
508
  if respond_to_without_attributes?(method, true)
508
509
  super
509
510
  else
510
- match = matched_attribute_method(method.to_s)
511
- match ? attribute_missing(match, *args, &block) : super
511
+ match = matched_attribute_method(method.name)
512
+ match ? attribute_missing(match, ...) : super
512
513
  end
513
514
  end
514
- ruby2_keywords(:method_missing)
515
515
 
516
516
  # +attribute_missing+ is like +method_missing+, but for attributes. When
517
517
  # +method_missing+ is called we check to see if there is a matching
518
518
  # attribute method. If so, we tell +attribute_missing+ to dispatch the
519
519
  # attribute. This method can be overloaded to customize the behavior.
520
- def attribute_missing(match, *args, &block)
521
- __send__(match.proxy_target, match.attr_name, *args, &block)
520
+ def attribute_missing(match, ...)
521
+ __send__(match.proxy_target, match.attr_name, ...)
522
522
  end
523
- ruby2_keywords(:attribute_missing)
524
523
 
525
524
  # A +Person+ instance with a +name+ attribute can ask
526
525
  # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
@@ -10,17 +10,28 @@ module ActiveModel
10
10
 
11
11
  module ClassMethods # :nodoc:
12
12
  def attribute(name, type = nil, default: (no_default = true), **options)
13
+ name = resolve_attribute_name(name)
13
14
  type = resolve_type_name(type, **options) if type.is_a?(Symbol)
15
+ type = hook_attribute_type(name, type) if type
14
16
 
15
- pending = pending_attribute(name)
16
- pending.type = type if type
17
- pending.default = default unless no_default
17
+ pending_attribute_modifications << PendingType.new(name, type) if type || no_default
18
+ pending_attribute_modifications << PendingDefault.new(name, default) unless no_default
19
+
20
+ reset_default_attributes
21
+ end
22
+
23
+ def decorate_attributes(names = nil, &decorator) # :nodoc:
24
+ names = names&.map { |name| resolve_attribute_name(name) }
25
+
26
+ pending_attribute_modifications << PendingDecorator.new(names, decorator)
18
27
 
19
28
  reset_default_attributes
20
29
  end
21
30
 
22
31
  def _default_attributes # :nodoc:
23
- @default_attributes ||= build_default_attributes
32
+ @default_attributes ||= AttributeSet.new({}).tap do |attribute_set|
33
+ apply_pending_attribute_modifications(attribute_set)
34
+ end
24
35
  end
25
36
 
26
37
  def attribute_types # :nodoc:
@@ -29,40 +40,62 @@ module ActiveModel
29
40
  end
30
41
  end
31
42
 
43
+ def type_for_attribute(attribute_name, &block)
44
+ attribute_name = resolve_attribute_name(attribute_name)
45
+
46
+ if block
47
+ attribute_types.fetch(attribute_name, &block)
48
+ else
49
+ attribute_types[attribute_name]
50
+ end
51
+ end
52
+
32
53
  private
33
- class PendingAttribute # :nodoc:
34
- attr_accessor :type, :default
54
+ PendingType = Struct.new(:name, :type) do # :nodoc:
55
+ def apply_to(attribute_set)
56
+ attribute = attribute_set[name]
57
+ attribute_set[name] = attribute.with_type(type || attribute.type)
58
+ end
59
+ end
35
60
 
36
- def apply_to(attribute)
37
- attribute = attribute.with_type(type || attribute.type)
38
- attribute = attribute.with_user_default(default) if defined?(@default)
39
- attribute
61
+ PendingDefault = Struct.new(:name, :default) do # :nodoc:
62
+ def apply_to(attribute_set)
63
+ attribute_set[name] = attribute_set[name].with_user_default(default)
40
64
  end
41
65
  end
42
66
 
43
- def pending_attribute(name)
44
- @pending_attributes ||= {}
45
- @pending_attributes[resolve_attribute_name(name)] ||= PendingAttribute.new
67
+ PendingDecorator = Struct.new(:names, :decorator) do # :nodoc:
68
+ def apply_to(attribute_set)
69
+ (names || attribute_set.keys).each do |name|
70
+ attribute = attribute_set[name]
71
+ type = decorator.call(name, attribute.type)
72
+ attribute_set[name] = attribute.with_type(type) if type
73
+ end
74
+ end
46
75
  end
47
76
 
48
- def apply_pending_attributes(attribute_set)
49
- superclass.send(__method__, attribute_set) if superclass.respond_to?(__method__, true)
77
+ def pending_attribute_modifications
78
+ @pending_attribute_modifications ||= []
79
+ end
50
80
 
51
- defined?(@pending_attributes) && @pending_attributes.each do |name, pending|
52
- attribute_set[name] = pending.apply_to(attribute_set[name])
81
+ def apply_pending_attribute_modifications(attribute_set)
82
+ if superclass.respond_to?(:apply_pending_attribute_modifications, true)
83
+ superclass.send(:apply_pending_attribute_modifications, attribute_set)
53
84
  end
54
85
 
55
- attribute_set
86
+ pending_attribute_modifications.each do |modification|
87
+ modification.apply_to(attribute_set)
88
+ end
56
89
  end
57
90
 
58
- def build_default_attributes
59
- apply_pending_attributes(AttributeSet.new({}))
91
+ def reset_default_attributes
92
+ reset_default_attributes!
93
+ subclasses.each { |subclass| subclass.send(:reset_default_attributes) }
60
94
  end
61
95
 
62
- def reset_default_attributes
96
+ def reset_default_attributes!
63
97
  @default_attributes = nil
64
98
  @attribute_types = nil
65
- subclasses.each { |subclass| subclass.send(__method__) }
66
99
  end
67
100
 
68
101
  def resolve_attribute_name(name)
@@ -72,6 +105,13 @@ module ActiveModel
72
105
  def resolve_type_name(name, **options)
73
106
  Type.lookup(name, **options)
74
107
  end
108
+
109
+ # Hook for other modules to override. The attribute type is passed
110
+ # through this method immediately after it is resolved, before any type
111
+ # decorations are applied.
112
+ def hook_attribute_type(attribute, type)
113
+ type
114
+ end
75
115
  end
76
116
  end
77
117
  end
@@ -75,6 +75,19 @@ module ActiveModel
75
75
  attribute_types.keys
76
76
  end
77
77
 
78
+ ##
79
+ # :method: type_for_attribute
80
+ # :call-seq: type_for_attribute(attribute_name, &block)
81
+ #
82
+ # Returns the type of the specified attribute after applying any
83
+ # modifiers. This method is the only valid source of information for
84
+ # anything related to the types of a model's attributes. The return value
85
+ # of this method will implement the interface described by
86
+ # ActiveModel::Type::Value (though the object itself may not subclass it).
87
+ #--
88
+ # Implemented by ActiveModel::AttributeRegistration::ClassMethods#type_for_attribute.
89
+
90
+ ##
78
91
  private
79
92
  def define_method_attribute=(canonical_name, owner:, as: canonical_name)
80
93
  ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
@@ -60,7 +60,7 @@ module ActiveModel
60
60
  # Would only create the +after_create+ and +before_create+ callback methods in
61
61
  # your class.
62
62
  #
63
- # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
63
+ # NOTE: Defining the same callback multiple times will overwrite previous callback definitions.
64
64
  #
65
65
  module Callbacks
66
66
  def self.extended(base) # :nodoc:
@@ -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.
@@ -289,22 +289,22 @@ module ActiveModel
289
289
  mutations_from_database.changed_attribute_names
290
290
  end
291
291
 
292
- # Dispatch target for {*_changed?}[rdoc-label:method-i-2A_changed-3F] attribute methods.
292
+ # Dispatch target for {*_changed?}[rdoc-ref:#*_changed?] attribute methods.
293
293
  def attribute_changed?(attr_name, **options)
294
294
  mutations_from_database.changed?(attr_name.to_s, **options)
295
295
  end
296
296
 
297
- # Dispatch target for {*_was}[rdoc-label:method-i-2A_was] attribute methods.
297
+ # Dispatch target for {*_was}[rdoc-ref:#*_was] attribute methods.
298
298
  def attribute_was(attr_name)
299
299
  mutations_from_database.original_value(attr_name.to_s)
300
300
  end
301
301
 
302
- # Dispatch target for {*_previously_changed?}[rdoc-label:method-i-2A_previously_changed-3F] attribute methods.
302
+ # Dispatch target for {*_previously_changed?}[rdoc-ref:#*_previously_changed?] attribute methods.
303
303
  def attribute_previously_changed?(attr_name, **options)
304
304
  mutations_before_last_save.changed?(attr_name.to_s, **options)
305
305
  end
306
306
 
307
- # Dispatch target for {*_previously_was}[rdoc-label:method-i-2A_previously_was] attribute methods.
307
+ # Dispatch target for {*_previously_was}[rdoc-ref:#*_previously_was] attribute methods.
308
308
  def attribute_previously_was(attr_name)
309
309
  mutations_before_last_save.original_value(attr_name.to_s)
310
310
  end
@@ -8,9 +8,9 @@ module ActiveModel
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 7
11
- MINOR = 1
12
- TINY = 5
13
- PRE = "1"
11
+ MINOR = 2
12
+ TINY = 3
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -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.
@@ -41,7 +41,7 @@ module ActiveModel
41
41
  #
42
42
  # To use +has_secure_password+, add bcrypt (~> 3.1.7) to your Gemfile:
43
43
  #
44
- # gem 'bcrypt', '~> 3.1.7'
44
+ # gem "bcrypt", "~> 3.1.7"
45
45
  #
46
46
  # ==== Examples
47
47
  #
@@ -140,7 +140,7 @@ module ActiveModel
140
140
  end
141
141
  end
142
142
 
143
- validates_confirmation_of attribute, allow_blank: true
143
+ validates_confirmation_of attribute, allow_nil: true
144
144
  end
145
145
  end
146
146
  end
@@ -50,10 +50,19 @@ module ActiveModel
50
50
  namespace, _, attribute = attribute.rpartition(".")
51
51
  namespace.tr!(".", "/")
52
52
 
53
+ if attribute.present?
54
+ key = "#{namespace}.#{attribute}"
55
+ separator = "/"
56
+ else
57
+ key = namespace
58
+ separator = "."
59
+ end
60
+
53
61
  defaults = lookup_ancestors.map do |klass|
54
- :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
62
+ :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}#{separator}#{key}"
55
63
  end
56
- defaults << :"#{i18n_scope}.attributes.#{namespace}.#{attribute}"
64
+ defaults << :"#{i18n_scope}.attributes.#{key}"
65
+ defaults << :"attributes.#{key}"
57
66
  else
58
67
  defaults = lookup_ancestors.map do |klass|
59
68
  :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}"
@@ -65,7 +74,10 @@ module ActiveModel
65
74
  defaults << MISSING_TRANSLATION
66
75
 
67
76
  translation = I18n.translate(defaults.shift, count: 1, **options, default: defaults)
68
- translation = attribute.humanize if translation == MISSING_TRANSLATION
77
+ if translation == MISSING_TRANSLATION
78
+ translation = attribute.present? ? attribute.humanize : namespace.humanize
79
+ end
80
+
69
81
  translation
70
82
  end
71
83
  end
@@ -15,14 +15,6 @@ module ActiveModel
15
15
  # attribute :weight, :float
16
16
  # end
17
17
  #
18
- # Values are cast using their +to_f+ method, except for the following
19
- # strings:
20
- #
21
- # - Blank strings are cast to +nil+.
22
- # - <tt>"Infinity"</tt> is cast to +Float::INFINITY+.
23
- # - <tt>"-Infinity"</tt> is cast to <tt>-Float::INFINITY</tt>.
24
- # - <tt>"NaN"</tt> is cast to +Float::NAN+.
25
- #
26
18
  # bag = BagOfCoffee.new
27
19
  #
28
20
  # bag.weight = "0.25"
@@ -33,6 +25,14 @@ module ActiveModel
33
25
  #
34
26
  # bag.weight = "NaN"
35
27
  # bag.weight # => Float::NAN
28
+ #
29
+ # Values are cast using their +to_f+ method, except for the following
30
+ # strings:
31
+ #
32
+ # - Blank strings are cast to +nil+.
33
+ # - <tt>"Infinity"</tt> is cast to +Float::INFINITY+.
34
+ # - <tt>"-Infinity"</tt> is cast to <tt>-Float::INFINITY</tt>.
35
+ # - <tt>"NaN"</tt> is cast to +Float::NAN+.
36
36
  class Float < Value
37
37
  include Helpers::Numeric
38
38
 
@@ -70,18 +70,33 @@ module ActiveModel
70
70
  /x
71
71
 
72
72
  if RUBY_VERSION >= "3.2"
73
- def fast_string_to_time(string)
74
- return unless ISO_DATETIME.match?(string)
75
-
76
- if is_utc?
77
- # XXX: Wrapping the Time object with Time.at because Time.new with `in:` in Ruby 3.2.0 used to return an invalid Time object
78
- # see: https://bugs.ruby-lang.org/issues/19292
79
- ::Time.at(::Time.new(string, in: "UTC"))
80
- else
81
- ::Time.new(string)
73
+ if Time.new(2000, 1, 1, 0, 0, 0, "-00:00").yday != 1 # Early 3.2.x had a bug
74
+ # BUG: Wrapping the Time object with Time.at because Time.new with `in:` in Ruby 3.2.0
75
+ # used to return an invalid Time object
76
+ # see: https://bugs.ruby-lang.org/issues/19292
77
+ def fast_string_to_time(string)
78
+ return unless string.include?("-") # Time.new("1234") # => 1234-01-01 00:00:00
79
+
80
+ if is_utc?
81
+ ::Time.at(::Time.new(string, in: "UTC"))
82
+ else
83
+ ::Time.new(string)
84
+ end
85
+ rescue ArgumentError
86
+ nil
87
+ end
88
+ else
89
+ def fast_string_to_time(string)
90
+ return unless string.include?("-") # Time.new("1234") # => 1234-01-01 00:00:00
91
+
92
+ if is_utc?
93
+ ::Time.new(string, in: "UTC")
94
+ else
95
+ ::Time.new(string)
96
+ end
97
+ rescue ArgumentError
98
+ nil
82
99
  end
83
- rescue ArgumentError
84
- nil
85
100
  end
86
101
  else
87
102
  def fast_string_to_time(string)
@@ -94,7 +109,13 @@ module ActiveModel
94
109
  end
95
110
 
96
111
  if $8
97
- offset = $8 == "Z" ? 0 : $8.to_i * 3600 + $9.to_i * 60
112
+ offset = \
113
+ if $8 == "Z"
114
+ 0
115
+ else
116
+ offset_h, offset_m = $8.to_i, $9.to_i
117
+ offset_h.to_i * 3600 + (offset_h.negative? ? -1 : 1) * offset_m * 60
118
+ end
98
119
  end
99
120
 
100
121
  new_time($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec, offset)
@@ -7,7 +7,11 @@ module ActiveModel
7
7
  module Helpers # :nodoc: all
8
8
  module Timezone
9
9
  def is_utc?
10
- ::Time.zone_default.nil? || ::Time.zone_default.match?("UTC")
10
+ if default = ::Time.zone_default
11
+ default.name == "UTC"
12
+ else
13
+ true
14
+ end
11
15
  end
12
16
 
13
17
  def default_timezone
@@ -20,16 +20,15 @@ module ActiveModel
20
20
  registrations[type_name] = block
21
21
  end
22
22
 
23
- def lookup(symbol, *args)
23
+ def lookup(symbol, ...)
24
24
  registration = registrations[symbol]
25
25
 
26
26
  if registration
27
- registration.call(symbol, *args)
27
+ registration.call(symbol, ...)
28
28
  else
29
29
  raise ArgumentError, "Unknown type #{symbol.inspect}"
30
30
  end
31
31
  end
32
- ruby2_keywords(:lookup)
33
32
 
34
33
  private
35
34
  attr_reader :registrations
@@ -101,7 +101,7 @@ module ActiveModel
101
101
  options[:on] = Array(options[:on])
102
102
  options[:if] = [
103
103
  ->(o) {
104
- !(options[:on] & Array(o.validation_context)).empty?
104
+ options[:on].intersect?(Array(o.validation_context))
105
105
  },
106
106
  *options[:if]
107
107
  ]
@@ -10,7 +10,7 @@ module ActiveModel
10
10
  include ResolveValue
11
11
 
12
12
  def check_validity!
13
- unless (options.keys & COMPARE_CHECKS.keys).any?
13
+ unless options.keys.intersect?(COMPARE_CHECKS.keys)
14
14
  raise ArgumentError, "Expected one of :greater_than, :greater_than_or_equal_to, "\
15
15
  ":equal_to, :less_than, :less_than_or_equal_to, or :other_than option to be supplied."
16
16
  end
@@ -10,7 +10,7 @@ module ActiveModel
10
10
  # validators can be overridden inside specific classes by creating
11
11
  # custom validator classes in their place such as PresenceValidator.
12
12
  #
13
- # Examples of using the default \Rails validators:
13
+ # Examples of using the default Rails validators:
14
14
  #
15
15
  # validates :username, absence: true
16
16
  # validates :terms, acceptance: true
@@ -84,7 +84,8 @@ module ActiveModel
84
84
  # end
85
85
  # end
86
86
  #
87
- # Options:
87
+ # ==== Options
88
+ #
88
89
  # * <tt>:on</tt> - Specifies the contexts where this validation is active.
89
90
  # Runs in all validation contexts by default +nil+. You can pass a symbol
90
91
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
@@ -150,7 +151,8 @@ module ActiveModel
150
151
  # Note that the return value of validation methods is not relevant.
151
152
  # It's not possible to halt the validate callback chain.
152
153
  #
153
- # Options:
154
+ # ==== Options
155
+ #
154
156
  # * <tt>:on</tt> - Specifies the contexts where this validation is active.
155
157
  # Runs in all validation contexts by default +nil+. You can pass a symbol
156
158
  # or an array of symbols. (e.g. <tt>on: :create</tt> or
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activemodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.5.1
4
+ version: 7.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - '='
18
17
  - !ruby/object:Gem::Version
19
- version: 7.1.5.1
18
+ version: 7.2.3
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - '='
25
24
  - !ruby/object:Gem::Version
26
- version: 7.1.5.1
25
+ version: 7.2.3
27
26
  description: A toolkit for building modeling frameworks like Active Record. Rich support
28
27
  for attributes, callbacks, validations, serialization, internationalization, and
29
28
  testing.
@@ -112,12 +111,11 @@ licenses:
112
111
  - MIT
113
112
  metadata:
114
113
  bug_tracker_uri: https://github.com/rails/rails/issues
115
- changelog_uri: https://github.com/rails/rails/blob/v7.1.5.1/activemodel/CHANGELOG.md
116
- documentation_uri: https://api.rubyonrails.org/v7.1.5.1/
114
+ changelog_uri: https://github.com/rails/rails/blob/v7.2.3/activemodel/CHANGELOG.md
115
+ documentation_uri: https://api.rubyonrails.org/v7.2.3/
117
116
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
118
- source_code_uri: https://github.com/rails/rails/tree/v7.1.5.1/activemodel
117
+ source_code_uri: https://github.com/rails/rails/tree/v7.2.3/activemodel
119
118
  rubygems_mfa_required: 'true'
120
- post_install_message:
121
119
  rdoc_options: []
122
120
  require_paths:
123
121
  - lib
@@ -125,15 +123,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
125
123
  requirements:
126
124
  - - ">="
127
125
  - !ruby/object:Gem::Version
128
- version: 2.7.0
126
+ version: 3.1.0
129
127
  required_rubygems_version: !ruby/object:Gem::Requirement
130
128
  requirements:
131
129
  - - ">="
132
130
  - !ruby/object:Gem::Version
133
131
  version: '0'
134
132
  requirements: []
135
- rubygems_version: 3.5.22
136
- signing_key:
133
+ rubygems_version: 3.6.9
137
134
  specification_version: 4
138
135
  summary: A toolkit for building modeling frameworks (part of Rails).
139
136
  test_files: []