activemodel 5.2.6 → 6.1.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -109
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -4
  5. data/lib/active_model.rb +2 -1
  6. data/lib/active_model/attribute.rb +21 -21
  7. data/lib/active_model/attribute/user_provided_default.rb +1 -2
  8. data/lib/active_model/attribute_assignment.rb +4 -6
  9. data/lib/active_model/attribute_methods.rb +117 -40
  10. data/lib/active_model/attribute_mutation_tracker.rb +90 -33
  11. data/lib/active_model/attribute_set.rb +20 -28
  12. data/lib/active_model/attribute_set/builder.rb +81 -16
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
  14. data/lib/active_model/attributes.rb +65 -44
  15. data/lib/active_model/callbacks.rb +11 -9
  16. data/lib/active_model/conversion.rb +1 -1
  17. data/lib/active_model/dirty.rb +51 -101
  18. data/lib/active_model/error.rb +207 -0
  19. data/lib/active_model/errors.rb +347 -155
  20. data/lib/active_model/gem_version.rb +3 -3
  21. data/lib/active_model/lint.rb +1 -1
  22. data/lib/active_model/naming.rb +22 -7
  23. data/lib/active_model/nested_error.rb +22 -0
  24. data/lib/active_model/railtie.rb +6 -0
  25. data/lib/active_model/secure_password.rb +54 -55
  26. data/lib/active_model/serialization.rb +9 -7
  27. data/lib/active_model/serializers/json.rb +17 -9
  28. data/lib/active_model/translation.rb +1 -1
  29. data/lib/active_model/type/big_integer.rb +0 -1
  30. data/lib/active_model/type/binary.rb +1 -1
  31. data/lib/active_model/type/boolean.rb +0 -1
  32. data/lib/active_model/type/date.rb +0 -5
  33. data/lib/active_model/type/date_time.rb +3 -8
  34. data/lib/active_model/type/decimal.rb +0 -1
  35. data/lib/active_model/type/float.rb +2 -3
  36. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +14 -6
  37. data/lib/active_model/type/helpers/numeric.rb +17 -6
  38. data/lib/active_model/type/helpers/time_value.rb +37 -15
  39. data/lib/active_model/type/helpers/timezone.rb +1 -1
  40. data/lib/active_model/type/immutable_string.rb +14 -11
  41. data/lib/active_model/type/integer.rb +15 -18
  42. data/lib/active_model/type/registry.rb +16 -16
  43. data/lib/active_model/type/string.rb +12 -3
  44. data/lib/active_model/type/time.rb +1 -6
  45. data/lib/active_model/type/value.rb +9 -2
  46. data/lib/active_model/validations.rb +6 -9
  47. data/lib/active_model/validations/absence.rb +2 -2
  48. data/lib/active_model/validations/acceptance.rb +34 -27
  49. data/lib/active_model/validations/callbacks.rb +15 -16
  50. data/lib/active_model/validations/clusivity.rb +6 -3
  51. data/lib/active_model/validations/confirmation.rb +4 -4
  52. data/lib/active_model/validations/exclusion.rb +1 -1
  53. data/lib/active_model/validations/format.rb +2 -3
  54. data/lib/active_model/validations/inclusion.rb +2 -2
  55. data/lib/active_model/validations/length.rb +3 -3
  56. data/lib/active_model/validations/numericality.rb +58 -44
  57. data/lib/active_model/validations/presence.rb +1 -1
  58. data/lib/active_model/validations/validates.rb +7 -6
  59. data/lib/active_model/validator.rb +8 -3
  60. metadata +14 -9
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/hash/indifferent_access"
4
+ require "active_support/core_ext/object/duplicable"
4
5
 
5
6
  module ActiveModel
6
7
  class AttributeMutationTracker # :nodoc:
@@ -8,7 +9,6 @@ module ActiveModel
8
9
 
9
10
  def initialize(attributes)
10
11
  @attributes = attributes
11
- @forced_changes = Set.new
12
12
  end
13
13
 
14
14
  def changed_attribute_names
@@ -18,24 +18,22 @@ module ActiveModel
18
18
  def changed_values
19
19
  attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
20
20
  if changed?(attr_name)
21
- result[attr_name] = attributes[attr_name].original_value
21
+ result[attr_name] = original_value(attr_name)
22
22
  end
23
23
  end
24
24
  end
25
25
 
26
26
  def changes
27
27
  attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
28
- change = change_to_attribute(attr_name)
29
- if change
28
+ if change = change_to_attribute(attr_name)
30
29
  result.merge!(attr_name => change)
31
30
  end
32
31
  end
33
32
  end
34
33
 
35
34
  def change_to_attribute(attr_name)
36
- attr_name = attr_name.to_s
37
35
  if changed?(attr_name)
38
- [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
36
+ [original_value(attr_name), fetch_value(attr_name)]
39
37
  end
40
38
  end
41
39
 
@@ -44,81 +42,140 @@ module ActiveModel
44
42
  end
45
43
 
46
44
  def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
47
- attr_name = attr_name.to_s
48
- forced_changes.include?(attr_name) ||
49
- attributes[attr_name].changed? &&
50
- (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
51
- (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
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)
52
48
  end
53
49
 
54
50
  def changed_in_place?(attr_name)
55
- attributes[attr_name.to_s].changed_in_place?
51
+ attributes[attr_name].changed_in_place?
56
52
  end
57
53
 
58
54
  def forget_change(attr_name)
59
- attr_name = attr_name.to_s
60
55
  attributes[attr_name] = attributes[attr_name].forgetting_assignment
61
56
  forced_changes.delete(attr_name)
62
57
  end
63
58
 
64
59
  def original_value(attr_name)
65
- attributes[attr_name.to_s].original_value
60
+ attributes[attr_name].original_value
66
61
  end
67
62
 
68
63
  def force_change(attr_name)
69
- forced_changes << attr_name.to_s
64
+ forced_changes[attr_name] = fetch_value(attr_name)
70
65
  end
71
66
 
72
- # TODO Change this to private once we've dropped Ruby 2.2 support.
73
- # Workaround for Ruby 2.2 "private attribute?" warning.
74
- protected
67
+ private
68
+ attr_reader :attributes
69
+
70
+ def forced_changes
71
+ @forced_changes ||= {}
72
+ end
73
+
74
+ def attr_names
75
+ attributes.keys
76
+ end
75
77
 
76
- attr_reader :attributes, :forced_changes
78
+ def attribute_changed?(attr_name)
79
+ forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
80
+ end
81
+
82
+ def fetch_value(attr_name)
83
+ attributes.fetch_value(attr_name)
84
+ end
85
+ end
86
+
87
+ class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
88
+ def initialize(attributes)
89
+ super
90
+ @finalized_changes = nil
91
+ end
92
+
93
+ def changed_in_place?(attr_name)
94
+ false
95
+ end
96
+
97
+ def change_to_attribute(attr_name)
98
+ if finalized_changes&.include?(attr_name)
99
+ finalized_changes[attr_name].dup
100
+ else
101
+ super
102
+ end
103
+ end
104
+
105
+ def forget_change(attr_name)
106
+ forced_changes.delete(attr_name)
107
+ end
108
+
109
+ def original_value(attr_name)
110
+ if changed?(attr_name)
111
+ forced_changes[attr_name]
112
+ else
113
+ fetch_value(attr_name)
114
+ end
115
+ end
116
+
117
+ def force_change(attr_name)
118
+ forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
119
+ end
120
+
121
+ def finalize_changes
122
+ @finalized_changes = changes
123
+ end
77
124
 
78
125
  private
126
+ attr_reader :finalized_changes
79
127
 
80
128
  def attr_names
81
- attributes.keys
129
+ forced_changes.keys
130
+ end
131
+
132
+ def attribute_changed?(attr_name)
133
+ forced_changes.include?(attr_name)
134
+ end
135
+
136
+ def fetch_value(attr_name)
137
+ attributes.send(:_read_attribute, attr_name)
138
+ end
139
+
140
+ def clone_value(attr_name)
141
+ value = fetch_value(attr_name)
142
+ value.duplicable? ? value.clone : value
143
+ rescue TypeError, NoMethodError
144
+ value
82
145
  end
83
146
  end
84
147
 
85
148
  class NullMutationTracker # :nodoc:
86
149
  include Singleton
87
150
 
88
- def changed_attribute_names(*)
151
+ def changed_attribute_names
89
152
  []
90
153
  end
91
154
 
92
- def changed_values(*)
155
+ def changed_values
93
156
  {}
94
157
  end
95
158
 
96
- def changes(*)
159
+ def changes
97
160
  {}
98
161
  end
99
162
 
100
163
  def change_to_attribute(attr_name)
101
164
  end
102
165
 
103
- def any_changes?(*)
166
+ def any_changes?
104
167
  false
105
168
  end
106
169
 
107
- def changed?(*)
170
+ def changed?(attr_name, **)
108
171
  false
109
172
  end
110
173
 
111
- def changed_in_place?(*)
174
+ def changed_in_place?(attr_name)
112
175
  false
113
176
  end
114
177
 
115
- def forget_change(*)
116
- end
117
-
118
- def original_value(*)
119
- end
120
-
121
- def force_change(*)
178
+ def original_value(attr_name)
122
179
  end
123
180
  end
124
181
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/enumerable"
3
4
  require "active_support/core_ext/object/deep_dup"
4
5
  require "active_model/attribute_set/builder"
5
6
  require "active_model/attribute_set/yaml_encoder"
@@ -13,11 +14,11 @@ module ActiveModel
13
14
  end
14
15
 
15
16
  def [](name)
16
- attributes[name] || Attribute.null(name)
17
+ @attributes[name] || default_attribute(name)
17
18
  end
18
19
 
19
20
  def []=(name, value)
20
- attributes[name] = value
21
+ @attributes[name] = value
21
22
  end
22
23
 
23
24
  def values_before_type_cast
@@ -25,9 +26,9 @@ module ActiveModel
25
26
  end
26
27
 
27
28
  def to_hash
28
- initialized_attributes.transform_values(&:value)
29
+ keys.index_with { |name| self[name].value }
29
30
  end
30
- alias_method :to_h, :to_hash
31
+ alias :to_h :to_hash
31
32
 
32
33
  def key?(name)
33
34
  attributes.key?(name) && self[name].initialized?
@@ -37,48 +38,41 @@ module ActiveModel
37
38
  attributes.each_key.select { |name| self[name].initialized? }
38
39
  end
39
40
 
40
- if defined?(JRUBY_VERSION)
41
- # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
42
- # https://github.com/jruby/jruby/pull/2562
43
- def fetch_value(name, &block)
44
- self[name].value(&block)
45
- end
46
- else
47
- def fetch_value(name)
48
- self[name].value { |n| yield n if block_given? }
49
- end
41
+ def fetch_value(name, &block)
42
+ self[name].value(&block)
50
43
  end
51
44
 
52
45
  def write_from_database(name, value)
53
- attributes[name] = self[name].with_value_from_database(value)
46
+ @attributes[name] = self[name].with_value_from_database(value)
54
47
  end
55
48
 
56
49
  def write_from_user(name, value)
57
- attributes[name] = self[name].with_value_from_user(value)
50
+ raise FrozenError, "can't modify frozen attributes" if frozen?
51
+ @attributes[name] = self[name].with_value_from_user(value)
52
+ value
58
53
  end
59
54
 
60
55
  def write_cast_value(name, value)
61
- attributes[name] = self[name].with_cast_value(value)
56
+ @attributes[name] = self[name].with_cast_value(value)
57
+ value
62
58
  end
63
59
 
64
60
  def freeze
65
- @attributes.freeze
61
+ attributes.freeze
66
62
  super
67
63
  end
68
64
 
69
65
  def deep_dup
70
- self.class.allocate.tap do |copy|
71
- copy.instance_variable_set(:@attributes, attributes.deep_dup)
72
- end
66
+ AttributeSet.new(attributes.deep_dup)
73
67
  end
74
68
 
75
69
  def initialize_dup(_)
76
- @attributes = attributes.dup
70
+ @attributes = @attributes.dup
77
71
  super
78
72
  end
79
73
 
80
74
  def initialize_clone(_)
81
- @attributes = attributes.clone
75
+ @attributes = @attributes.clone
82
76
  super
83
77
  end
84
78
 
@@ -89,7 +83,7 @@ module ActiveModel
89
83
  end
90
84
 
91
85
  def accessed
92
- attributes.select { |_, attr| attr.has_been_read? }.keys
86
+ attributes.each_key.select { |name| self[name].has_been_read? }
93
87
  end
94
88
 
95
89
  def map(&block)
@@ -102,13 +96,11 @@ module ActiveModel
102
96
  end
103
97
 
104
98
  protected
105
-
106
99
  attr_reader :attributes
107
100
 
108
101
  private
109
-
110
- def initialized_attributes
111
- attributes.select { |_, attr| attr.initialized? }
102
+ def default_attribute(name)
103
+ Attribute.null(name)
112
104
  end
113
105
  end
114
106
  end
@@ -13,14 +13,86 @@ module ActiveModel
13
13
  end
14
14
 
15
15
  def build_from_database(values = {}, additional_types = {})
16
- attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
17
- AttributeSet.new(attributes)
16
+ LazyAttributeSet.new(values, types, additional_types, default_attributes)
18
17
  end
19
18
  end
20
19
  end
21
20
 
21
+ class LazyAttributeSet < AttributeSet # :nodoc:
22
+ def initialize(values, types, additional_types, default_attributes, attributes = {})
23
+ super(attributes)
24
+ @values = values
25
+ @types = types
26
+ @additional_types = additional_types
27
+ @default_attributes = default_attributes
28
+ @casted_values = {}
29
+ @materialized = false
30
+ end
31
+
32
+ def key?(name)
33
+ (values.key?(name) || types.key?(name) || @attributes.key?(name)) && self[name].initialized?
34
+ end
35
+
36
+ def keys
37
+ keys = values.keys | types.keys | @attributes.keys
38
+ keys.keep_if { |name| self[name].initialized? }
39
+ end
40
+
41
+ def fetch_value(name, &block)
42
+ if attr = @attributes[name]
43
+ return attr.value(&block)
44
+ end
45
+
46
+ @casted_values.fetch(name) do
47
+ value_present = true
48
+ value = values.fetch(name) { value_present = false }
49
+
50
+ if value_present
51
+ type = additional_types.fetch(name, types[name])
52
+ @casted_values[name] = type.deserialize(value)
53
+ else
54
+ attr = default_attribute(name, value_present, value)
55
+ attr.value(&block)
56
+ end
57
+ end
58
+ end
59
+
60
+ protected
61
+ def attributes
62
+ unless @materialized
63
+ values.each_key { |key| self[key] }
64
+ types.each_key { |key| self[key] }
65
+ @materialized = true
66
+ end
67
+ @attributes
68
+ end
69
+
70
+ private
71
+ attr_reader :values, :types, :additional_types, :default_attributes
72
+
73
+ def default_attribute(
74
+ name,
75
+ value_present = true,
76
+ value = values.fetch(name) { value_present = false }
77
+ )
78
+ type = additional_types.fetch(name, types[name])
79
+
80
+ if value_present
81
+ @attributes[name] = Attribute.from_database(name, value, type, @casted_values[name])
82
+ elsif types.key?(name)
83
+ if attr = default_attributes[name]
84
+ @attributes[name] = attr.dup
85
+ else
86
+ @attributes[name] = Attribute.uninitialized(name, type)
87
+ end
88
+ else
89
+ Attribute.null(name)
90
+ end
91
+ end
92
+ end
93
+
22
94
  class LazyAttributeHash # :nodoc:
23
- delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
95
+ delegate :transform_values, :each_value, :fetch, :except, to: :materialize
24
96
 
25
97
  def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
26
98
  @types = types
@@ -40,9 +112,6 @@ module ActiveModel
40
112
  end
41
113
 
42
114
  def []=(key, value)
43
- if frozen?
44
- raise RuntimeError, "Can't modify frozen hash"
45
- end
46
115
  delegate_hash[key] = value
47
116
  end
48
117
 
@@ -57,14 +126,9 @@ module ActiveModel
57
126
  super
58
127
  end
59
128
 
60
- def select
129
+ def each_key(&block)
61
130
  keys = types.keys | values.keys | delegate_hash.keys
62
- keys.each_with_object({}) do |key, hash|
63
- attribute = self[key]
64
- if yield(key, attribute)
65
- hash[key] = attribute
66
- end
67
- end
131
+ keys.each(&block)
68
132
  end
69
133
 
70
134
  def ==(other)
@@ -81,6 +145,9 @@ module ActiveModel
81
145
 
82
146
  def marshal_load(values)
83
147
  if values.is_a?(Hash)
148
+ ActiveSupport::Deprecation.warn(<<~MSG)
149
+ Marshalling load from legacy attributes format is deprecated and will be removed in Rails 6.2.
150
+ MSG
84
151
  empty_hash = {}.freeze
85
152
  initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
86
153
  @materialized = true
@@ -90,9 +157,6 @@ module ActiveModel
90
157
  end
91
158
 
92
159
  protected
93
-
94
- attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
95
-
96
160
  def materialize
97
161
  unless @materialized
98
162
  values.each_key { |key| self[key] }
@@ -105,6 +169,7 @@ module ActiveModel
105
169
  end
106
170
 
107
171
  private
172
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
108
173
 
109
174
  def assign_default_value(name)
110
175
  type = additional_types.fetch(name, types[name])