activemodel 7.0.8.7 → 7.2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -263
  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 +145 -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 +4 -4
  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 +12 -7
@@ -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,41 @@ 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
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)
214
+ end
215
+ end
216
+
217
+ def generate_alias_attribute_methods(code_generator, new_name, old_name)
218
+ ActiveSupport::CodeGenerator.batch(code_generator, __FILE__, __LINE__) do |owner|
219
+ attribute_method_patterns.each do |pattern|
220
+ alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
233
221
  end
222
+ attribute_method_patterns_cache.clear
234
223
  end
235
224
  end
236
225
 
226
+ def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
227
+ method_name = pattern.method_name(new_name).to_s
228
+ target_name = pattern.method_name(old_name).to_s
229
+ parameters = pattern.parameters
230
+
231
+ mangled_name = build_mangled_name(target_name)
232
+
233
+ call_args = []
234
+ call_args << parameters if parameters
235
+
236
+ define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute, as: method_name)
237
+ end
238
+
237
239
  # Is +new_name+ an alias?
238
240
  def attribute_alias?(new_name)
239
241
  attribute_aliases.key? new_name.to_s
@@ -245,7 +247,7 @@ module ActiveModel
245
247
  end
246
248
 
247
249
  # Declares the attributes that should be prefixed and suffixed by
248
- # <tt>ActiveModel::AttributeMethods</tt>.
250
+ # +ActiveModel::AttributeMethods+.
249
251
  #
250
252
  # To use, pass attribute names (as strings or symbols). Be sure to declare
251
253
  # +define_attribute_methods+ after you define any prefix, suffix, or affix
@@ -269,12 +271,17 @@ module ActiveModel
269
271
  # end
270
272
  def define_attribute_methods(*attr_names)
271
273
  ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
272
- attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
274
+ attr_names.flatten.each do |attr_name|
275
+ define_attribute_method(attr_name, _owner: owner)
276
+ aliases_by_attribute_name[attr_name.to_s].each do |aliased_name|
277
+ generate_alias_attribute_methods owner, aliased_name, attr_name
278
+ end
279
+ end
273
280
  end
274
281
  end
275
282
 
276
283
  # Declares an attribute that should be prefixed and suffixed by
277
- # <tt>ActiveModel::AttributeMethods</tt>.
284
+ # +ActiveModel::AttributeMethods+.
278
285
  #
279
286
  # To use, pass an attribute name (as string or symbol). Be sure to declare
280
287
  # +define_attribute_method+ after you define any prefix, suffix or affix
@@ -301,26 +308,46 @@ module ActiveModel
301
308
  # person.name = 'Bob'
302
309
  # person.name # => "Bob"
303
310
  # person.name_short? # => true
304
- def define_attribute_method(attr_name, _owner: generated_attribute_methods)
311
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
305
312
  ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
306
- attribute_method_matchers.each do |matcher|
307
- method_name = matcher.method_name(attr_name)
313
+ attribute_method_patterns.each do |pattern|
314
+ define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
315
+ end
316
+ attribute_method_patterns_cache.clear
317
+ end
318
+ end
308
319
 
309
- unless instance_method_already_implemented?(method_name)
310
- generate_method = "define_method_#{matcher.target}"
320
+ def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
321
+ canonical_method_name = pattern.method_name(attr_name)
322
+ public_method_name = pattern.method_name(as)
323
+
324
+ # If defining a regular attribute method, we don't override methods that are explictly
325
+ # defined in parrent classes.
326
+ if instance_method_already_implemented?(public_method_name)
327
+ # However, for `alias_attribute`, we always define the method.
328
+ # We check for override second because `instance_method_already_implemented?`
329
+ # also check for dangerous methods.
330
+ return unless override
331
+ end
311
332
 
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
333
+ generate_method = "define_method_#{pattern.proxy_target}"
334
+
335
+ if respond_to?(generate_method, true)
336
+ send(generate_method, attr_name.to_s, owner: owner, as: as)
337
+ else
338
+ define_proxy_call(
339
+ owner,
340
+ canonical_method_name,
341
+ pattern.proxy_target,
342
+ pattern.parameters,
343
+ attr_name.to_s,
344
+ namespace: :active_model_proxy,
345
+ as: public_method_name
346
+ )
320
347
  end
321
348
  end
322
349
 
323
- # Removes all the previously dynamically defined methods from the class.
350
+ # Removes all the previously dynamically defined methods from the class, including alias attribute methods.
324
351
  #
325
352
  # class Person
326
353
  # include ActiveModel::AttributeMethods
@@ -328,6 +355,7 @@ module ActiveModel
328
355
  # attr_accessor :name
329
356
  # attribute_method_suffix '_short?'
330
357
  # define_attribute_method :name
358
+ # alias_attribute :first_name, :name
331
359
  #
332
360
  # private
333
361
  # def attribute_short?(attr)
@@ -337,19 +365,38 @@ module ActiveModel
337
365
  #
338
366
  # person = Person.new
339
367
  # person.name = 'Bob'
368
+ # person.first_name # => "Bob"
340
369
  # person.name_short? # => true
341
370
  #
342
371
  # Person.undefine_attribute_methods
343
372
  #
344
373
  # person.name_short? # => NoMethodError
374
+ # person.first_name # => NoMethodError
345
375
  def undefine_attribute_methods
346
376
  generated_attribute_methods.module_eval do
347
377
  undef_method(*instance_methods)
348
378
  end
349
- attribute_method_matchers_cache.clear
379
+ attribute_method_patterns_cache.clear
380
+ end
381
+
382
+ def aliases_by_attribute_name # :nodoc:
383
+ @aliases_by_attribute_name ||= Hash.new { |h, k| h[k] = [] }
350
384
  end
351
385
 
352
386
  private
387
+ def inherited(base) # :nodoc:
388
+ super
389
+ base.class_eval do
390
+ @attribute_method_patterns_cache = nil
391
+ @aliases_by_attribute_name = nil
392
+ @generated_attribute_methods = nil
393
+ end
394
+ end
395
+
396
+ def resolve_attribute_name(name)
397
+ attribute_aliases.fetch(super, &:itself)
398
+ end
399
+
353
400
  def generated_attribute_methods
354
401
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
355
402
  end
@@ -367,62 +414,77 @@ module ActiveModel
367
414
  # used to alleviate the GC, which ultimately also speeds up the app
368
415
  # significantly (in our case our test suite finishes 10% faster with
369
416
  # this cache).
370
- def attribute_method_matchers_cache
371
- @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
417
+ def attribute_method_patterns_cache
418
+ @attribute_method_patterns_cache ||= Concurrent::Map.new(initial_capacity: 4)
372
419
  end
373
420
 
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) }
421
+ def attribute_method_patterns_matching(method_name)
422
+ attribute_method_patterns_cache.compute_if_absent(method_name) do
423
+ attribute_method_patterns.filter_map { |pattern| pattern.match(method_name) }
377
424
  end
378
425
  end
379
426
 
380
427
  # Define a method `name` in `mod` that dispatches to `send`
381
428
  # using the given `extra` args. This falls back on `send`
382
429
  # if the called name cannot be compiled.
383
- def define_proxy_call(code_generator, name, target, parameters, *call_args, namespace:)
430
+ def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
431
+ mangled_name = build_mangled_name(name)
432
+
433
+ call_args.map!(&:inspect)
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
440
+ namespace = :"#{namespace}_#{proxy_target}"
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)
384
446
  mangled_name = name
447
+
385
448
  unless NAME_COMPILABLE_REGEXP.match?(name)
386
- mangled_name = "__temp__#{name.unpack1("h*")}"
449
+ mangled_name = :"__temp__#{name.unpack1("h*")}"
387
450
  end
388
451
 
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
452
+ mangled_name
453
+ end
392
454
 
393
- body = if CALL_COMPILABLE_REGEXP.match?(target)
394
- "self.#{target}(#{call_args.join(", ")})"
455
+ def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:, as:)
456
+ code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
457
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
458
+ "self.#{target_name}(#{call_args.join(", ")})"
395
459
  else
396
- call_args.unshift(":'#{target}'")
460
+ call_args.unshift(":'#{target_name}'")
397
461
  "send(#{call_args.join(", ")})"
398
462
  end
399
463
 
400
- modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
401
-
402
464
  batch <<
403
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
465
+ "def #{mangled_name}(#{parameters || ''})" <<
404
466
  body <<
405
467
  "end"
406
468
  end
407
469
  end
408
470
 
409
- class AttributeMethodMatcher # :nodoc:
410
- attr_reader :prefix, :suffix, :target, :parameters
471
+ class AttributeMethodPattern # :nodoc:
472
+ attr_reader :prefix, :suffix, :proxy_target, :parameters
411
473
 
412
- AttributeMethodMatch = Struct.new(:target, :attr_name)
474
+ AttributeMethod = Struct.new(:proxy_target, :attr_name)
413
475
 
414
476
  def initialize(prefix: "", suffix: "", parameters: nil)
415
477
  @prefix = prefix
416
478
  @suffix = suffix
417
- @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
479
+ @parameters = parameters.nil? ? "..." : parameters
418
480
  @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
419
- @target = "#{@prefix}attribute#{@suffix}"
481
+ @proxy_target = "#{@prefix}attribute#{@suffix}"
420
482
  @method_name = "#{prefix}%s#{suffix}"
421
483
  end
422
484
 
423
485
  def match(method_name)
424
486
  if @regex =~ method_name
425
- AttributeMethodMatch.new(target, $1)
487
+ AttributeMethod.new(proxy_target, $1)
426
488
  end
427
489
  end
428
490
 
@@ -442,24 +504,22 @@ module ActiveModel
442
504
  # It's also possible to instantiate related objects, so a <tt>Client</tt>
443
505
  # class belonging to the +clients+ table with a +master_id+ foreign key
444
506
  # can instantiate master through <tt>Client#master</tt>.
445
- def method_missing(method, *args, &block)
507
+ def method_missing(method, ...)
446
508
  if respond_to_without_attributes?(method, true)
447
509
  super
448
510
  else
449
- match = matched_attribute_method(method.to_s)
450
- match ? attribute_missing(match, *args, &block) : super
511
+ match = matched_attribute_method(method.name)
512
+ match ? attribute_missing(match, ...) : super
451
513
  end
452
514
  end
453
- ruby2_keywords(:method_missing)
454
515
 
455
516
  # +attribute_missing+ is like +method_missing+, but for attributes. When
456
517
  # +method_missing+ is called we check to see if there is a matching
457
518
  # attribute method. If so, we tell +attribute_missing+ to dispatch the
458
519
  # 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)
520
+ def attribute_missing(match, ...)
521
+ __send__(match.proxy_target, match.attr_name, ...)
461
522
  end
462
- ruby2_keywords(:attribute_missing)
463
523
 
464
524
  # A +Person+ instance with a +name+ attribute can ask
465
525
  # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
@@ -485,12 +545,12 @@ module ActiveModel
485
545
  # Returns a struct representing the matching attribute method.
486
546
  # The struct's attributes are prefix, base and suffix.
487
547
  def matched_attribute_method(method_name)
488
- matches = self.class.send(:attribute_method_matchers_matching, method_name)
548
+ matches = self.class.send(:attribute_method_patterns_matching, method_name)
489
549
  matches.detect { |match| attribute_method?(match.attr_name) }
490
550
  end
491
551
 
492
552
  def missing_attribute(attr_name, stack)
493
- raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
553
+ raise ActiveModel::MissingAttributeError, "missing attribute '#{attr_name}' for #{self.class}", stack
494
554
  end
495
555
 
496
556
  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