activemodel 7.0.4 → 7.1.5.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +191 -94
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +11 -11
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +5 -5
  7. data/lib/active_model/attribute/user_provided_default.rb +4 -0
  8. data/lib/active_model/attribute.rb +26 -1
  9. data/lib/active_model/attribute_assignment.rb +1 -1
  10. data/lib/active_model/attribute_methods.rb +165 -112
  11. data/lib/active_model/attribute_mutation_tracker.rb +10 -2
  12. data/lib/active_model/attribute_registration.rb +77 -0
  13. data/lib/active_model/attribute_set.rb +10 -1
  14. data/lib/active_model/attributes.rb +65 -48
  15. data/lib/active_model/callbacks.rb +6 -6
  16. data/lib/active_model/conversion.rb +14 -4
  17. data/lib/active_model/deprecator.rb +7 -0
  18. data/lib/active_model/dirty.rb +134 -13
  19. data/lib/active_model/error.rb +5 -4
  20. data/lib/active_model/errors.rb +37 -6
  21. data/lib/active_model/forbidden_attributes_protection.rb +2 -0
  22. data/lib/active_model/gem_version.rb +4 -4
  23. data/lib/active_model/lint.rb +1 -1
  24. data/lib/active_model/locale/en.yml +1 -0
  25. data/lib/active_model/model.rb +34 -2
  26. data/lib/active_model/naming.rb +29 -10
  27. data/lib/active_model/railtie.rb +4 -0
  28. data/lib/active_model/secure_password.rb +61 -23
  29. data/lib/active_model/serialization.rb +3 -3
  30. data/lib/active_model/serializers/json.rb +1 -1
  31. data/lib/active_model/translation.rb +18 -16
  32. data/lib/active_model/type/big_integer.rb +23 -1
  33. data/lib/active_model/type/binary.rb +13 -3
  34. data/lib/active_model/type/boolean.rb +11 -9
  35. data/lib/active_model/type/date.rb +28 -2
  36. data/lib/active_model/type/date_time.rb +45 -3
  37. data/lib/active_model/type/decimal.rb +39 -1
  38. data/lib/active_model/type/float.rb +30 -1
  39. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +5 -1
  40. data/lib/active_model/type/helpers/mutable.rb +4 -0
  41. data/lib/active_model/type/helpers/numeric.rb +6 -1
  42. data/lib/active_model/type/helpers/time_value.rb +28 -12
  43. data/lib/active_model/type/immutable_string.rb +37 -1
  44. data/lib/active_model/type/integer.rb +44 -1
  45. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  46. data/lib/active_model/type/string.rb +9 -1
  47. data/lib/active_model/type/time.rb +48 -7
  48. data/lib/active_model/type/value.rb +25 -1
  49. data/lib/active_model/type.rb +1 -0
  50. data/lib/active_model/validations/absence.rb +1 -1
  51. data/lib/active_model/validations/acceptance.rb +1 -1
  52. data/lib/active_model/validations/callbacks.rb +10 -12
  53. data/lib/active_model/validations/clusivity.rb +5 -8
  54. data/lib/active_model/validations/comparability.rb +0 -11
  55. data/lib/active_model/validations/comparison.rb +15 -7
  56. data/lib/active_model/validations/format.rb +6 -7
  57. data/lib/active_model/validations/length.rb +13 -12
  58. data/lib/active_model/validations/numericality.rb +35 -23
  59. data/lib/active_model/validations/presence.rb +1 -1
  60. data/lib/active_model/validations/resolve_value.rb +26 -0
  61. data/lib/active_model/validations/validates.rb +5 -5
  62. data/lib/active_model/validations/with.rb +9 -2
  63. data/lib/active_model/validations.rb +44 -9
  64. data/lib/active_model/validator.rb +7 -5
  65. data/lib/active_model/version.rb +1 -1
  66. data/lib/active_model.rb +6 -1
  67. metadata +16 -11
@@ -11,17 +11,17 @@ module ActiveModel
11
11
  #
12
12
  # user = User.first
13
13
  # user.pets.select(:id).first.user_id
14
- # # => ActiveModel::MissingAttributeError: missing attribute: user_id
14
+ # # => ActiveModel::MissingAttributeError: missing attribute 'user_id' for Pet
15
15
  class MissingAttributeError < NoMethodError
16
16
  end
17
17
 
18
- # == Active \Model \Attribute \Methods
18
+ # = Active \Model \Attribute \Methods
19
19
  #
20
20
  # Provides a way to add prefixes and suffixes to your methods as
21
- # well as handling the creation of <tt>ActiveRecord::Base</tt>-like
21
+ # well as handling the creation of ActiveRecord::Base - like
22
22
  # class methods such as +table_name+.
23
23
  #
24
- # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
24
+ # The requirements to implement +ActiveModel::AttributeMethods+ are to:
25
25
  #
26
26
  # * <tt>include ActiveModel::AttributeMethods</tt> in your class.
27
27
  # * Call each of its methods you want to add, such as +attribute_method_suffix+
@@ -49,18 +49,17 @@ module ActiveModel
49
49
  # end
50
50
  #
51
51
  # private
52
+ # def attribute_contrived?(attr)
53
+ # true
54
+ # end
52
55
  #
53
- # def attribute_contrived?(attr)
54
- # true
55
- # end
56
- #
57
- # def clear_attribute(attr)
58
- # send("#{attr}=", nil)
59
- # end
56
+ # def clear_attribute(attr)
57
+ # send("#{attr}=", nil)
58
+ # end
60
59
  #
61
- # def reset_attribute_to_default!(attr)
62
- # send("#{attr}=", 'Default Name')
63
- # end
60
+ # def reset_attribute_to_default!(attr)
61
+ # send("#{attr}=", 'Default Name')
62
+ # end
64
63
  # end
65
64
  module AttributeMethods
66
65
  extend ActiveSupport::Concern
@@ -71,7 +70,7 @@ module ActiveModel
71
70
 
72
71
  included do
73
72
  class_attribute :attribute_aliases, instance_writer: false, default: {}
74
- class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
73
+ class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]
75
74
  end
76
75
 
77
76
  module ClassMethods
@@ -95,10 +94,9 @@ module ActiveModel
95
94
  # define_attribute_methods :name
96
95
  #
97
96
  # private
98
- #
99
- # def clear_attribute(attr)
100
- # send("#{attr}=", nil)
101
- # end
97
+ # def clear_attribute(attr)
98
+ # send("#{attr}=", nil)
99
+ # end
102
100
  # end
103
101
  #
104
102
  # person = Person.new
@@ -107,7 +105,7 @@ module ActiveModel
107
105
  # person.clear_name
108
106
  # person.name # => nil
109
107
  def attribute_method_prefix(*prefixes, parameters: nil)
110
- self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new(prefix: prefix, parameters: parameters) }
108
+ self.attribute_method_patterns += prefixes.map! { |prefix| AttributeMethodPattern.new(prefix: prefix, parameters: parameters) }
111
109
  undefine_attribute_methods
112
110
  end
113
111
 
@@ -131,10 +129,9 @@ module ActiveModel
131
129
  # define_attribute_methods :name
132
130
  #
133
131
  # private
134
- #
135
- # def attribute_short?(attr)
136
- # send(attr).length < 5
137
- # end
132
+ # def attribute_short?(attr)
133
+ # send(attr).length < 5
134
+ # end
138
135
  # end
139
136
  #
140
137
  # person = Person.new
@@ -142,7 +139,7 @@ module ActiveModel
142
139
  # person.name # => "Bob"
143
140
  # person.name_short? # => true
144
141
  def attribute_method_suffix(*suffixes, parameters: nil)
145
- self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new(suffix: suffix, parameters: parameters) }
142
+ self.attribute_method_patterns += suffixes.map! { |suffix| AttributeMethodPattern.new(suffix: suffix, parameters: parameters) }
146
143
  undefine_attribute_methods
147
144
  end
148
145
 
@@ -167,10 +164,9 @@ module ActiveModel
167
164
  # define_attribute_methods :name
168
165
  #
169
166
  # private
170
- #
171
- # def reset_attribute_to_default!(attr)
172
- # send("#{attr}=", 'Default Name')
173
- # end
167
+ # def reset_attribute_to_default!(attr)
168
+ # send("#{attr}=", 'Default Name')
169
+ # end
174
170
  # end
175
171
  #
176
172
  # person = Person.new
@@ -178,7 +174,7 @@ module ActiveModel
178
174
  # person.reset_name_to_default!
179
175
  # person.name # => 'Default Name'
180
176
  def attribute_method_affix(*affixes)
181
- self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new(**affix) }
177
+ self.attribute_method_patterns += affixes.map! { |affix| AttributeMethodPattern.new(**affix) }
182
178
  undefine_attribute_methods
183
179
  end
184
180
 
@@ -194,10 +190,9 @@ module ActiveModel
194
190
  # alias_attribute :nickname, :name
195
191
  #
196
192
  # private
197
- #
198
- # def attribute_short?(attr)
199
- # send(attr).length < 5
200
- # end
193
+ # def attribute_short?(attr)
194
+ # send(attr).length < 5
195
+ # end
201
196
  # end
202
197
  #
203
198
  # person = Person.new
@@ -207,35 +202,53 @@ module ActiveModel
207
202
  # person.name_short? # => true
208
203
  # person.nickname_short? # => true
209
204
  def alias_attribute(new_name, old_name)
210
- self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
211
- ActiveSupport::CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
212
- attribute_method_matchers.each do |matcher|
213
- method_name = matcher.method_name(new_name).to_s
214
- target_name = matcher.method_name(old_name).to_s
215
- parameters = matcher.parameters
216
-
217
- mangled_name = target_name
218
- unless NAME_COMPILABLE_REGEXP.match?(target_name)
219
- mangled_name = "__temp__#{target_name.unpack1("h*")}"
220
- end
205
+ old_name = old_name.to_s
206
+ new_name = new_name.to_s
207
+ self.attribute_aliases = attribute_aliases.merge(new_name => old_name)
208
+ aliases_by_attribute_name[old_name] << new_name
209
+ eagerly_generate_alias_attribute_methods(new_name, old_name)
210
+ end
221
211
 
222
- code_generator.define_cached_method(method_name, as: mangled_name, namespace: :alias_attribute) do |batch|
223
- body = if CALL_COMPILABLE_REGEXP.match?(target_name)
224
- "self.#{target_name}(#{parameters || ''})"
225
- else
226
- call_args = [":'#{target_name}'"]
227
- call_args << parameters if parameters
228
- "send(#{call_args.join(", ")})"
229
- end
230
-
231
- modifier = matcher.parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
232
-
233
- batch <<
234
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
235
- body <<
236
- "end"
237
- end
212
+ def eagerly_generate_alias_attribute_methods(new_name, old_name) # :nodoc:
213
+ ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |code_generator|
214
+ generate_alias_attribute_methods(code_generator, new_name, old_name)
215
+ end
216
+ end
217
+
218
+ def generate_alias_attribute_methods(code_generator, new_name, old_name) # :nodoc:
219
+ ActiveSupport::CodeGenerator.batch(code_generator, __FILE__, __LINE__) do |owner|
220
+ attribute_method_patterns.each do |pattern|
221
+ alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
222
+ end
223
+ attribute_method_patterns_cache.clear
224
+ end
225
+ end
226
+
227
+ def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
228
+ method_name = pattern.method_name(new_name).to_s
229
+ target_name = pattern.method_name(old_name).to_s
230
+ parameters = pattern.parameters
231
+ mangled_name = target_name
232
+
233
+ unless NAME_COMPILABLE_REGEXP.match?(target_name)
234
+ mangled_name = "__temp__#{target_name.unpack1("h*")}"
235
+ end
236
+
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(", ")})"
238
244
  end
245
+
246
+ modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
247
+
248
+ batch <<
249
+ "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
250
+ body <<
251
+ "end"
239
252
  end
240
253
  end
241
254
 
@@ -250,7 +263,7 @@ module ActiveModel
250
263
  end
251
264
 
252
265
  # Declares the attributes that should be prefixed and suffixed by
253
- # <tt>ActiveModel::AttributeMethods</tt>.
266
+ # +ActiveModel::AttributeMethods+.
254
267
  #
255
268
  # To use, pass attribute names (as strings or symbols). Be sure to declare
256
269
  # +define_attribute_methods+ after you define any prefix, suffix, or affix
@@ -268,19 +281,23 @@ module ActiveModel
268
281
  # define_attribute_methods :name, :age, :address
269
282
  #
270
283
  # private
271
- #
272
- # def clear_attribute(attr)
273
- # send("#{attr}=", nil)
274
- # end
284
+ # def clear_attribute(attr)
285
+ # send("#{attr}=", nil)
286
+ # end
275
287
  # end
276
288
  def define_attribute_methods(*attr_names)
277
289
  ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
278
- attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
290
+ attr_names.flatten.each do |attr_name|
291
+ define_attribute_method(attr_name, _owner: owner)
292
+ aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
293
+ generate_alias_attribute_methods owner, aliased_name, attr_name
294
+ end
295
+ end
279
296
  end
280
297
  end
281
298
 
282
299
  # Declares an attribute that should be prefixed and suffixed by
283
- # <tt>ActiveModel::AttributeMethods</tt>.
300
+ # +ActiveModel::AttributeMethods+.
284
301
  #
285
302
  # To use, pass an attribute name (as string or symbol). Be sure to declare
286
303
  # +define_attribute_method+ after you define any prefix, suffix or affix
@@ -298,36 +315,54 @@ module ActiveModel
298
315
  # define_attribute_method :name
299
316
  #
300
317
  # private
301
- #
302
- # def attribute_short?(attr)
303
- # send(attr).length < 5
304
- # end
318
+ # def attribute_short?(attr)
319
+ # send(attr).length < 5
320
+ # end
305
321
  # end
306
322
  #
307
323
  # person = Person.new
308
324
  # person.name = 'Bob'
309
325
  # person.name # => "Bob"
310
326
  # person.name_short? # => true
311
- def define_attribute_method(attr_name, _owner: generated_attribute_methods)
327
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
312
328
  ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
313
- attribute_method_matchers.each do |matcher|
314
- method_name = matcher.method_name(attr_name)
329
+ attribute_method_patterns.each do |pattern|
330
+ define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
331
+ end
332
+ attribute_method_patterns_cache.clear
333
+ end
334
+ end
315
335
 
316
- unless instance_method_already_implemented?(method_name)
317
- generate_method = "define_method_#{matcher.target}"
336
+ def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
337
+ canonical_method_name = pattern.method_name(attr_name)
338
+ public_method_name = pattern.method_name(as)
339
+
340
+ # If defining a regular attribute method, we don't override methods that are explictly
341
+ # defined in parrent classes.
342
+ if instance_method_already_implemented?(public_method_name)
343
+ # However, for `alias_attribute`, we always define the method.
344
+ # We check for override second because `instance_method_already_implemented?`
345
+ # also check for dangerous methods.
346
+ return unless override
347
+ end
318
348
 
319
- if respond_to?(generate_method, true)
320
- send(generate_method, attr_name.to_s, owner: owner)
321
- else
322
- define_proxy_call(owner, method_name, matcher.target, matcher.parameters, attr_name.to_s, namespace: :active_model_proxy)
323
- end
324
- end
325
- end
326
- attribute_method_matchers_cache.clear
349
+ generate_method = "define_method_#{pattern.proxy_target}"
350
+ if respond_to?(generate_method, true)
351
+ send(generate_method, attr_name.to_s, owner: owner, as: as)
352
+ else
353
+ define_proxy_call(
354
+ owner,
355
+ canonical_method_name,
356
+ pattern.proxy_target,
357
+ pattern.parameters,
358
+ attr_name.to_s,
359
+ namespace: :active_model_proxy,
360
+ as: public_method_name,
361
+ )
327
362
  end
328
363
  end
329
364
 
330
- # Removes all the previously dynamically defined methods from the class.
365
+ # Removes all the previously dynamically defined methods from the class, including alias attribute methods.
331
366
  #
332
367
  # class Person
333
368
  # include ActiveModel::AttributeMethods
@@ -335,29 +370,46 @@ module ActiveModel
335
370
  # attr_accessor :name
336
371
  # attribute_method_suffix '_short?'
337
372
  # define_attribute_method :name
373
+ # alias_attribute :first_name, :name
338
374
  #
339
375
  # private
340
- #
341
- # def attribute_short?(attr)
342
- # send(attr).length < 5
343
- # end
376
+ # def attribute_short?(attr)
377
+ # send(attr).length < 5
378
+ # end
344
379
  # end
345
380
  #
346
381
  # person = Person.new
347
382
  # person.name = 'Bob'
383
+ # person.first_name # => "Bob"
348
384
  # person.name_short? # => true
349
385
  #
350
386
  # Person.undefine_attribute_methods
351
387
  #
352
388
  # person.name_short? # => NoMethodError
389
+ # person.first_name # => NoMethodError
353
390
  def undefine_attribute_methods
354
391
  generated_attribute_methods.module_eval do
355
392
  undef_method(*instance_methods)
356
393
  end
357
- attribute_method_matchers_cache.clear
394
+ attribute_method_patterns_cache.clear
395
+ end
396
+
397
+ def aliases_by_attribute_name # :nodoc:
398
+ @aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
358
399
  end
359
400
 
360
401
  private
402
+ def inherited(base) # :nodoc:
403
+ super
404
+ base.class_eval do
405
+ @attribute_method_patterns_cache = nil
406
+ end
407
+ end
408
+
409
+ def resolve_attribute_name(name)
410
+ attribute_aliases.fetch(super, &:itself)
411
+ end
412
+
361
413
  def generated_attribute_methods
362
414
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
363
415
  end
@@ -375,33 +427,34 @@ module ActiveModel
375
427
  # used to alleviate the GC, which ultimately also speeds up the app
376
428
  # significantly (in our case our test suite finishes 10% faster with
377
429
  # this cache).
378
- def attribute_method_matchers_cache
379
- @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
430
+ def attribute_method_patterns_cache
431
+ @attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
380
432
  end
381
433
 
382
- def attribute_method_matchers_matching(method_name)
383
- attribute_method_matchers_cache.compute_if_absent(method_name) do
384
- attribute_method_matchers.filter_map { |matcher| matcher.match(method_name) }
434
+ def attribute_method_patterns_matching(method_name)
435
+ attribute_method_patterns_cache.compute_if_absent(method_name) do
436
+ attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
385
437
  end
386
438
  end
387
439
 
388
440
  # Define a method `name` in `mod` that dispatches to `send`
389
441
  # using the given `extra` args. This falls back on `send`
390
442
  # if the called name cannot be compiled.
391
- def define_proxy_call(code_generator, name, target, parameters, *call_args, namespace:)
443
+ def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
392
444
  mangled_name = name
393
445
  unless NAME_COMPILABLE_REGEXP.match?(name)
394
446
  mangled_name = "__temp__#{name.unpack1("h*")}"
395
447
  end
396
448
 
397
- code_generator.define_cached_method(name, as: mangled_name, namespace: :"#{namespace}_#{target}") do |batch|
398
- call_args.map!(&:inspect)
399
- call_args << parameters if parameters
449
+ call_args.map!(&:inspect)
450
+ call_args << parameters if parameters
451
+ namespace = :"#{namespace}_#{proxy_target}"
400
452
 
401
- body = if CALL_COMPILABLE_REGEXP.match?(target)
402
- "self.#{target}(#{call_args.join(", ")})"
453
+ 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(", ")})"
403
456
  else
404
- call_args.unshift(":'#{target}'")
457
+ call_args.unshift(":'#{proxy_target}'")
405
458
  "send(#{call_args.join(", ")})"
406
459
  end
407
460
 
@@ -414,23 +467,23 @@ module ActiveModel
414
467
  end
415
468
  end
416
469
 
417
- class AttributeMethodMatcher # :nodoc:
418
- attr_reader :prefix, :suffix, :target, :parameters
470
+ class AttributeMethodPattern # :nodoc:
471
+ attr_reader :prefix, :suffix, :proxy_target, :parameters
419
472
 
420
- AttributeMethodMatch = Struct.new(:target, :attr_name)
473
+ AttributeMethod = Struct.new(:proxy_target, :attr_name)
421
474
 
422
475
  def initialize(prefix: "", suffix: "", parameters: nil)
423
476
  @prefix = prefix
424
477
  @suffix = suffix
425
478
  @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
426
- @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
427
- @target = "#{@prefix}attribute#{@suffix}"
479
+ @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
480
+ @proxy_target = "#{@prefix}attribute#{@suffix}"
428
481
  @method_name = "#{prefix}%s#{suffix}"
429
482
  end
430
483
 
431
484
  def match(method_name)
432
485
  if @regex =~ method_name
433
- AttributeMethodMatch.new(target, $1)
486
+ AttributeMethod.new(proxy_target, $1)
434
487
  end
435
488
  end
436
489
 
@@ -465,7 +518,7 @@ module ActiveModel
465
518
  # attribute method. If so, we tell +attribute_missing+ to dispatch the
466
519
  # attribute. This method can be overloaded to customize the behavior.
467
520
  def attribute_missing(match, *args, &block)
468
- __send__(match.target, match.attr_name, *args, &block)
521
+ __send__(match.proxy_target, match.attr_name, *args, &block)
469
522
  end
470
523
  ruby2_keywords(:attribute_missing)
471
524
 
@@ -493,12 +546,12 @@ module ActiveModel
493
546
  # Returns a struct representing the matching attribute method.
494
547
  # The struct's attributes are prefix, base and suffix.
495
548
  def matched_attribute_method(method_name)
496
- matches = self.class.send(:attribute_method_matchers_matching, method_name)
549
+ matches = self.class.send(:attribute_method_patterns_matching, method_name)
497
550
  matches.detect { |match| attribute_method?(match.attr_name) }
498
551
  end
499
552
 
500
553
  def missing_attribute(attr_name, stack)
501
- raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
554
+ raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
502
555
  end
503
556
 
504
557
  def _read_attribute(attr)
@@ -43,8 +43,8 @@ module ActiveModel
43
43
 
44
44
  def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
45
45
  attribute_changed?(attr_name) &&
46
- (OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
47
- (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
46
+ (OPTION_NOT_GIVEN == from || original_value(attr_name) == type_cast(attr_name, from)) &&
47
+ (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == type_cast(attr_name, to))
48
48
  end
49
49
 
50
50
  def changed_in_place?(attr_name)
@@ -82,6 +82,10 @@ module ActiveModel
82
82
  def fetch_value(attr_name)
83
83
  attributes.fetch_value(attr_name)
84
84
  end
85
+
86
+ def type_cast(attr_name, value)
87
+ attributes[attr_name].type_cast(value)
88
+ end
85
89
  end
86
90
 
87
91
  class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
@@ -143,6 +147,10 @@ module ActiveModel
143
147
  rescue TypeError, NoMethodError
144
148
  value
145
149
  end
150
+
151
+ def type_cast(attr_name, value)
152
+ value
153
+ end
146
154
  end
147
155
 
148
156
  class NullMutationTracker # :nodoc:
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/class/subclasses"
4
+ require "active_model/attribute_set"
5
+ require "active_model/attribute/user_provided_default"
6
+
7
+ module ActiveModel
8
+ module AttributeRegistration # :nodoc:
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods # :nodoc:
12
+ def attribute(name, type = nil, default: (no_default = true), **options)
13
+ type = resolve_type_name(type, **options) if type.is_a?(Symbol)
14
+
15
+ pending = pending_attribute(name)
16
+ pending.type = type if type
17
+ pending.default = default unless no_default
18
+
19
+ reset_default_attributes
20
+ end
21
+
22
+ def _default_attributes # :nodoc:
23
+ @default_attributes ||= build_default_attributes
24
+ end
25
+
26
+ def attribute_types # :nodoc:
27
+ @attribute_types ||= _default_attributes.cast_types.tap do |hash|
28
+ hash.default = Type.default_value
29
+ end
30
+ end
31
+
32
+ private
33
+ class PendingAttribute # :nodoc:
34
+ attr_accessor :type, :default
35
+
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
40
+ end
41
+ end
42
+
43
+ def pending_attribute(name)
44
+ @pending_attributes ||= {}
45
+ @pending_attributes[resolve_attribute_name(name)] ||= PendingAttribute.new
46
+ end
47
+
48
+ def apply_pending_attributes(attribute_set)
49
+ superclass.send(__method__, attribute_set) if superclass.respond_to?(__method__, true)
50
+
51
+ defined?(@pending_attributes) && @pending_attributes.each do |name, pending|
52
+ attribute_set[name] = pending.apply_to(attribute_set[name])
53
+ end
54
+
55
+ attribute_set
56
+ end
57
+
58
+ def build_default_attributes
59
+ apply_pending_attributes(AttributeSet.new({}))
60
+ end
61
+
62
+ def reset_default_attributes
63
+ @default_attributes = nil
64
+ @attribute_types = nil
65
+ subclasses.each { |subclass| subclass.send(__method__) }
66
+ end
67
+
68
+ def resolve_attribute_name(name)
69
+ name.to_s
70
+ end
71
+
72
+ def resolve_type_name(name, **options)
73
+ Type.lookup(name, **options)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -21,6 +21,10 @@ module ActiveModel
21
21
  @attributes[name] = value
22
22
  end
23
23
 
24
+ def cast_types
25
+ attributes.transform_values(&:type)
26
+ end
27
+
24
28
  def values_before_type_cast
25
29
  attributes.transform_values(&:value_before_type_cast)
26
30
  end
@@ -37,6 +41,7 @@ module ActiveModel
37
41
  def key?(name)
38
42
  attributes.key?(name) && self[name].initialized?
39
43
  end
44
+ alias :include? :key?
40
45
 
41
46
  def keys
42
47
  attributes.each_key.select { |name| self[name].initialized? }
@@ -94,8 +99,12 @@ module ActiveModel
94
99
  AttributeSet.new(new_attributes)
95
100
  end
96
101
 
102
+ def reverse_merge!(target_attributes)
103
+ attributes.reverse_merge!(target_attributes.attributes) && self
104
+ end
105
+
97
106
  def ==(other)
98
- attributes == other.attributes
107
+ other.is_a?(AttributeSet) && attributes == other.send(:attributes)
99
108
  end
100
109
 
101
110
  protected