activemodel 7.0.8 → 7.2.0

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 +16 -256
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +18 -18
  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 +27 -2
  9. data/lib/active_model/attribute_assignment.rb +4 -2
  10. data/lib/active_model/attribute_methods.rb +140 -85
  11. data/lib/active_model/attribute_registration.rb +117 -0
  12. data/lib/active_model/attribute_set.rb +10 -1
  13. data/lib/active_model/attributes.rb +78 -48
  14. data/lib/active_model/callbacks.rb +6 -6
  15. data/lib/active_model/conversion.rb +14 -4
  16. data/lib/active_model/deprecator.rb +7 -0
  17. data/lib/active_model/dirty.rb +134 -13
  18. data/lib/active_model/error.rb +4 -3
  19. data/lib/active_model/errors.rb +37 -6
  20. data/lib/active_model/forbidden_attributes_protection.rb +2 -0
  21. data/lib/active_model/gem_version.rb +3 -3
  22. data/lib/active_model/lint.rb +1 -1
  23. data/lib/active_model/locale/en.yml +1 -0
  24. data/lib/active_model/model.rb +34 -2
  25. data/lib/active_model/naming.rb +29 -10
  26. data/lib/active_model/railtie.rb +4 -0
  27. data/lib/active_model/secure_password.rb +62 -24
  28. data/lib/active_model/serialization.rb +3 -3
  29. data/lib/active_model/serializers/json.rb +1 -1
  30. data/lib/active_model/translation.rb +18 -16
  31. data/lib/active_model/type/big_integer.rb +23 -1
  32. data/lib/active_model/type/binary.rb +7 -1
  33. data/lib/active_model/type/boolean.rb +11 -9
  34. data/lib/active_model/type/date.rb +28 -2
  35. data/lib/active_model/type/date_time.rb +45 -3
  36. data/lib/active_model/type/decimal.rb +39 -1
  37. data/lib/active_model/type/float.rb +30 -1
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +5 -1
  39. data/lib/active_model/type/helpers/numeric.rb +6 -1
  40. data/lib/active_model/type/helpers/time_value.rb +50 -13
  41. data/lib/active_model/type/helpers/timezone.rb +5 -1
  42. data/lib/active_model/type/immutable_string.rb +37 -1
  43. data/lib/active_model/type/integer.rb +44 -1
  44. data/lib/active_model/type/registry.rb +2 -3
  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 +17 -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 +5 -5
  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 +16 -8
  56. data/lib/active_model/validations/format.rb +6 -7
  57. data/lib/active_model/validations/length.rb +10 -8
  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 +4 -4
  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 +5 -1
  67. metadata +14 -9
@@ -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+
@@ -66,11 +66,10 @@ 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: {}
73
- class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
72
+ class_attribute :attribute_method_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ]
74
73
  end
75
74
 
76
75
  module ClassMethods
@@ -105,7 +104,7 @@ module ActiveModel
105
104
  # person.clear_name
106
105
  # person.name # => nil
107
106
  def attribute_method_prefix(*prefixes, parameters: nil)
108
- self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new(prefix: prefix, parameters: parameters) }
107
+ self.attribute_method_patterns += prefixes.map! { |prefix| AttributeMethodPattern.new(prefix: prefix, parameters: parameters) }
109
108
  undefine_attribute_methods
110
109
  end
111
110
 
@@ -139,7 +138,7 @@ module ActiveModel
139
138
  # person.name # => "Bob"
140
139
  # person.name_short? # => true
141
140
  def attribute_method_suffix(*suffixes, parameters: nil)
142
- self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new(suffix: suffix, parameters: parameters) }
141
+ self.attribute_method_patterns += suffixes.map! { |suffix| AttributeMethodPattern.new(suffix: suffix, parameters: parameters) }
143
142
  undefine_attribute_methods
144
143
  end
145
144
 
@@ -174,7 +173,7 @@ module ActiveModel
174
173
  # person.reset_name_to_default!
175
174
  # person.name # => 'Default Name'
176
175
  def attribute_method_affix(*affixes)
177
- self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new(**affix) }
176
+ self.attribute_method_patterns += affixes.map! { |affix| AttributeMethodPattern.new(**affix) }
178
177
  undefine_attribute_methods
179
178
  end
180
179
 
@@ -202,38 +201,36 @@ module ActiveModel
202
201
  # person.name_short? # => true
203
202
  # person.nickname_short? # => true
204
203
  def alias_attribute(new_name, old_name)
205
- self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
206
- ActiveSupport::CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
207
- attribute_method_matchers.each do |matcher|
208
- method_name = matcher.method_name(new_name).to_s
209
- target_name = matcher.method_name(old_name).to_s
210
- parameters = matcher.parameters
211
-
212
- mangled_name = target_name
213
- unless NAME_COMPILABLE_REGEXP.match?(target_name)
214
- mangled_name = "__temp__#{target_name.unpack1("h*")}"
215
- end
204
+ old_name = old_name.to_s
205
+ new_name = new_name.to_s
206
+ self.attribute_aliases = attribute_aliases.merge(new_name => old_name)
207
+ aliases_by_attribute_name[old_name] << new_name
208
+ eagerly_generate_alias_attribute_methods(new_name, old_name)
209
+ end
216
210
 
217
- code_generator.define_cached_method(method_name, as: mangled_name, namespace: :alias_attribute) do |batch|
218
- body = if CALL_COMPILABLE_REGEXP.match?(target_name)
219
- "self.#{target_name}(#{parameters || ''})"
220
- else
221
- call_args = [":'#{target_name}'"]
222
- call_args << parameters if parameters
223
- "send(#{call_args.join(", ")})"
224
- end
225
-
226
- modifier = matcher.parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
227
-
228
- batch <<
229
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
230
- body <<
231
- "end"
232
- end
233
- end
211
+ def eagerly_generate_alias_attribute_methods(new_name, old_name) # :nodoc:
212
+ ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |code_generator|
213
+ generate_alias_attribute_methods(code_generator, new_name, old_name)
234
214
  end
235
215
  end
236
216
 
217
+ def generate_alias_attribute_methods(code_generator, new_name, old_name)
218
+ define_attribute_method(old_name, _owner: code_generator, as: new_name)
219
+ end
220
+
221
+ def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
222
+ method_name = pattern.method_name(new_name).to_s
223
+ target_name = pattern.method_name(old_name).to_s
224
+ parameters = pattern.parameters
225
+
226
+ mangled_name = build_mangled_name(target_name)
227
+
228
+ call_args = []
229
+ call_args << parameters if parameters
230
+
231
+ define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute)
232
+ end
233
+
237
234
  # Is +new_name+ an alias?
238
235
  def attribute_alias?(new_name)
239
236
  attribute_aliases.key? new_name.to_s
@@ -245,7 +242,7 @@ module ActiveModel
245
242
  end
246
243
 
247
244
  # Declares the attributes that should be prefixed and suffixed by
248
- # <tt>ActiveModel::AttributeMethods</tt>.
245
+ # +ActiveModel::AttributeMethods+.
249
246
  #
250
247
  # To use, pass attribute names (as strings or symbols). Be sure to declare
251
248
  # +define_attribute_methods+ after you define any prefix, suffix, or affix
@@ -269,12 +266,17 @@ module ActiveModel
269
266
  # end
270
267
  def define_attribute_methods(*attr_names)
271
268
  ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
272
- attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
269
+ attr_names.flatten.each do |attr_name|
270
+ define_attribute_method(attr_name, _owner: owner)
271
+ aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
272
+ generate_alias_attribute_methods owner, aliased_name, attr_name
273
+ end
274
+ end
273
275
  end
274
276
  end
275
277
 
276
278
  # Declares an attribute that should be prefixed and suffixed by
277
- # <tt>ActiveModel::AttributeMethods</tt>.
279
+ # +ActiveModel::AttributeMethods+.
278
280
  #
279
281
  # To use, pass an attribute name (as string or symbol). Be sure to declare
280
282
  # +define_attribute_method+ after you define any prefix, suffix or affix
@@ -301,26 +303,46 @@ module ActiveModel
301
303
  # person.name = 'Bob'
302
304
  # person.name # => "Bob"
303
305
  # person.name_short? # => true
304
- def define_attribute_method(attr_name, _owner: generated_attribute_methods)
306
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
305
307
  ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
306
- attribute_method_matchers.each do |matcher|
307
- method_name = matcher.method_name(attr_name)
308
+ attribute_method_patterns.each do |pattern|
309
+ define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
310
+ end
311
+ attribute_method_patterns_cache.clear
312
+ end
313
+ end
308
314
 
309
- unless instance_method_already_implemented?(method_name)
310
- generate_method = "define_method_#{matcher.target}"
315
+ def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
316
+ canonical_method_name = pattern.method_name(attr_name)
317
+ public_method_name = pattern.method_name(as)
318
+
319
+ # If defining a regular attribute method, we don't override methods that are explictly
320
+ # defined in parrent classes.
321
+ if instance_method_already_implemented?(public_method_name)
322
+ # However, for `alias_attribute`, we always define the method.
323
+ # We check for override second because `instance_method_already_implemented?`
324
+ # also check for dangerous methods.
325
+ return unless override
326
+ end
311
327
 
312
- if respond_to?(generate_method, true)
313
- send(generate_method, attr_name.to_s, owner: owner)
314
- else
315
- define_proxy_call(owner, method_name, matcher.target, matcher.parameters, attr_name.to_s, namespace: :active_model_proxy)
316
- end
317
- end
318
- end
319
- attribute_method_matchers_cache.clear
328
+ generate_method = "define_method_#{pattern.proxy_target}"
329
+
330
+ if respond_to?(generate_method, true)
331
+ send(generate_method, attr_name.to_s, owner: owner, as: as)
332
+ else
333
+ define_proxy_call(
334
+ owner,
335
+ canonical_method_name,
336
+ pattern.proxy_target,
337
+ pattern.parameters,
338
+ attr_name.to_s,
339
+ namespace: :active_model_proxy,
340
+ as: public_method_name
341
+ )
320
342
  end
321
343
  end
322
344
 
323
- # Removes all the previously dynamically defined methods from the class.
345
+ # Removes all the previously dynamically defined methods from the class, including alias attribute methods.
324
346
  #
325
347
  # class Person
326
348
  # include ActiveModel::AttributeMethods
@@ -328,6 +350,7 @@ module ActiveModel
328
350
  # attr_accessor :name
329
351
  # attribute_method_suffix '_short?'
330
352
  # define_attribute_method :name
353
+ # alias_attribute :first_name, :name
331
354
  #
332
355
  # private
333
356
  # def attribute_short?(attr)
@@ -337,19 +360,38 @@ module ActiveModel
337
360
  #
338
361
  # person = Person.new
339
362
  # person.name = 'Bob'
363
+ # person.first_name # => "Bob"
340
364
  # person.name_short? # => true
341
365
  #
342
366
  # Person.undefine_attribute_methods
343
367
  #
344
368
  # person.name_short? # => NoMethodError
369
+ # person.first_name # => NoMethodError
345
370
  def undefine_attribute_methods
346
371
  generated_attribute_methods.module_eval do
347
372
  undef_method(*instance_methods)
348
373
  end
349
- attribute_method_matchers_cache.clear
374
+ attribute_method_patterns_cache.clear
375
+ end
376
+
377
+ def aliases_by_attribute_name # :nodoc:
378
+ @aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
350
379
  end
351
380
 
352
381
  private
382
+ def inherited(base) # :nodoc:
383
+ super
384
+ base.class_eval do
385
+ @attribute_method_patterns_cache = nil
386
+ @aliases_by_attribute_name = nil
387
+ @generated_attribute_methods = nil
388
+ end
389
+ end
390
+
391
+ def resolve_attribute_name(name)
392
+ attribute_aliases.fetch(super, &:itself)
393
+ end
394
+
353
395
  def generated_attribute_methods
354
396
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
355
397
  end
@@ -367,62 +409,77 @@ module ActiveModel
367
409
  # used to alleviate the GC, which ultimately also speeds up the app
368
410
  # significantly (in our case our test suite finishes 10% faster with
369
411
  # this cache).
370
- def attribute_method_matchers_cache
371
- @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
412
+ def attribute_method_patterns_cache
413
+ @attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
372
414
  end
373
415
 
374
- def attribute_method_matchers_matching(method_name)
375
- attribute_method_matchers_cache.compute_if_absent(method_name) do
376
- attribute_method_matchers.filter_map { |matcher| matcher.match(method_name) }
416
+ def attribute_method_patterns_matching(method_name)
417
+ attribute_method_patterns_cache.compute_if_absent(method_name) do
418
+ attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
377
419
  end
378
420
  end
379
421
 
380
422
  # Define a method `name` in `mod` that dispatches to `send`
381
423
  # using the given `extra` args. This falls back on `send`
382
424
  # if the called name cannot be compiled.
383
- def define_proxy_call(code_generator, name, target, parameters, *call_args, namespace:)
425
+ def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
426
+ mangled_name = build_mangled_name(name)
427
+
428
+ call_args.map!(&:inspect)
429
+ call_args << parameters if parameters
430
+
431
+ # We have to use a different namespace for every target method, because
432
+ # if someone defines an attribute that look like an attribute method we could clash, e.g.
433
+ # attribute :title_was
434
+ # attribute :title
435
+ namespace = :"#{namespace}_#{proxy_target}"
436
+
437
+ define_call(code_generator, name, proxy_target, mangled_name, parameters, call_args, namespace: namespace, as: as)
438
+ end
439
+
440
+ def build_mangled_name(name)
384
441
  mangled_name = name
442
+
385
443
  unless NAME_COMPILABLE_REGEXP.match?(name)
386
444
  mangled_name = "__temp__#{name.unpack1("h*")}"
387
445
  end
388
446
 
389
- code_generator.define_cached_method(name, as: mangled_name, namespace: :"#{namespace}_#{target}") do |batch|
390
- call_args.map!(&:inspect)
391
- call_args << parameters if parameters
447
+ mangled_name
448
+ end
392
449
 
393
- body = if CALL_COMPILABLE_REGEXP.match?(target)
394
- "self.#{target}(#{call_args.join(", ")})"
450
+ def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:, as:)
451
+ code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
452
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
453
+ "self.#{target_name}(#{call_args.join(", ")})"
395
454
  else
396
- call_args.unshift(":'#{target}'")
455
+ call_args.unshift(":'#{target_name}'")
397
456
  "send(#{call_args.join(", ")})"
398
457
  end
399
458
 
400
- modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
401
-
402
459
  batch <<
403
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
460
+ "def #{mangled_name}(#{parameters || ''})" <<
404
461
  body <<
405
462
  "end"
406
463
  end
407
464
  end
408
465
 
409
- class AttributeMethodMatcher # :nodoc:
410
- attr_reader :prefix, :suffix, :target, :parameters
466
+ class AttributeMethodPattern # :nodoc:
467
+ attr_reader :prefix, :suffix, :proxy_target, :parameters
411
468
 
412
- AttributeMethodMatch = Struct.new(:target, :attr_name)
469
+ AttributeMethod = Struct.new(:proxy_target, :attr_name)
413
470
 
414
471
  def initialize(prefix: "", suffix: "", parameters: nil)
415
472
  @prefix = prefix
416
473
  @suffix = suffix
417
- @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
474
+ @parameters = parameters.nil? ? "..." : parameters
418
475
  @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
419
- @target = "#{@prefix}attribute#{@suffix}"
476
+ @proxy_target = "#{@prefix}attribute#{@suffix}"
420
477
  @method_name = "#{prefix}%s#{suffix}"
421
478
  end
422
479
 
423
480
  def match(method_name)
424
481
  if @regex =~ method_name
425
- AttributeMethodMatch.new(target, $1)
482
+ AttributeMethod.new(proxy_target, $1)
426
483
  end
427
484
  end
428
485
 
@@ -442,24 +499,22 @@ module ActiveModel
442
499
  # It's also possible to instantiate related objects, so a <tt>Client</tt>
443
500
  # class belonging to the +clients+ table with a +master_id+ foreign key
444
501
  # can instantiate master through <tt>Client#master</tt>.
445
- def method_missing(method, *args, &block)
502
+ def method_missing(method, ...)
446
503
  if respond_to_without_attributes?(method, true)
447
504
  super
448
505
  else
449
- match = matched_attribute_method(method.to_s)
450
- match ? attribute_missing(match, *args, &block) : super
506
+ match = matched_attribute_method(method.name)
507
+ match ? attribute_missing(match, ...) : super
451
508
  end
452
509
  end
453
- ruby2_keywords(:method_missing)
454
510
 
455
511
  # +attribute_missing+ is like +method_missing+, but for attributes. When
456
512
  # +method_missing+ is called we check to see if there is a matching
457
513
  # attribute method. If so, we tell +attribute_missing+ to dispatch the
458
514
  # attribute. This method can be overloaded to customize the behavior.
459
- def attribute_missing(match, *args, &block)
460
- __send__(match.target, match.attr_name, *args, &block)
515
+ def attribute_missing(match, ...)
516
+ __send__(match.proxy_target, match.attr_name, ...)
461
517
  end
462
- ruby2_keywords(:attribute_missing)
463
518
 
464
519
  # A +Person+ instance with a +name+ attribute can ask
465
520
  # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
@@ -485,12 +540,12 @@ module ActiveModel
485
540
  # Returns a struct representing the matching attribute method.
486
541
  # The struct's attributes are prefix, base and suffix.
487
542
  def matched_attribute_method(method_name)
488
- matches = self.class.send(:attribute_method_matchers_matching, method_name)
543
+ matches = self.class.send(:attribute_method_patterns_matching, method_name)
489
544
  matches.detect { |match| attribute_method?(match.attr_name) }
490
545
  end
491
546
 
492
547
  def missing_attribute(attr_name, stack)
493
- raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
548
+ raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
494
549
  end
495
550
 
496
551
  def _read_attribute(attr)
@@ -0,0 +1,117 @@
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
+ name = resolve_attribute_name(name)
14
+ type = resolve_type_name(type, **options) if type.is_a?(Symbol)
15
+ type = hook_attribute_type(name, type) if type
16
+
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)
27
+
28
+ reset_default_attributes
29
+ end
30
+
31
+ def _default_attributes # :nodoc:
32
+ @default_attributes ||= AttributeSet.new({}).tap do |attribute_set|
33
+ apply_pending_attribute_modifications(attribute_set)
34
+ end
35
+ end
36
+
37
+ def attribute_types # :nodoc:
38
+ @attribute_types ||= _default_attributes.cast_types.tap do |hash|
39
+ hash.default = Type.default_value
40
+ end
41
+ end
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
+
53
+ private
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
60
+
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)
64
+ end
65
+ end
66
+
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
75
+ end
76
+
77
+ def pending_attribute_modifications
78
+ @pending_attribute_modifications ||= []
79
+ end
80
+
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)
84
+ end
85
+
86
+ pending_attribute_modifications.each do |modification|
87
+ modification.apply_to(attribute_set)
88
+ end
89
+ end
90
+
91
+ def reset_default_attributes
92
+ reset_default_attributes!
93
+ subclasses.each { |subclass| subclass.send(:reset_default_attributes) }
94
+ end
95
+
96
+ def reset_default_attributes!
97
+ @default_attributes = nil
98
+ @attribute_types = nil
99
+ end
100
+
101
+ def resolve_attribute_name(name)
102
+ name.to_s
103
+ end
104
+
105
+ def resolve_type_name(name, **options)
106
+ Type.lookup(name, **options)
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
115
+ end
116
+ end
117
+ 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