activemodel 4.2.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +49 -37
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +16 -22
  5. data/lib/active_model/attribute/user_provided_default.rb +51 -0
  6. data/lib/active_model/attribute.rb +248 -0
  7. data/lib/active_model/attribute_assignment.rb +55 -0
  8. data/lib/active_model/attribute_methods.rb +150 -73
  9. data/lib/active_model/attribute_mutation_tracker.rb +181 -0
  10. data/lib/active_model/attribute_set/builder.rb +191 -0
  11. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  12. data/lib/active_model/attribute_set.rb +106 -0
  13. data/lib/active_model/attributes.rb +132 -0
  14. data/lib/active_model/callbacks.rb +31 -25
  15. data/lib/active_model/conversion.rb +20 -9
  16. data/lib/active_model/dirty.rb +142 -116
  17. data/lib/active_model/error.rb +207 -0
  18. data/lib/active_model/errors.rb +436 -202
  19. data/lib/active_model/forbidden_attributes_protection.rb +6 -3
  20. data/lib/active_model/gem_version.rb +5 -3
  21. data/lib/active_model/lint.rb +47 -42
  22. data/lib/active_model/locale/en.yml +2 -1
  23. data/lib/active_model/model.rb +7 -7
  24. data/lib/active_model/naming.rb +36 -18
  25. data/lib/active_model/nested_error.rb +22 -0
  26. data/lib/active_model/railtie.rb +8 -0
  27. data/lib/active_model/secure_password.rb +61 -67
  28. data/lib/active_model/serialization.rb +48 -17
  29. data/lib/active_model/serializers/json.rb +22 -13
  30. data/lib/active_model/translation.rb +5 -4
  31. data/lib/active_model/type/big_integer.rb +14 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +46 -0
  34. data/lib/active_model/type/date.rb +52 -0
  35. data/lib/active_model/type/date_time.rb +46 -0
  36. data/lib/active_model/type/decimal.rb +69 -0
  37. data/lib/active_model/type/float.rb +35 -0
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +49 -0
  39. data/lib/active_model/type/helpers/mutable.rb +20 -0
  40. data/lib/active_model/type/helpers/numeric.rb +48 -0
  41. data/lib/active_model/type/helpers/time_value.rb +90 -0
  42. data/lib/active_model/type/helpers/timezone.rb +19 -0
  43. data/lib/active_model/type/helpers.rb +7 -0
  44. data/lib/active_model/type/immutable_string.rb +35 -0
  45. data/lib/active_model/type/integer.rb +67 -0
  46. data/lib/active_model/type/registry.rb +70 -0
  47. data/lib/active_model/type/string.rb +35 -0
  48. data/lib/active_model/type/time.rb +46 -0
  49. data/lib/active_model/type/value.rb +133 -0
  50. data/lib/active_model/type.rb +53 -0
  51. data/lib/active_model/validations/absence.rb +6 -4
  52. data/lib/active_model/validations/acceptance.rb +72 -14
  53. data/lib/active_model/validations/callbacks.rb +23 -19
  54. data/lib/active_model/validations/clusivity.rb +18 -12
  55. data/lib/active_model/validations/confirmation.rb +27 -14
  56. data/lib/active_model/validations/exclusion.rb +7 -4
  57. data/lib/active_model/validations/format.rb +27 -27
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +8 -7
  60. data/lib/active_model/validations/length.rb +35 -32
  61. data/lib/active_model/validations/numericality.rb +72 -34
  62. data/lib/active_model/validations/presence.rb +3 -3
  63. data/lib/active_model/validations/validates.rb +17 -15
  64. data/lib/active_model/validations/with.rb +6 -12
  65. data/lib/active_model/validations.rb +58 -23
  66. data/lib/active_model/validator.rb +23 -17
  67. data/lib/active_model/version.rb +4 -2
  68. data/lib/active_model.rb +18 -11
  69. metadata +44 -25
  70. data/lib/active_model/serializers/xml.rb +0 -238
  71. data/lib/active_model/test_case.rb +0 -4
@@ -1,12 +1,17 @@
1
- # -*- coding: utf-8 -*-
1
+ # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext/array/conversions'
4
- require 'active_support/core_ext/string/inflections'
3
+ require "active_support/core_ext/array/conversions"
4
+ require "active_support/core_ext/string/inflections"
5
+ require "active_support/core_ext/object/deep_dup"
6
+ require "active_support/core_ext/string/filters"
7
+ require "active_model/error"
8
+ require "active_model/nested_error"
9
+ require "forwardable"
5
10
 
6
11
  module ActiveModel
7
12
  # == Active \Model \Errors
8
13
  #
9
- # Provides a modified +Hash+ that you can include in your object
14
+ # Provides error related functionalities you can include in your object
10
15
  # for handling error messages and interacting with Action View helpers.
11
16
  #
12
17
  # A minimal implementation could be:
@@ -23,7 +28,7 @@ module ActiveModel
23
28
  # attr_reader :errors
24
29
  #
25
30
  # def validate!
26
- # errors.add(:name, "cannot be nil") if name.nil?
31
+ # errors.add(:name, :blank, message: "cannot be nil") if name.nil?
27
32
  # end
28
33
  #
29
34
  # # The following methods are needed to be minimally implemented
@@ -32,20 +37,20 @@ module ActiveModel
32
37
  # send(attr)
33
38
  # end
34
39
  #
35
- # def Person.human_attribute_name(attr, options = {})
40
+ # def self.human_attribute_name(attr, options = {})
36
41
  # attr
37
42
  # end
38
43
  #
39
- # def Person.lookup_ancestors
44
+ # def self.lookup_ancestors
40
45
  # [self]
41
46
  # end
42
47
  # end
43
48
  #
44
- # The last three methods are required in your object for Errors to be
49
+ # The last three methods are required in your object for +Errors+ to be
45
50
  # able to generate error messages correctly and also handle multiple
46
- # languages. Of course, if you extend your object with ActiveModel::Translation
51
+ # languages. Of course, if you extend your object with <tt>ActiveModel::Translation</tt>
47
52
  # you will not need to implement the last two. Likewise, using
48
- # ActiveModel::Validations will handle the validation related methods
53
+ # <tt>ActiveModel::Validations</tt> will handle the validation related methods
49
54
  # for you.
50
55
  #
51
56
  # The above allows you to do:
@@ -57,9 +62,18 @@ module ActiveModel
57
62
  class Errors
58
63
  include Enumerable
59
64
 
60
- CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
65
+ extend Forwardable
66
+ def_delegators :@errors, :size, :clear, :blank?, :empty?, :uniq!, :any?
67
+ # TODO: forward all enumerable methods after `each` deprecation is removed.
68
+ def_delegators :@errors, :count
61
69
 
62
- attr_reader :messages
70
+ LEGACY_ATTRIBUTES = [:messages, :details].freeze
71
+ private_constant :LEGACY_ATTRIBUTES
72
+
73
+ # The actual array of +Error+ objects
74
+ # This method is aliased to <tt>objects</tt>.
75
+ attr_reader :errors
76
+ alias :objects :errors
63
77
 
64
78
  # Pass in the instance of the object that is using the errors object.
65
79
  #
@@ -69,22 +83,92 @@ module ActiveModel
69
83
  # end
70
84
  # end
71
85
  def initialize(base)
72
- @base = base
73
- @messages = {}
86
+ @base = base
87
+ @errors = []
74
88
  end
75
89
 
76
90
  def initialize_dup(other) # :nodoc:
77
- @messages = other.messages.dup
91
+ @errors = other.errors.deep_dup
78
92
  super
79
93
  end
80
94
 
81
- # Clear the error messages.
95
+ # Copies the errors from <tt>other</tt>.
96
+ # For copying errors but keep <tt>@base</tt> as is.
82
97
  #
83
- # person.errors.full_messages # => ["name cannot be nil"]
84
- # person.errors.clear
85
- # person.errors.full_messages # => []
86
- def clear
87
- messages.clear
98
+ # other - The ActiveModel::Errors instance.
99
+ #
100
+ # Examples
101
+ #
102
+ # person.errors.copy!(other)
103
+ def copy!(other) # :nodoc:
104
+ @errors = other.errors.deep_dup
105
+ @errors.each { |error|
106
+ error.instance_variable_set(:@base, @base)
107
+ }
108
+ end
109
+
110
+ # Imports one error
111
+ # Imported errors are wrapped as a NestedError,
112
+ # providing access to original error object.
113
+ # If attribute or type needs to be overridden, use `override_options`.
114
+ #
115
+ # override_options - Hash
116
+ # @option override_options [Symbol] :attribute Override the attribute the error belongs to
117
+ # @option override_options [Symbol] :type Override type of the error.
118
+ def import(error, override_options = {})
119
+ [:attribute, :type].each do |key|
120
+ if override_options.key?(key)
121
+ override_options[key] = override_options[key].to_sym
122
+ end
123
+ end
124
+ @errors.append(NestedError.new(@base, error, override_options))
125
+ end
126
+
127
+ # Merges the errors from <tt>other</tt>,
128
+ # each <tt>Error</tt> wrapped as <tt>NestedError</tt>.
129
+ #
130
+ # other - The ActiveModel::Errors instance.
131
+ #
132
+ # Examples
133
+ #
134
+ # person.errors.merge!(other)
135
+ def merge!(other)
136
+ other.errors.each { |error|
137
+ import(error)
138
+ }
139
+ end
140
+
141
+ # Removes all errors except the given keys. Returns a hash containing the removed errors.
142
+ #
143
+ # person.errors.keys # => [:name, :age, :gender, :city]
144
+ # person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }
145
+ # person.errors.keys # => [:age, :gender]
146
+ def slice!(*keys)
147
+ deprecation_removal_warning(:slice!)
148
+
149
+ keys = keys.map(&:to_sym)
150
+
151
+ results = messages.dup.slice!(*keys)
152
+
153
+ @errors.keep_if do |error|
154
+ keys.include?(error.attribute)
155
+ end
156
+
157
+ results
158
+ end
159
+
160
+ # Search for errors matching +attribute+, +type+ or +options+.
161
+ #
162
+ # Only supplied params will be matched.
163
+ #
164
+ # person.errors.where(:name) # => all name errors.
165
+ # person.errors.where(:name, :too_short) # => all name errors being too short
166
+ # person.errors.where(:name, :too_short, minimum: 2) # => all name errors being too short and minimum is 2
167
+ def where(attribute, type = nil, **options)
168
+ attribute, type, options = normalize_arguments(attribute, type, **options)
169
+ @errors.select { |error|
170
+ error.match?(attribute, type, **options)
171
+ }
88
172
  end
89
173
 
90
174
  # Returns +true+ if the error messages include an error for the given key
@@ -94,38 +178,25 @@ module ActiveModel
94
178
  # person.errors.include?(:name) # => true
95
179
  # person.errors.include?(:age) # => false
96
180
  def include?(attribute)
97
- messages[attribute].present?
181
+ @errors.any? { |error|
182
+ error.match?(attribute.to_sym)
183
+ }
98
184
  end
99
- # aliases include?
100
185
  alias :has_key? :include?
101
- # aliases include?
102
186
  alias :key? :include?
103
187
 
104
- # Get messages for +key+.
105
- #
106
- # person.errors.messages # => {:name=>["cannot be nil"]}
107
- # person.errors.get(:name) # => ["cannot be nil"]
108
- # person.errors.get(:age) # => nil
109
- def get(key)
110
- messages[key]
111
- end
112
-
113
- # Set messages for +key+ to +value+.
114
- #
115
- # person.errors.get(:name) # => ["cannot be nil"]
116
- # person.errors.set(:name, ["can't be nil"])
117
- # person.errors.get(:name) # => ["can't be nil"]
118
- def set(key, value)
119
- messages[key] = value
120
- end
121
-
122
188
  # Delete messages for +key+. Returns the deleted messages.
123
189
  #
124
- # person.errors.get(:name) # => ["cannot be nil"]
190
+ # person.errors[:name] # => ["cannot be nil"]
125
191
  # person.errors.delete(:name) # => ["cannot be nil"]
126
- # person.errors.get(:name) # => nil
127
- def delete(key)
128
- messages.delete(key)
192
+ # person.errors[:name] # => []
193
+ def delete(attribute, type = nil, **options)
194
+ attribute, type, options = normalize_arguments(attribute, type, **options)
195
+ matches = where(attribute, type, **options)
196
+ matches.each do |error|
197
+ @errors.delete(error)
198
+ end
199
+ matches.map(&:message).presence
129
200
  end
130
201
 
131
202
  # When passed a symbol or a name of a method, returns an array of errors
@@ -134,53 +205,65 @@ module ActiveModel
134
205
  # person.errors[:name] # => ["cannot be nil"]
135
206
  # person.errors['name'] # => ["cannot be nil"]
136
207
  def [](attribute)
137
- get(attribute.to_sym) || set(attribute.to_sym, [])
208
+ DeprecationHandlingMessageArray.new(messages_for(attribute), self, attribute)
138
209
  end
139
210
 
140
- # Adds to the supplied attribute the supplied error message.
211
+ # Iterates through each error object.
141
212
  #
142
- # person.errors[:name] = "must be set"
143
- # person.errors[:name] # => ['must be set']
144
- def []=(attribute, error)
145
- self[attribute] << error
146
- end
147
-
148
- # Iterates through each error key, value pair in the error messages hash.
213
+ # person.errors.add(:name, :too_short, count: 2)
214
+ # person.errors.each do |error|
215
+ # # Will yield <#ActiveModel::Error attribute=name, type=too_short,
216
+ # options={:count=>3}>
217
+ # end
218
+ #
219
+ # To be backward compatible with past deprecated hash-like behavior,
220
+ # when block accepts two parameters instead of one, it
221
+ # iterates through each error key, value pair in the error messages hash.
149
222
  # Yields the attribute and the error for that attribute. If the attribute
150
223
  # has more than one error message, yields once for each error message.
151
224
  #
152
- # person.errors.add(:name, "can't be blank")
153
- # person.errors.each do |attribute, error|
225
+ # person.errors.add(:name, :blank, message: "can't be blank")
226
+ # person.errors.each do |attribute, message|
154
227
  # # Will yield :name and "can't be blank"
155
228
  # end
156
229
  #
157
- # person.errors.add(:name, "must be specified")
158
- # person.errors.each do |attribute, error|
230
+ # person.errors.add(:name, :not_specified, message: "must be specified")
231
+ # person.errors.each do |attribute, message|
159
232
  # # Will yield :name and "can't be blank"
160
233
  # # then yield :name and "must be specified"
161
234
  # end
162
- def each
163
- messages.each_key do |attribute|
164
- self[attribute].each { |error| yield attribute, error }
235
+ def each(&block)
236
+ if block.arity <= 1
237
+ @errors.each(&block)
238
+ else
239
+ ActiveSupport::Deprecation.warn(<<~MSG)
240
+ Enumerating ActiveModel::Errors as a hash has been deprecated.
241
+ In Rails 6.1, `errors` is an array of Error objects,
242
+ therefore it should be accessed by a block with a single block
243
+ parameter like this:
244
+
245
+ person.errors.each do |error|
246
+ attribute = error.attribute
247
+ message = error.message
248
+ end
249
+
250
+ You are passing a block expecting two parameters,
251
+ so the old hash behavior is simulated. As this is deprecated,
252
+ this will result in an ArgumentError in Rails 6.2.
253
+ MSG
254
+ @errors.
255
+ sort { |a, b| a.attribute <=> b.attribute }.
256
+ each { |error| yield error.attribute, error.message }
165
257
  end
166
258
  end
167
259
 
168
- # Returns the number of error messages.
169
- #
170
- # person.errors.add(:name, "can't be blank")
171
- # person.errors.size # => 1
172
- # person.errors.add(:name, "must be specified")
173
- # person.errors.size # => 2
174
- def size
175
- values.flatten.size
176
- end
177
-
178
260
  # Returns all message values.
179
261
  #
180
262
  # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
181
263
  # person.errors.values # => [["cannot be nil", "must be specified"]]
182
264
  def values
183
- messages.values
265
+ deprecation_removal_warning(:values, "errors.map { |error| error.message }")
266
+ @errors.map(&:message).freeze
184
267
  end
185
268
 
186
269
  # Returns all message keys.
@@ -188,43 +271,24 @@ module ActiveModel
188
271
  # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
189
272
  # person.errors.keys # => [:name]
190
273
  def keys
191
- messages.keys
274
+ deprecation_removal_warning(:keys, "errors.attribute_names")
275
+ keys = @errors.map(&:attribute)
276
+ keys.uniq!
277
+ keys.freeze
192
278
  end
193
279
 
194
- # Returns an array of error messages, with the attribute name included.
280
+ # Returns all error attribute names
195
281
  #
196
- # person.errors.add(:name, "can't be blank")
197
- # person.errors.add(:name, "must be specified")
198
- # person.errors.to_a # => ["name can't be blank", "name must be specified"]
199
- def to_a
200
- full_messages
282
+ # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
283
+ # person.errors.attribute_names # => [:name]
284
+ def attribute_names
285
+ @errors.map(&:attribute).uniq.freeze
201
286
  end
202
287
 
203
- # Returns the number of error messages.
204
- #
205
- # person.errors.add(:name, "can't be blank")
206
- # person.errors.count # => 1
207
- # person.errors.add(:name, "must be specified")
208
- # person.errors.count # => 2
209
- def count
210
- to_a.size
211
- end
212
-
213
- # Returns +true+ if no errors are found, +false+ otherwise.
214
- # If the error message is a string it can be empty.
215
- #
216
- # person.errors.full_messages # => ["name cannot be nil"]
217
- # person.errors.empty? # => false
218
- def empty?
219
- all? { |k, v| v && v.empty? && !v.is_a?(String) }
220
- end
221
- # aliases empty?
222
- alias_method :blank?, :empty?
223
-
224
288
  # Returns an xml formatted representation of the Errors hash.
225
289
  #
226
- # person.errors.add(:name, "can't be blank")
227
- # person.errors.add(:name, "must be specified")
290
+ # person.errors.add(:name, :blank, message: "can't be blank")
291
+ # person.errors.add(:name, :not_specified, message: "must be specified")
228
292
  # person.errors.to_xml
229
293
  # # =>
230
294
  # # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
@@ -232,7 +296,8 @@ module ActiveModel
232
296
  # # <error>name can't be blank</error>
233
297
  # # <error>name must be specified</error>
234
298
  # # </errors>
235
- def to_xml(options={})
299
+ def to_xml(options = {})
300
+ deprecation_removal_warning(:to_xml)
236
301
  to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
237
302
  end
238
303
 
@@ -242,7 +307,7 @@ module ActiveModel
242
307
  #
243
308
  # person.errors.as_json # => {:name=>["cannot be nil"]}
244
309
  # person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]}
245
- def as_json(options=nil)
310
+ def as_json(options = nil)
246
311
  to_hash(options && options[:full_messages])
247
312
  end
248
313
 
@@ -252,95 +317,151 @@ module ActiveModel
252
317
  # person.errors.to_hash # => {:name=>["cannot be nil"]}
253
318
  # person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
254
319
  def to_hash(full_messages = false)
255
- if full_messages
256
- self.messages.each_with_object({}) do |(attribute, array), messages|
257
- messages[attribute] = array.map { |message| full_message(attribute, message) }
258
- end
259
- else
260
- self.messages.dup
320
+ message_method = full_messages ? :full_message : :message
321
+ group_by_attribute.transform_values do |errors|
322
+ errors.map(&message_method)
261
323
  end
262
324
  end
263
325
 
264
- # Adds +message+ to the error messages on +attribute+. More than one error
265
- # can be added to the same +attribute+. If no +message+ is supplied,
266
- # <tt>:invalid</tt> is assumed.
326
+ def to_h
327
+ ActiveSupport::Deprecation.warn(<<~EOM)
328
+ ActiveModel::Errors#to_h is deprecated and will be removed in Rails 6.2.
329
+ Please use `ActiveModel::Errors.to_hash` instead. The values in the hash
330
+ returned by `ActiveModel::Errors.to_hash` is an array of error messages.
331
+ EOM
332
+
333
+ to_hash.transform_values { |values| values.last }
334
+ end
335
+
336
+ # Returns a Hash of attributes with an array of their error messages.
337
+ #
338
+ # Updating this hash would still update errors state for backward
339
+ # compatibility, but this behavior is deprecated.
340
+ def messages
341
+ DeprecationHandlingMessageHash.new(self)
342
+ end
343
+
344
+ # Returns a Hash of attributes with an array of their error details.
345
+ #
346
+ # Updating this hash would still update errors state for backward
347
+ # compatibility, but this behavior is deprecated.
348
+ def details
349
+ hash = group_by_attribute.transform_values do |errors|
350
+ errors.map(&:details)
351
+ end
352
+ DeprecationHandlingDetailsHash.new(hash)
353
+ end
354
+
355
+ # Returns a Hash of attributes with an array of their Error objects.
356
+ #
357
+ # person.errors.group_by_attribute
358
+ # # => {:name=>[<#ActiveModel::Error>, <#ActiveModel::Error>]}
359
+ def group_by_attribute
360
+ @errors.group_by(&:attribute)
361
+ end
362
+
363
+ # Adds a new error of +type+ on +attribute+.
364
+ # More than one error can be added to the same +attribute+.
365
+ # If no +type+ is supplied, <tt>:invalid</tt> is assumed.
267
366
  #
268
367
  # person.errors.add(:name)
269
- # # => ["is invalid"]
270
- # person.errors.add(:name, 'must be implemented')
271
- # # => ["is invalid", "must be implemented"]
368
+ # # Adds <#ActiveModel::Error attribute=name, type=invalid>
369
+ # person.errors.add(:name, :not_implemented, message: "must be implemented")
370
+ # # Adds <#ActiveModel::Error attribute=name, type=not_implemented,
371
+ # options={:message=>"must be implemented"}>
272
372
  #
273
373
  # person.errors.messages
274
- # # => {:name=>["must be implemented", "is invalid"]}
374
+ # # => {:name=>["is invalid", "must be implemented"]}
375
+ #
376
+ # If +type+ is a string, it will be used as error message.
275
377
  #
276
- # If +message+ is a symbol, it will be translated using the appropriate
378
+ # If +type+ is a symbol, it will be translated using the appropriate
277
379
  # scope (see +generate_message+).
278
380
  #
279
- # If +message+ is a proc, it will be called, allowing for things like
381
+ # If +type+ is a proc, it will be called, allowing for things like
280
382
  # <tt>Time.now</tt> to be used within an error.
281
383
  #
282
384
  # If the <tt>:strict</tt> option is set to +true+, it will raise
283
385
  # ActiveModel::StrictValidationFailed instead of adding the error.
284
386
  # <tt>:strict</tt> option can also be set to any other exception.
285
387
  #
286
- # person.errors.add(:name, nil, strict: true)
287
- # # => ActiveModel::StrictValidationFailed: name is invalid
288
- # person.errors.add(:name, nil, strict: NameIsInvalid)
289
- # # => NameIsInvalid: name is invalid
388
+ # person.errors.add(:name, :invalid, strict: true)
389
+ # # => ActiveModel::StrictValidationFailed: Name is invalid
390
+ # person.errors.add(:name, :invalid, strict: NameIsInvalid)
391
+ # # => NameIsInvalid: Name is invalid
290
392
  #
291
393
  # person.errors.messages # => {}
292
394
  #
293
395
  # +attribute+ should be set to <tt>:base</tt> if the error is not
294
396
  # directly associated with a single attribute.
295
397
  #
296
- # person.errors.add(:base, "either name or email must be present")
398
+ # person.errors.add(:base, :name_or_email_blank,
399
+ # message: "either name or email must be present")
297
400
  # person.errors.messages
298
401
  # # => {:base=>["either name or email must be present"]}
299
- def add(attribute, message = :invalid, options = {})
300
- message = normalize_message(attribute, message, options)
402
+ # person.errors.details
403
+ # # => {:base=>[{error: :name_or_email_blank}]}
404
+ def add(attribute, type = :invalid, **options)
405
+ attribute, type, options = normalize_arguments(attribute, type, **options)
406
+ error = Error.new(@base, attribute, type, **options)
407
+
301
408
  if exception = options[:strict]
302
409
  exception = ActiveModel::StrictValidationFailed if exception == true
303
- raise exception, full_message(attribute, message)
410
+ raise exception, error.full_message
304
411
  end
305
412
 
306
- self[attribute] << message
307
- end
413
+ @errors.append(error)
308
414
 
309
- # Will add an error message to each of the attributes in +attributes+
310
- # that is empty.
311
- #
312
- # person.errors.add_on_empty(:name)
313
- # person.errors.messages
314
- # # => {:name=>["can't be empty"]}
315
- def add_on_empty(attributes, options = {})
316
- Array(attributes).each do |attribute|
317
- value = @base.send(:read_attribute_for_validation, attribute)
318
- is_empty = value.respond_to?(:empty?) ? value.empty? : false
319
- add(attribute, :empty, options) if value.nil? || is_empty
320
- end
415
+ error
321
416
  end
322
417
 
323
- # Will add an error message to each of the attributes in +attributes+ that
324
- # is blank (using Object#blank?).
418
+ # Returns +true+ if an error matches provided +attribute+ and +type+,
419
+ # or +false+ otherwise. +type+ is treated the same as for +add+.
325
420
  #
326
- # person.errors.add_on_blank(:name)
327
- # person.errors.messages
328
- # # => {:name=>["can't be blank"]}
329
- def add_on_blank(attributes, options = {})
330
- Array(attributes).each do |attribute|
331
- value = @base.send(:read_attribute_for_validation, attribute)
332
- add(attribute, :blank, options) if value.blank?
421
+ # person.errors.add :name, :blank
422
+ # person.errors.added? :name, :blank # => true
423
+ # person.errors.added? :name, "can't be blank" # => true
424
+ #
425
+ # If the error requires options, then it returns +true+ with
426
+ # the correct options, or +false+ with incorrect or missing options.
427
+ #
428
+ # person.errors.add :name, :too_long, { count: 25 }
429
+ # person.errors.added? :name, :too_long, count: 25 # => true
430
+ # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
431
+ # person.errors.added? :name, :too_long, count: 24 # => false
432
+ # person.errors.added? :name, :too_long # => false
433
+ # person.errors.added? :name, "is too long" # => false
434
+ def added?(attribute, type = :invalid, options = {})
435
+ attribute, type, options = normalize_arguments(attribute, type, **options)
436
+
437
+ if type.is_a? Symbol
438
+ @errors.any? { |error|
439
+ error.strict_match?(attribute, type, **options)
440
+ }
441
+ else
442
+ messages_for(attribute).include?(type)
333
443
  end
334
444
  end
335
445
 
336
- # Returns +true+ if an error on the attribute with the given message is
337
- # present, +false+ otherwise. +message+ is treated the same as for +add+.
338
- #
339
- # person.errors.add :name, :blank
340
- # person.errors.added? :name, :blank # => true
341
- def added?(attribute, message = :invalid, options = {})
342
- message = normalize_message(attribute, message, options)
343
- self[attribute].include? message
446
+ # Returns +true+ if an error on the attribute with the given type is
447
+ # present, or +false+ otherwise. +type+ is treated the same as for +add+.
448
+ #
449
+ # person.errors.add :age
450
+ # person.errors.add :name, :too_long, { count: 25 }
451
+ # person.errors.of_kind? :age # => true
452
+ # person.errors.of_kind? :name # => false
453
+ # person.errors.of_kind? :name, :too_long # => true
454
+ # person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true
455
+ # person.errors.of_kind? :name, :not_too_long # => false
456
+ # person.errors.of_kind? :name, "is too long" # => false
457
+ def of_kind?(attribute, type = :invalid)
458
+ attribute, type = normalize_arguments(attribute, type)
459
+
460
+ if type.is_a? Symbol
461
+ !where(attribute, type).empty?
462
+ else
463
+ messages_for(attribute).include?(type)
464
+ end
344
465
  end
345
466
 
346
467
  # Returns all the full error messages in an array.
@@ -354,8 +475,9 @@ module ActiveModel
354
475
  # person.errors.full_messages
355
476
  # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
356
477
  def full_messages
357
- map { |attribute, message| full_message(attribute, message) }
478
+ @errors.map(&:full_message)
358
479
  end
480
+ alias :to_a :full_messages
359
481
 
360
482
  # Returns all the full error messages for a given attribute in an array.
361
483
  #
@@ -368,28 +490,35 @@ module ActiveModel
368
490
  # person.errors.full_messages_for(:name)
369
491
  # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
370
492
  def full_messages_for(attribute)
371
- (get(attribute) || []).map { |message| full_message(attribute, message) }
493
+ where(attribute).map(&:full_message).freeze
494
+ end
495
+
496
+ # Returns all the error messages for a given attribute in an array.
497
+ #
498
+ # class Person
499
+ # validates_presence_of :name, :email
500
+ # validates_length_of :name, in: 5..30
501
+ # end
502
+ #
503
+ # person = Person.create()
504
+ # person.errors.messages_for(:name)
505
+ # # => ["is too short (minimum is 5 characters)", "can't be blank"]
506
+ def messages_for(attribute)
507
+ where(attribute).map(&:message)
372
508
  end
373
509
 
374
510
  # Returns a full message for a given attribute.
375
511
  #
376
512
  # person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
377
513
  def full_message(attribute, message)
378
- return message if attribute == :base
379
- attr_name = attribute.to_s.tr('.', '_').humanize
380
- attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
381
- I18n.t(:"errors.format", {
382
- default: "%{attribute} %{message}",
383
- attribute: attr_name,
384
- message: message
385
- })
514
+ Error.full_message(attribute, message, @base)
386
515
  end
387
516
 
388
517
  # Translates an error message in its default scope
389
518
  # (<tt>activemodel.errors.messages</tt>).
390
519
  #
391
- # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>,
392
- # if it's not there, it's looked up in <tt>models.MODEL.MESSAGE</tt> and if
520
+ # Error messages are first looked up in <tt>activemodel.errors.models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>,
521
+ # if it's not there, it's looked up in <tt>activemodel.errors.models.MODEL.MESSAGE</tt> and if
393
522
  # that is not there also, it returns the translation of the default message
394
523
  # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model
395
524
  # name, translated attribute name and the value are available for
@@ -410,48 +539,129 @@ module ActiveModel
410
539
  # * <tt>errors.attributes.title.blank</tt>
411
540
  # * <tt>errors.messages.blank</tt>
412
541
  def generate_message(attribute, type = :invalid, options = {})
413
- type = options.delete(:message) if options[:message].is_a?(Symbol)
542
+ Error.generate_message(attribute, type, @base, options)
543
+ end
544
+
545
+ def marshal_load(array) # :nodoc:
546
+ # Rails 5
547
+ @errors = []
548
+ @base = array[0]
549
+ add_from_legacy_details_hash(array[2])
550
+ end
551
+
552
+ def init_with(coder) # :nodoc:
553
+ data = coder.map
554
+
555
+ data.each { |k, v|
556
+ next if LEGACY_ATTRIBUTES.include?(k.to_sym)
557
+ instance_variable_set(:"@#{k}", v)
558
+ }
559
+
560
+ @errors ||= []
561
+
562
+ # Legacy support Rails 5.x details hash
563
+ add_from_legacy_details_hash(data["details"]) if data.key?("details")
564
+ end
414
565
 
415
- if @base.class.respond_to?(:i18n_scope)
416
- defaults = @base.class.lookup_ancestors.map do |klass|
417
- [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
418
- :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
566
+ private
567
+ def normalize_arguments(attribute, type, **options)
568
+ # Evaluate proc first
569
+ if type.respond_to?(:call)
570
+ type = type.call(@base, options)
419
571
  end
420
- else
421
- defaults = []
572
+
573
+ [attribute.to_sym, type, options]
422
574
  end
423
575
 
424
- defaults << options.delete(:message)
425
- defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
426
- defaults << :"errors.attributes.#{attribute}.#{type}"
427
- defaults << :"errors.messages.#{type}"
576
+ def add_from_legacy_details_hash(details)
577
+ details.each { |attribute, errors|
578
+ errors.each { |error|
579
+ type = error.delete(:error)
580
+ add(attribute, type, **error)
581
+ }
582
+ }
583
+ end
428
584
 
429
- defaults.compact!
430
- defaults.flatten!
585
+ def deprecation_removal_warning(method_name, alternative_message = nil)
586
+ message = +"ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.2."
587
+ if alternative_message
588
+ message << "\n\nTo achieve the same use:\n\n "
589
+ message << alternative_message
590
+ end
591
+ ActiveSupport::Deprecation.warn(message)
592
+ end
431
593
 
432
- key = defaults.shift
433
- value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
594
+ def deprecation_rename_warning(old_method_name, new_method_name)
595
+ ActiveSupport::Deprecation.warn("ActiveModel::Errors##{old_method_name} is deprecated. Please call ##{new_method_name} instead.")
596
+ end
597
+ end
434
598
 
435
- options = {
436
- default: defaults,
437
- model: @base.model_name.human,
438
- attribute: @base.class.human_attribute_name(attribute),
439
- value: value
440
- }.merge!(options)
599
+ class DeprecationHandlingMessageHash < SimpleDelegator
600
+ def initialize(errors)
601
+ @errors = errors
602
+ super(prepare_content)
603
+ end
441
604
 
442
- I18n.translate(key, options)
605
+ def []=(attribute, value)
606
+ ActiveSupport::Deprecation.warn("Calling `[]=` to an ActiveModel::Errors is deprecated. Please call `ActiveModel::Errors#add` instead.")
607
+
608
+ @errors.delete(attribute)
609
+ Array(value).each do |message|
610
+ @errors.add(attribute, message)
611
+ end
612
+
613
+ __setobj__ prepare_content
443
614
  end
444
615
 
445
- private
446
- def normalize_message(attribute, message, options)
447
- case message
448
- when Symbol
449
- generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
450
- when Proc
451
- message.call
452
- else
453
- message
616
+ def delete(attribute)
617
+ ActiveSupport::Deprecation.warn("Calling `delete` to an ActiveModel::Errors messages hash is deprecated. Please call `ActiveModel::Errors#delete` instead.")
618
+
619
+ @errors.delete(attribute)
620
+ end
621
+
622
+ private
623
+ def prepare_content
624
+ content = @errors.to_hash
625
+ content.each do |attribute, value|
626
+ content[attribute] = DeprecationHandlingMessageArray.new(value, @errors, attribute)
627
+ end
628
+ content.default_proc = proc do |hash, attribute|
629
+ hash = hash.dup
630
+ hash[attribute] = DeprecationHandlingMessageArray.new([], @errors, attribute)
631
+ __setobj__ hash.freeze
632
+ hash[attribute]
633
+ end
634
+ content.freeze
454
635
  end
636
+ end
637
+
638
+ class DeprecationHandlingMessageArray < SimpleDelegator
639
+ def initialize(content, errors, attribute)
640
+ @errors = errors
641
+ @attribute = attribute
642
+ super(content.freeze)
643
+ end
644
+
645
+ def <<(message)
646
+ ActiveSupport::Deprecation.warn("Calling `<<` to an ActiveModel::Errors message array in order to add an error is deprecated. Please call `ActiveModel::Errors#add` instead.")
647
+
648
+ @errors.add(@attribute, message)
649
+ __setobj__ @errors.messages_for(@attribute)
650
+ self
651
+ end
652
+
653
+ def clear
654
+ ActiveSupport::Deprecation.warn("Calling `clear` to an ActiveModel::Errors message array in order to delete all errors is deprecated. Please call `ActiveModel::Errors#delete` instead.")
655
+
656
+ @errors.delete(@attribute)
657
+ end
658
+ end
659
+
660
+ class DeprecationHandlingDetailsHash < SimpleDelegator
661
+ def initialize(details)
662
+ details.default = []
663
+ details.freeze
664
+ super(details)
455
665
  end
456
666
  end
457
667
 
@@ -472,4 +682,28 @@ module ActiveModel
472
682
  # # => ActiveModel::StrictValidationFailed: Name can't be blank
473
683
  class StrictValidationFailed < StandardError
474
684
  end
685
+
686
+ # Raised when attribute values are out of range.
687
+ class RangeError < ::RangeError
688
+ end
689
+
690
+ # Raised when unknown attributes are supplied via mass assignment.
691
+ #
692
+ # class Person
693
+ # include ActiveModel::AttributeAssignment
694
+ # include ActiveModel::Validations
695
+ # end
696
+ #
697
+ # person = Person.new
698
+ # person.assign_attributes(name: 'Gorby')
699
+ # # => ActiveModel::UnknownAttributeError: unknown attribute 'name' for Person.
700
+ class UnknownAttributeError < NoMethodError
701
+ attr_reader :record, :attribute
702
+
703
+ def initialize(record, attribute)
704
+ @record = record
705
+ @attribute = attribute
706
+ super("unknown attribute '#{attribute}' for #{@record.class}.")
707
+ end
708
+ end
475
709
  end