activemodel 7.1.5.2 → 8.1.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -281
  3. data/README.rdoc +10 -10
  4. data/lib/active_model/attribute/user_provided_default.rb +10 -0
  5. data/lib/active_model/attribute.rb +10 -2
  6. data/lib/active_model/attribute_assignment.rb +26 -3
  7. data/lib/active_model/attribute_methods.rb +42 -43
  8. data/lib/active_model/attribute_mutation_tracker.rb +5 -5
  9. data/lib/active_model/attribute_registration.rb +62 -22
  10. data/lib/active_model/attribute_set.rb +1 -1
  11. data/lib/active_model/attributes/normalization.rb +192 -0
  12. data/lib/active_model/attributes.rb +13 -0
  13. data/lib/active_model/callbacks.rb +1 -1
  14. data/lib/active_model/conversion.rb +1 -1
  15. data/lib/active_model/dirty.rb +16 -9
  16. data/lib/active_model/error.rb +3 -2
  17. data/lib/active_model/errors.rb +1 -4
  18. data/lib/active_model/gem_version.rb +3 -3
  19. data/lib/active_model/lint.rb +7 -3
  20. data/lib/active_model/model.rb +2 -2
  21. data/lib/active_model/naming.rb +0 -1
  22. data/lib/active_model/nested_error.rb +1 -3
  23. data/lib/active_model/railtie.rb +8 -4
  24. data/lib/active_model/secure_password.rb +62 -5
  25. data/lib/active_model/serialization.rb +21 -21
  26. data/lib/active_model/translation.rb +22 -5
  27. data/lib/active_model/type/big_integer.rb +21 -0
  28. data/lib/active_model/type/boolean.rb +1 -0
  29. data/lib/active_model/type/date.rb +1 -0
  30. data/lib/active_model/type/date_time.rb +8 -0
  31. data/lib/active_model/type/decimal.rb +1 -0
  32. data/lib/active_model/type/float.rb +9 -8
  33. data/lib/active_model/type/helpers/immutable.rb +13 -0
  34. data/lib/active_model/type/helpers/time_value.rb +12 -15
  35. data/lib/active_model/type/helpers/timezone.rb +5 -1
  36. data/lib/active_model/type/helpers.rb +1 -0
  37. data/lib/active_model/type/immutable_string.rb +2 -0
  38. data/lib/active_model/type/integer.rb +31 -19
  39. data/lib/active_model/type/registry.rb +2 -3
  40. data/lib/active_model/type/string.rb +4 -0
  41. data/lib/active_model/type/value.rb +4 -4
  42. data/lib/active_model/validations/acceptance.rb +1 -1
  43. data/lib/active_model/validations/callbacks.rb +11 -1
  44. data/lib/active_model/validations/comparison.rb +1 -1
  45. data/lib/active_model/validations/validates.rb +8 -3
  46. data/lib/active_model/validations.rb +57 -32
  47. data/lib/active_model.rb +6 -0
  48. metadata +10 -8
@@ -66,7 +66,6 @@ module ActiveModel
66
66
 
67
67
  NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
68
68
  CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
69
- FORWARD_PARAMETERS = "*args"
70
69
 
71
70
  included do
72
71
  class_attribute :attribute_aliases, instance_writer: false, default: {}
@@ -228,28 +227,13 @@ module ActiveModel
228
227
  method_name = pattern.method_name(new_name).to_s
229
228
  target_name = pattern.method_name(old_name).to_s
230
229
  parameters = pattern.parameters
231
- mangled_name = target_name
232
230
 
233
- unless NAME_COMPILABLE_REGEXP.match?(target_name)
234
- mangled_name = "__temp__#{target_name.unpack1("h*")}"
235
- end
231
+ mangled_name = build_mangled_name(target_name)
236
232
 
237
- code_generator.define_cached_method(mangled_name, as: method_name, namespace: :alias_attribute) do |batch|
238
- body = if CALL_COMPILABLE_REGEXP.match?(target_name)
239
- "self.#{target_name}(#{parameters || ''})"
240
- else
241
- call_args = [":'#{target_name}'"]
242
- call_args << parameters if parameters
243
- "send(#{call_args.join(", ")})"
244
- end
233
+ call_args = []
234
+ call_args << parameters if parameters
245
235
 
246
- modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
247
-
248
- batch <<
249
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
250
- body <<
251
- "end"
252
- end
236
+ define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute, as: method_name)
253
237
  end
254
238
 
255
239
  # Is +new_name+ an alias?
@@ -337,8 +321,8 @@ module ActiveModel
337
321
  canonical_method_name = pattern.method_name(attr_name)
338
322
  public_method_name = pattern.method_name(as)
339
323
 
340
- # If defining a regular attribute method, we don't override methods that are explictly
341
- # defined in parrent classes.
324
+ # If defining a regular attribute method, we don't override methods that are explicitly
325
+ # defined in parent classes.
342
326
  if instance_method_already_implemented?(public_method_name)
343
327
  # However, for `alias_attribute`, we always define the method.
344
328
  # We check for override second because `instance_method_already_implemented?`
@@ -347,6 +331,7 @@ module ActiveModel
347
331
  end
348
332
 
349
333
  generate_method = "define_method_#{pattern.proxy_target}"
334
+
350
335
  if respond_to?(generate_method, true)
351
336
  send(generate_method, attr_name.to_s, owner: owner, as: as)
352
337
  else
@@ -357,7 +342,7 @@ module ActiveModel
357
342
  pattern.parameters,
358
343
  attr_name.to_s,
359
344
  namespace: :active_model_proxy,
360
- as: public_method_name,
345
+ as: public_method_name
361
346
  )
362
347
  end
363
348
  end
@@ -388,7 +373,7 @@ module ActiveModel
388
373
  # person.name_short? # => NoMethodError
389
374
  # person.first_name # => NoMethodError
390
375
  def undefine_attribute_methods
391
- generated_attribute_methods.module_eval do
376
+ @generated_attribute_methods&.module_eval do
392
377
  undef_method(*instance_methods)
393
378
  end
394
379
  attribute_method_patterns_cache.clear
@@ -403,6 +388,8 @@ module ActiveModel
403
388
  super
404
389
  base.class_eval do
405
390
  @attribute_method_patterns_cache = nil
391
+ @aliases_by_attribute_name = nil
392
+ @generated_attribute_methods = nil
406
393
  end
407
394
  end
408
395
 
@@ -415,7 +402,7 @@ module ActiveModel
415
402
  end
416
403
 
417
404
  def instance_method_already_implemented?(method_name)
418
- generated_attribute_methods.method_defined?(method_name)
405
+ @generated_attribute_methods&.method_defined?(method_name)
419
406
  end
420
407
 
421
408
  # The methods +method_missing+ and +respond_to?+ of this module are
@@ -441,27 +428,41 @@ module ActiveModel
441
428
  # using the given `extra` args. This falls back on `send`
442
429
  # if the called name cannot be compiled.
443
430
  def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
444
- mangled_name = name
445
- unless NAME_COMPILABLE_REGEXP.match?(name)
446
- mangled_name = "__temp__#{name.unpack1("h*")}"
447
- end
431
+ mangled_name = build_mangled_name(name)
448
432
 
449
433
  call_args.map!(&:inspect)
450
434
  call_args << parameters if parameters
435
+
436
+ # We have to use a different namespace for every target method, because
437
+ # if someone defines an attribute that look like an attribute method we could clash, e.g.
438
+ # attribute :title_was
439
+ # attribute :title
451
440
  namespace = :"#{namespace}_#{proxy_target}"
452
441
 
442
+ define_call(code_generator, name, proxy_target, mangled_name, parameters, call_args, namespace: namespace, as: as)
443
+ end
444
+
445
+ def build_mangled_name(name)
446
+ mangled_name = name
447
+
448
+ unless NAME_COMPILABLE_REGEXP.match?(name)
449
+ mangled_name = :"__temp__#{name.unpack1("h*")}"
450
+ end
451
+
452
+ mangled_name
453
+ end
454
+
455
+ def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:, as:)
453
456
  code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
454
- body = if CALL_COMPILABLE_REGEXP.match?(proxy_target)
455
- "self.#{proxy_target}(#{call_args.join(", ")})"
457
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
458
+ "self.#{target_name}(#{call_args.join(", ")})"
456
459
  else
457
- call_args.unshift(":'#{proxy_target}'")
460
+ call_args.unshift(":'#{target_name}'")
458
461
  "send(#{call_args.join(", ")})"
459
462
  end
460
463
 
461
- modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
462
-
463
464
  batch <<
464
- "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
465
+ "def #{mangled_name}(#{parameters || ''})" <<
465
466
  body <<
466
467
  "end"
467
468
  end
@@ -475,7 +476,7 @@ module ActiveModel
475
476
  def initialize(prefix: "", suffix: "", parameters: nil)
476
477
  @prefix = prefix
477
478
  @suffix = suffix
478
- @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
479
+ @parameters = parameters.nil? ? "..." : parameters
479
480
  @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/
480
481
  @proxy_target = "#{@prefix}attribute#{@suffix}"
481
482
  @method_name = "#{prefix}%s#{suffix}"
@@ -503,24 +504,22 @@ module ActiveModel
503
504
  # It's also possible to instantiate related objects, so a <tt>Client</tt>
504
505
  # class belonging to the +clients+ table with a +master_id+ foreign key
505
506
  # can instantiate master through <tt>Client#master</tt>.
506
- def method_missing(method, *args, &block)
507
+ def method_missing(method, ...)
507
508
  if respond_to_without_attributes?(method, true)
508
509
  super
509
510
  else
510
- match = matched_attribute_method(method.to_s)
511
- match ? attribute_missing(match, *args, &block) : super
511
+ match = matched_attribute_method(method.name)
512
+ match ? attribute_missing(match, ...) : super
512
513
  end
513
514
  end
514
- ruby2_keywords(:method_missing)
515
515
 
516
516
  # +attribute_missing+ is like +method_missing+, but for attributes. When
517
517
  # +method_missing+ is called we check to see if there is a matching
518
518
  # attribute method. If so, we tell +attribute_missing+ to dispatch the
519
519
  # attribute. This method can be overloaded to customize the behavior.
520
- def attribute_missing(match, *args, &block)
521
- __send__(match.proxy_target, match.attr_name, *args, &block)
520
+ def attribute_missing(match, ...)
521
+ __send__(match.proxy_target, match.attr_name, ...)
522
522
  end
523
- ruby2_keywords(:attribute_missing)
524
523
 
525
524
  # A +Person+ instance with a +name+ attribute can ask
526
525
  # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
@@ -5,7 +5,7 @@ require "active_support/core_ext/object/duplicable"
5
5
 
6
6
  module ActiveModel
7
7
  class AttributeMutationTracker # :nodoc:
8
- OPTION_NOT_GIVEN = Object.new
8
+ OPTION_NOT_GIVEN = Object.new.freeze
9
9
 
10
10
  def initialize(attributes)
11
11
  @attributes = attributes
@@ -16,17 +16,17 @@ module ActiveModel
16
16
  end
17
17
 
18
18
  def changed_values
19
- attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
19
+ attr_names.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |attr_name, result|
20
20
  if changed?(attr_name)
21
- result[attr_name] = original_value(attr_name)
21
+ result.store(attr_name, original_value(attr_name), convert_value: false)
22
22
  end
23
23
  end
24
24
  end
25
25
 
26
26
  def changes
27
- attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
27
+ attr_names.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |attr_name, result|
28
28
  if change = change_to_attribute(attr_name)
29
- result.merge!(attr_name => change)
29
+ result.store(attr_name, change, convert_value: false)
30
30
  end
31
31
  end
32
32
  end
@@ -10,17 +10,28 @@ module ActiveModel
10
10
 
11
11
  module ClassMethods # :nodoc:
12
12
  def attribute(name, type = nil, default: (no_default = true), **options)
13
+ name = resolve_attribute_name(name)
13
14
  type = resolve_type_name(type, **options) if type.is_a?(Symbol)
15
+ type = hook_attribute_type(name, type) if type
14
16
 
15
- pending = pending_attribute(name)
16
- pending.type = type if type
17
- pending.default = default unless no_default
17
+ pending_attribute_modifications << PendingType.new(name, type) if type || no_default
18
+ pending_attribute_modifications << PendingDefault.new(name, default) unless no_default
19
+
20
+ reset_default_attributes
21
+ end
22
+
23
+ def decorate_attributes(names = nil, &decorator) # :nodoc:
24
+ names = names&.map { |name| resolve_attribute_name(name) }
25
+
26
+ pending_attribute_modifications << PendingDecorator.new(names, decorator)
18
27
 
19
28
  reset_default_attributes
20
29
  end
21
30
 
22
31
  def _default_attributes # :nodoc:
23
- @default_attributes ||= build_default_attributes
32
+ @default_attributes ||= AttributeSet.new({}).tap do |attribute_set|
33
+ apply_pending_attribute_modifications(attribute_set)
34
+ end
24
35
  end
25
36
 
26
37
  def attribute_types # :nodoc:
@@ -29,40 +40,62 @@ module ActiveModel
29
40
  end
30
41
  end
31
42
 
43
+ def type_for_attribute(attribute_name, &block)
44
+ attribute_name = resolve_attribute_name(attribute_name)
45
+
46
+ if block
47
+ attribute_types.fetch(attribute_name, &block)
48
+ else
49
+ attribute_types[attribute_name]
50
+ end
51
+ end
52
+
32
53
  private
33
- class PendingAttribute # :nodoc:
34
- attr_accessor :type, :default
54
+ PendingType = Struct.new(:name, :type) do # :nodoc:
55
+ def apply_to(attribute_set)
56
+ attribute = attribute_set[name]
57
+ attribute_set[name] = attribute.with_type(type || attribute.type)
58
+ end
59
+ end
35
60
 
36
- def apply_to(attribute)
37
- attribute = attribute.with_type(type || attribute.type)
38
- attribute = attribute.with_user_default(default) if defined?(@default)
39
- attribute
61
+ PendingDefault = Struct.new(:name, :default) do # :nodoc:
62
+ def apply_to(attribute_set)
63
+ attribute_set[name] = attribute_set[name].with_user_default(default)
40
64
  end
41
65
  end
42
66
 
43
- def pending_attribute(name)
44
- @pending_attributes ||= {}
45
- @pending_attributes[resolve_attribute_name(name)] ||= PendingAttribute.new
67
+ PendingDecorator = Struct.new(:names, :decorator) do # :nodoc:
68
+ def apply_to(attribute_set)
69
+ (names || attribute_set.keys).each do |name|
70
+ attribute = attribute_set[name]
71
+ type = decorator.call(name, attribute.type)
72
+ attribute_set[name] = attribute.with_type(type) if type
73
+ end
74
+ end
46
75
  end
47
76
 
48
- def apply_pending_attributes(attribute_set)
49
- superclass.send(__method__, attribute_set) if superclass.respond_to?(__method__, true)
77
+ def pending_attribute_modifications
78
+ @pending_attribute_modifications ||= []
79
+ end
50
80
 
51
- defined?(@pending_attributes) && @pending_attributes.each do |name, pending|
52
- attribute_set[name] = pending.apply_to(attribute_set[name])
81
+ def apply_pending_attribute_modifications(attribute_set)
82
+ if superclass.respond_to?(:apply_pending_attribute_modifications, true)
83
+ superclass.send(:apply_pending_attribute_modifications, attribute_set)
53
84
  end
54
85
 
55
- attribute_set
86
+ pending_attribute_modifications.each do |modification|
87
+ modification.apply_to(attribute_set)
88
+ end
56
89
  end
57
90
 
58
- def build_default_attributes
59
- apply_pending_attributes(AttributeSet.new({}))
91
+ def reset_default_attributes
92
+ reset_default_attributes!
93
+ subclasses.each { |subclass| subclass.send(:reset_default_attributes) }
60
94
  end
61
95
 
62
- def reset_default_attributes
96
+ def reset_default_attributes!
63
97
  @default_attributes = nil
64
98
  @attribute_types = nil
65
- subclasses.each { |subclass| subclass.send(__method__) }
66
99
  end
67
100
 
68
101
  def resolve_attribute_name(name)
@@ -72,6 +105,13 @@ module ActiveModel
72
105
  def resolve_type_name(name, **options)
73
106
  Type.lookup(name, **options)
74
107
  end
108
+
109
+ # Hook for other modules to override. The attribute type is passed
110
+ # through this method immediately after it is resolved, before any type
111
+ # decorations are applied.
112
+ def hook_attribute_type(attribute, type)
113
+ type
114
+ end
75
115
  end
76
116
  end
77
117
  end
@@ -71,7 +71,7 @@ module ActiveModel
71
71
  end
72
72
 
73
73
  def deep_dup
74
- AttributeSet.new(attributes.deep_dup)
74
+ AttributeSet.new(attributes.transform_values(&:dup_or_share))
75
75
  end
76
76
 
77
77
  def initialize_dup(_)
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Attributes
5
+ module Normalization
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include ActiveModel::Dirty
10
+ include ActiveModel::Validations::Callbacks
11
+
12
+ class_attribute :normalized_attributes, default: Set.new
13
+
14
+ before_validation :normalize_changed_in_place_attributes
15
+ end
16
+
17
+ # Normalizes a specified attribute using its declared normalizations.
18
+ #
19
+ # ==== Examples
20
+ #
21
+ # class User
22
+ # include ActiveModel::Attributes
23
+ # include ActiveModel::Attributes::Normalization
24
+ #
25
+ # attribute :email, :string
26
+ #
27
+ # normalizes :email, with: -> email { email.strip.downcase }
28
+ # end
29
+ #
30
+ # legacy_user = User.load_from_legacy_data(...)
31
+ # legacy_user.email # => " CRUISE-CONTROL@EXAMPLE.COM\n"
32
+ # legacy_user.normalize_attribute(:email)
33
+ # legacy_user.email # => "cruise-control@example.com"
34
+ #
35
+ # ==== Behavior with Active Record
36
+ #
37
+ # To prevent confusion, normalization will not be applied
38
+ # when the attribute is fetched from the database. This means that if a
39
+ # record was persisted before the normalization was declared, the record's
40
+ # attribute will not be normalized until either it is assigned a new
41
+ # value, or it is explicitly migrated via Normalization#normalize_attribute.
42
+ #
43
+ # Be aware that if your app was created before Rails 7.1, and your app
44
+ # marshals instances of the targeted model (for example, when caching),
45
+ # then you should set ActiveRecord.marshalling_format_version to +7.1+ or
46
+ # higher via either <tt>config.load_defaults 7.1</tt> or
47
+ # <tt>config.active_record.marshalling_format_version = 7.1</tt>.
48
+ # Otherwise, +Marshal+ may attempt to serialize the normalization +Proc+
49
+ # and raise +TypeError+.
50
+ #
51
+ # class User < ActiveRecord::Base
52
+ # normalizes :email, with: -> email { email.strip.downcase }
53
+ # normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
54
+ # end
55
+ #
56
+ # user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
57
+ # user.email # => "cruise-control@example.com"
58
+ #
59
+ # user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")
60
+ # user.email # => "cruise-control@example.com"
61
+ # user.email_before_type_cast # => "cruise-control@example.com"
62
+ #
63
+ # User.where(email: "\tCRUISE-CONTROL@EXAMPLE.COM ").count # => 1
64
+ # User.where(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]).count # => 0
65
+ #
66
+ # User.exists?(email: "\tCRUISE-CONTROL@EXAMPLE.COM ") # => true
67
+ # User.exists?(["email = ?", "\tCRUISE-CONTROL@EXAMPLE.COM "]) # => false
68
+ #
69
+ # User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
70
+ def normalize_attribute(name)
71
+ # Treat the value as a new, unnormalized value.
72
+ send(:"#{name}=", send(name))
73
+ end
74
+
75
+ module ClassMethods
76
+ # Declares a normalization for one or more attributes. The normalization
77
+ # is applied when the attribute is assigned or validated.
78
+ #
79
+ # Because the normalization may be applied multiple times, it should be
80
+ # _idempotent_. In other words, applying the normalization more than once
81
+ # should have the same result as applying it only once.
82
+ #
83
+ # By default, the normalization will not be applied to +nil+ values. This
84
+ # behavior can be changed with the +:apply_to_nil+ option.
85
+ #
86
+ # ==== Options
87
+ #
88
+ # * +:with+ - Any callable object that accepts the attribute's value as
89
+ # its sole argument, and returns it normalized.
90
+ # * +:apply_to_nil+ - Whether to apply the normalization to +nil+ values.
91
+ # Defaults to +false+.
92
+ #
93
+ # ==== Examples
94
+ #
95
+ # class User
96
+ # include ActiveModel::Attributes
97
+ # include ActiveModel::Attributes::Normalization
98
+ #
99
+ # attribute :email, :string
100
+ # attribute :phone, :string
101
+ #
102
+ # normalizes :email, with: -> email { email.strip.downcase }
103
+ # normalizes :phone, with: -> phone { phone.delete("^0-9").delete_prefix("1") }
104
+ # end
105
+ #
106
+ # user = User.new
107
+ # user.email = " CRUISE-CONTROL@EXAMPLE.COM\n"
108
+ # user.email # => "cruise-control@example.com"
109
+ #
110
+ # User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
111
+ def normalizes(*names, with:, apply_to_nil: false)
112
+ decorate_attributes(names) do |name, cast_type|
113
+ NormalizedValueType.new(cast_type: cast_type, normalizer: with, normalize_nil: apply_to_nil)
114
+ end
115
+
116
+ self.normalized_attributes += names.map(&:to_sym)
117
+ end
118
+
119
+ # Normalizes a given +value+ using normalizations declared for +name+.
120
+ #
121
+ # ==== Examples
122
+ #
123
+ # class User
124
+ # include ActiveModel::Attributes
125
+ # include ActiveModel::Attributes::Normalization
126
+ #
127
+ # attribute :email, :string
128
+ #
129
+ # normalizes :email, with: -> email { email.strip.downcase }
130
+ # end
131
+ #
132
+ # User.normalize_value_for(:email, " CRUISE-CONTROL@EXAMPLE.COM\n")
133
+ # # => "cruise-control@example.com"
134
+ def normalize_value_for(name, value)
135
+ type_for_attribute(name).cast(value)
136
+ end
137
+ end
138
+
139
+ private
140
+ def normalize_changed_in_place_attributes
141
+ self.class.normalized_attributes.each do |name|
142
+ normalize_attribute(name) if attribute_changed_in_place?(name)
143
+ end
144
+ end
145
+
146
+ class NormalizedValueType < DelegateClass(ActiveModel::Type::Value) # :nodoc:
147
+ include ActiveModel::Type::SerializeCastValue
148
+
149
+ attr_reader :cast_type, :normalizer, :normalize_nil
150
+ alias :normalize_nil? :normalize_nil
151
+
152
+ def initialize(cast_type:, normalizer:, normalize_nil:)
153
+ @cast_type = cast_type
154
+ @normalizer = normalizer
155
+ @normalize_nil = normalize_nil
156
+ super(cast_type)
157
+ end
158
+
159
+ def cast(value)
160
+ normalize(super(value))
161
+ end
162
+
163
+ def serialize(value)
164
+ serialize_cast_value(cast(value))
165
+ end
166
+
167
+ def serialize_cast_value(value)
168
+ ActiveModel::Type::SerializeCastValue.serialize(cast_type, value)
169
+ end
170
+
171
+ def ==(other)
172
+ self.class == other.class &&
173
+ normalize_nil? == other.normalize_nil? &&
174
+ normalizer == other.normalizer &&
175
+ cast_type == other.cast_type
176
+ end
177
+ alias eql? ==
178
+
179
+ def hash
180
+ [self.class, cast_type, normalizer, normalize_nil?].hash
181
+ end
182
+
183
+ define_method(:inspect, Kernel.instance_method(:inspect))
184
+
185
+ private
186
+ def normalize(value)
187
+ normalizer.call(value) unless value.nil? && !normalize_nil?
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -75,6 +75,19 @@ module ActiveModel
75
75
  attribute_types.keys
76
76
  end
77
77
 
78
+ ##
79
+ # :method: type_for_attribute
80
+ # :call-seq: type_for_attribute(attribute_name, &block)
81
+ #
82
+ # Returns the type of the specified attribute after applying any
83
+ # modifiers. This method is the only valid source of information for
84
+ # anything related to the types of a model's attributes. The return value
85
+ # of this method will implement the interface described by
86
+ # ActiveModel::Type::Value (though the object itself may not subclass it).
87
+ #--
88
+ # Implemented by ActiveModel::AttributeRegistration::ClassMethods#type_for_attribute.
89
+
90
+ ##
78
91
  private
79
92
  def define_method_attribute=(canonical_name, owner:, as: canonical_name)
80
93
  ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
@@ -60,7 +60,7 @@ module ActiveModel
60
60
  # Would only create the +after_create+ and +before_create+ callback methods in
61
61
  # your class.
62
62
  #
63
- # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
63
+ # NOTE: Defining the same callback multiple times will overwrite previous callback definitions.
64
64
  #
65
65
  module Callbacks
66
66
  def self.extended(base) # :nodoc:
@@ -3,7 +3,7 @@
3
3
  module ActiveModel
4
4
  # = Active \Model \Conversion
5
5
  #
6
- # Handles default conversions: to_model, to_key, to_param, and to_partial_path.
6
+ # Handles default conversions: #to_model, #to_key, #to_param, and #to_partial_path.
7
7
  #
8
8
  # Let's take for example this non-persisted object.
9
9
  #
@@ -108,7 +108,7 @@ module ActiveModel
108
108
  # person.changes # => {"name" => ["Bill", "Bob"]}
109
109
  #
110
110
  # If an attribute is modified in-place then make use of
111
- # {*_will_change!}[rdoc-label:method-i-2A_will_change-21] to mark that the attribute is changing.
111
+ # {*_will_change!}[rdoc-ref:#*_will_change!] to mark that the attribute is changing.
112
112
  # Otherwise \Active \Model can't track changes to in-place attributes. Note
113
113
  # that Active Record can detect in-place modifications automatically. You do
114
114
  # not need to call <tt>*_will_change!</tt> on Active Record models.
@@ -247,16 +247,23 @@ module ActiveModel
247
247
 
248
248
  def initialize_dup(other) # :nodoc:
249
249
  super
250
+ @mutations_from_database = nil
251
+ end
252
+
253
+ def init_attributes(other) # :nodoc:
254
+ attrs = super
250
255
  if self.class.respond_to?(:_default_attributes)
251
- @attributes = self.class._default_attributes.map do |attr|
252
- attr.with_value_from_user(@attributes.fetch_value(attr.name))
256
+ self.class._default_attributes.map do |attr|
257
+ attr.with_value_from_user(attrs.fetch_value(attr.name))
253
258
  end
259
+ else
260
+ attrs
254
261
  end
255
- @mutations_from_database = nil
256
262
  end
257
263
 
258
264
  def as_json(options = {}) # :nodoc:
259
- options[:except] = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
265
+ except = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
266
+ options = options.merge except: except
260
267
  super(options)
261
268
  end
262
269
 
@@ -289,22 +296,22 @@ module ActiveModel
289
296
  mutations_from_database.changed_attribute_names
290
297
  end
291
298
 
292
- # Dispatch target for {*_changed?}[rdoc-label:method-i-2A_changed-3F] attribute methods.
299
+ # Dispatch target for {*_changed?}[rdoc-ref:#*_changed?] attribute methods.
293
300
  def attribute_changed?(attr_name, **options)
294
301
  mutations_from_database.changed?(attr_name.to_s, **options)
295
302
  end
296
303
 
297
- # Dispatch target for {*_was}[rdoc-label:method-i-2A_was] attribute methods.
304
+ # Dispatch target for {*_was}[rdoc-ref:#*_was] attribute methods.
298
305
  def attribute_was(attr_name)
299
306
  mutations_from_database.original_value(attr_name.to_s)
300
307
  end
301
308
 
302
- # Dispatch target for {*_previously_changed?}[rdoc-label:method-i-2A_previously_changed-3F] attribute methods.
309
+ # Dispatch target for {*_previously_changed?}[rdoc-ref:#*_previously_changed?] attribute methods.
303
310
  def attribute_previously_changed?(attr_name, **options)
304
311
  mutations_before_last_save.changed?(attr_name.to_s, **options)
305
312
  end
306
313
 
307
- # Dispatch target for {*_previously_was}[rdoc-label:method-i-2A_previously_was] attribute methods.
314
+ # Dispatch target for {*_previously_was}[rdoc-ref:#*_previously_was] attribute methods.
308
315
  def attribute_previously_was(attr_name)
309
316
  mutations_before_last_save.original_value(attr_name.to_s)
310
317
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/class/attribute"
4
3
 
5
4
  module ActiveModel
6
5
  # = Active \Model \Error
@@ -83,7 +82,7 @@ module ActiveModel
83
82
  defaults << :"#{i18n_scope}.errors.messages.#{type}"
84
83
 
85
84
  catch(:exception) do
86
- translation = I18n.translate(defaults.first, **options.merge(default: defaults.drop(1), throw: true))
85
+ translation = I18n.translate(defaults.first, **options, default: defaults.drop(1), throw: true)
87
86
  return translation unless translation.nil?
88
87
  end unless options[:message]
89
88
  else
@@ -205,4 +204,6 @@ module ActiveModel
205
204
  [@base, @attribute, @raw_type, @options.except(*CALLBACKS_OPTIONS)]
206
205
  end
207
206
  end
207
+
208
+ ActiveSupport.run_load_hooks(:active_model_error, Error)
208
209
  end