activemodel 7.0.4 → 7.1.3.4

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 +162 -107
  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 +133 -102
  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 +62 -45
  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 +13 -8
@@ -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,50 @@ 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
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
230
217
 
231
- modifier = matcher.parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
218
+ def generate_alias_attribute_methods(code_generator, new_name, old_name)
219
+ attribute_method_patterns.each do |pattern|
220
+ alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
221
+ end
222
+ end
232
223
 
233
- batch <<
234
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
235
- body <<
236
- "end"
237
- end
224
+ def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
225
+ method_name = pattern.method_name(new_name).to_s
226
+ target_name = pattern.method_name(old_name).to_s
227
+ parameters = pattern.parameters
228
+ mangled_name = target_name
229
+
230
+ unless NAME_COMPILABLE_REGEXP.match?(target_name)
231
+ mangled_name = "__temp__#{target_name.unpack1("h*")}"
232
+ end
233
+
234
+ code_generator.define_cached_method(method_name, as: mangled_name, namespace: :alias_attribute) do |batch|
235
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
236
+ "self.#{target_name}(#{parameters || ''})"
237
+ else
238
+ call_args = [":'#{target_name}'"]
239
+ call_args << parameters if parameters
240
+ "send(#{call_args.join(", ")})"
238
241
  end
242
+
243
+ modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
244
+
245
+ batch <<
246
+ "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
247
+ body <<
248
+ "end"
239
249
  end
240
250
  end
241
251
 
@@ -250,7 +260,7 @@ module ActiveModel
250
260
  end
251
261
 
252
262
  # Declares the attributes that should be prefixed and suffixed by
253
- # <tt>ActiveModel::AttributeMethods</tt>.
263
+ # +ActiveModel::AttributeMethods+.
254
264
  #
255
265
  # To use, pass attribute names (as strings or symbols). Be sure to declare
256
266
  # +define_attribute_methods+ after you define any prefix, suffix, or affix
@@ -268,19 +278,23 @@ module ActiveModel
268
278
  # define_attribute_methods :name, :age, :address
269
279
  #
270
280
  # private
271
- #
272
- # def clear_attribute(attr)
273
- # send("#{attr}=", nil)
274
- # end
281
+ # def clear_attribute(attr)
282
+ # send("#{attr}=", nil)
283
+ # end
275
284
  # end
276
285
  def define_attribute_methods(*attr_names)
277
286
  ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
278
- attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
287
+ attr_names.flatten.each do |attr_name|
288
+ define_attribute_method(attr_name, _owner: owner)
289
+ aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
290
+ generate_alias_attribute_methods owner, aliased_name, attr_name
291
+ end
292
+ end
279
293
  end
280
294
  end
281
295
 
282
296
  # Declares an attribute that should be prefixed and suffixed by
283
- # <tt>ActiveModel::AttributeMethods</tt>.
297
+ # +ActiveModel::AttributeMethods+.
284
298
  #
285
299
  # To use, pass an attribute name (as string or symbol). Be sure to declare
286
300
  # +define_attribute_method+ after you define any prefix, suffix or affix
@@ -298,10 +312,9 @@ module ActiveModel
298
312
  # define_attribute_method :name
299
313
  #
300
314
  # private
301
- #
302
- # def attribute_short?(attr)
303
- # send(attr).length < 5
304
- # end
315
+ # def attribute_short?(attr)
316
+ # send(attr).length < 5
317
+ # end
305
318
  # end
306
319
  #
307
320
  # person = Person.new
@@ -310,24 +323,24 @@ module ActiveModel
310
323
  # person.name_short? # => true
311
324
  def define_attribute_method(attr_name, _owner: generated_attribute_methods)
312
325
  ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
313
- attribute_method_matchers.each do |matcher|
314
- method_name = matcher.method_name(attr_name)
326
+ attribute_method_patterns.each do |pattern|
327
+ method_name = pattern.method_name(attr_name)
315
328
 
316
329
  unless instance_method_already_implemented?(method_name)
317
- generate_method = "define_method_#{matcher.target}"
330
+ generate_method = "define_method_#{pattern.proxy_target}"
318
331
 
319
332
  if respond_to?(generate_method, true)
320
333
  send(generate_method, attr_name.to_s, owner: owner)
321
334
  else
322
- define_proxy_call(owner, method_name, matcher.target, matcher.parameters, attr_name.to_s, namespace: :active_model_proxy)
335
+ define_proxy_call(owner, method_name, pattern.proxy_target, pattern.parameters, attr_name.to_s, namespace: :active_model_proxy)
323
336
  end
324
337
  end
325
338
  end
326
- attribute_method_matchers_cache.clear
339
+ attribute_method_patterns_cache.clear
327
340
  end
328
341
  end
329
342
 
330
- # Removes all the previously dynamically defined methods from the class.
343
+ # Removes all the previously dynamically defined methods from the class, including alias attribute methods.
331
344
  #
332
345
  # class Person
333
346
  # include ActiveModel::AttributeMethods
@@ -335,29 +348,46 @@ module ActiveModel
335
348
  # attr_accessor :name
336
349
  # attribute_method_suffix '_short?'
337
350
  # define_attribute_method :name
351
+ # alias_attribute :first_name, :name
338
352
  #
339
353
  # private
340
- #
341
- # def attribute_short?(attr)
342
- # send(attr).length < 5
343
- # end
354
+ # def attribute_short?(attr)
355
+ # send(attr).length < 5
356
+ # end
344
357
  # end
345
358
  #
346
359
  # person = Person.new
347
360
  # person.name = 'Bob'
361
+ # person.first_name # => "Bob"
348
362
  # person.name_short? # => true
349
363
  #
350
364
  # Person.undefine_attribute_methods
351
365
  #
352
366
  # person.name_short? # => NoMethodError
367
+ # person.first_name # => NoMethodError
353
368
  def undefine_attribute_methods
354
369
  generated_attribute_methods.module_eval do
355
370
  undef_method(*instance_methods)
356
371
  end
357
- attribute_method_matchers_cache.clear
372
+ attribute_method_patterns_cache.clear
373
+ end
374
+
375
+ def aliases_by_attribute_name # :nodoc:
376
+ @aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
358
377
  end
359
378
 
360
379
  private
380
+ def inherited(base) # :nodoc:
381
+ super
382
+ base.class_eval do
383
+ @attribute_method_patterns_cache = nil
384
+ end
385
+ end
386
+
387
+ def resolve_attribute_name(name)
388
+ attribute_aliases.fetch(super, &:itself)
389
+ end
390
+
361
391
  def generated_attribute_methods
362
392
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
363
393
  end
@@ -375,33 +405,34 @@ module ActiveModel
375
405
  # used to alleviate the GC, which ultimately also speeds up the app
376
406
  # significantly (in our case our test suite finishes 10% faster with
377
407
  # this cache).
378
- def attribute_method_matchers_cache
379
- @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
408
+ def attribute_method_patterns_cache
409
+ @attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
380
410
  end
381
411
 
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) }
412
+ def attribute_method_patterns_matching(method_name)
413
+ attribute_method_patterns_cache.compute_if_absent(method_name) do
414
+ attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
385
415
  end
386
416
  end
387
417
 
388
418
  # Define a method `name` in `mod` that dispatches to `send`
389
419
  # using the given `extra` args. This falls back on `send`
390
420
  # if the called name cannot be compiled.
391
- def define_proxy_call(code_generator, name, target, parameters, *call_args, namespace:)
421
+ def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:)
392
422
  mangled_name = name
393
423
  unless NAME_COMPILABLE_REGEXP.match?(name)
394
424
  mangled_name = "__temp__#{name.unpack1("h*")}"
395
425
  end
396
426
 
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
427
+ call_args.map!(&:inspect)
428
+ call_args << parameters if parameters
429
+ namespace = :"#{namespace}_#{proxy_target}_#{call_args.join("_")}}"
400
430
 
401
- body = if CALL_COMPILABLE_REGEXP.match?(target)
402
- "self.#{target}(#{call_args.join(", ")})"
431
+ code_generator.define_cached_method(name, as: mangled_name, namespace: namespace) do |batch|
432
+ body = if CALL_COMPILABLE_REGEXP.match?(proxy_target)
433
+ "self.#{proxy_target}(#{call_args.join(", ")})"
403
434
  else
404
- call_args.unshift(":'#{target}'")
435
+ call_args.unshift(":'#{proxy_target}'")
405
436
  "send(#{call_args.join(", ")})"
406
437
  end
407
438
 
@@ -414,23 +445,23 @@ module ActiveModel
414
445
  end
415
446
  end
416
447
 
417
- class AttributeMethodMatcher # :nodoc:
418
- attr_reader :prefix, :suffix, :target, :parameters
448
+ class AttributeMethodPattern # :nodoc:
449
+ attr_reader :prefix, :suffix, :proxy_target, :parameters
419
450
 
420
- AttributeMethodMatch = Struct.new(:target, :attr_name)
451
+ AttributeMethod = Struct.new(:proxy_target, :attr_name)
421
452
 
422
453
  def initialize(prefix: "", suffix: "", parameters: nil)
423
454
  @prefix = prefix
424
455
  @suffix = suffix
425
456
  @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
426
- @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
427
- @target = "#{@prefix}attribute#{@suffix}"
457
+ @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
458
+ @proxy_target = "#{@prefix}attribute#{@suffix}"
428
459
  @method_name = "#{prefix}%s#{suffix}"
429
460
  end
430
461
 
431
462
  def match(method_name)
432
463
  if @regex =~ method_name
433
- AttributeMethodMatch.new(target, $1)
464
+ AttributeMethod.new(proxy_target, $1)
434
465
  end
435
466
  end
436
467
 
@@ -465,7 +496,7 @@ module ActiveModel
465
496
  # attribute method. If so, we tell +attribute_missing+ to dispatch the
466
497
  # attribute. This method can be overloaded to customize the behavior.
467
498
  def attribute_missing(match, *args, &block)
468
- __send__(match.target, match.attr_name, *args, &block)
499
+ __send__(match.proxy_target, match.attr_name, *args, &block)
469
500
  end
470
501
  ruby2_keywords(:attribute_missing)
471
502
 
@@ -493,12 +524,12 @@ module ActiveModel
493
524
  # Returns a struct representing the matching attribute method.
494
525
  # The struct's attributes are prefix, base and suffix.
495
526
  def matched_attribute_method(method_name)
496
- matches = self.class.send(:attribute_method_matchers_matching, method_name)
527
+ matches = self.class.send(:attribute_method_patterns_matching, method_name)
497
528
  matches.detect { |match| attribute_method?(match.attr_name) }
498
529
  end
499
530
 
500
531
  def missing_attribute(attr_name, stack)
501
- raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
532
+ raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
502
533
  end
503
534
 
504
535
  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