activemodel 4.2.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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