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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +49 -37
- data/MIT-LICENSE +1 -1
- data/README.rdoc +16 -22
- data/lib/active_model/attribute/user_provided_default.rb +51 -0
- data/lib/active_model/attribute.rb +248 -0
- data/lib/active_model/attribute_assignment.rb +55 -0
- data/lib/active_model/attribute_methods.rb +150 -73
- data/lib/active_model/attribute_mutation_tracker.rb +181 -0
- data/lib/active_model/attribute_set/builder.rb +191 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
- data/lib/active_model/attribute_set.rb +106 -0
- data/lib/active_model/attributes.rb +132 -0
- data/lib/active_model/callbacks.rb +31 -25
- data/lib/active_model/conversion.rb +20 -9
- data/lib/active_model/dirty.rb +142 -116
- data/lib/active_model/error.rb +207 -0
- data/lib/active_model/errors.rb +436 -202
- data/lib/active_model/forbidden_attributes_protection.rb +6 -3
- data/lib/active_model/gem_version.rb +5 -3
- data/lib/active_model/lint.rb +47 -42
- data/lib/active_model/locale/en.yml +2 -1
- data/lib/active_model/model.rb +7 -7
- data/lib/active_model/naming.rb +36 -18
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +8 -0
- data/lib/active_model/secure_password.rb +61 -67
- data/lib/active_model/serialization.rb +48 -17
- data/lib/active_model/serializers/json.rb +22 -13
- data/lib/active_model/translation.rb +5 -4
- data/lib/active_model/type/big_integer.rb +14 -0
- data/lib/active_model/type/binary.rb +52 -0
- data/lib/active_model/type/boolean.rb +46 -0
- data/lib/active_model/type/date.rb +52 -0
- data/lib/active_model/type/date_time.rb +46 -0
- data/lib/active_model/type/decimal.rb +69 -0
- data/lib/active_model/type/float.rb +35 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +49 -0
- data/lib/active_model/type/helpers/mutable.rb +20 -0
- data/lib/active_model/type/helpers/numeric.rb +48 -0
- data/lib/active_model/type/helpers/time_value.rb +90 -0
- data/lib/active_model/type/helpers/timezone.rb +19 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/immutable_string.rb +35 -0
- data/lib/active_model/type/integer.rb +67 -0
- data/lib/active_model/type/registry.rb +70 -0
- data/lib/active_model/type/string.rb +35 -0
- data/lib/active_model/type/time.rb +46 -0
- data/lib/active_model/type/value.rb +133 -0
- data/lib/active_model/type.rb +53 -0
- data/lib/active_model/validations/absence.rb +6 -4
- data/lib/active_model/validations/acceptance.rb +72 -14
- data/lib/active_model/validations/callbacks.rb +23 -19
- data/lib/active_model/validations/clusivity.rb +18 -12
- data/lib/active_model/validations/confirmation.rb +27 -14
- data/lib/active_model/validations/exclusion.rb +7 -4
- data/lib/active_model/validations/format.rb +27 -27
- data/lib/active_model/validations/helper_methods.rb +15 -0
- data/lib/active_model/validations/inclusion.rb +8 -7
- data/lib/active_model/validations/length.rb +35 -32
- data/lib/active_model/validations/numericality.rb +72 -34
- data/lib/active_model/validations/presence.rb +3 -3
- data/lib/active_model/validations/validates.rb +17 -15
- data/lib/active_model/validations/with.rb +6 -12
- data/lib/active_model/validations.rb +58 -23
- data/lib/active_model/validator.rb +23 -17
- data/lib/active_model/version.rb +4 -2
- data/lib/active_model.rb +18 -11
- metadata +44 -25
- data/lib/active_model/serializers/xml.rb +0 -238
- data/lib/active_model/test_case.rb +0 -4
data/lib/active_model/errors.rb
CHANGED
@@ -1,12 +1,17 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
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
|
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
|
40
|
+
# def self.human_attribute_name(attr, options = {})
|
36
41
|
# attr
|
37
42
|
# end
|
38
43
|
#
|
39
|
-
# def
|
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
|
-
|
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
|
-
|
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
|
73
|
-
@
|
86
|
+
@base = base
|
87
|
+
@errors = []
|
74
88
|
end
|
75
89
|
|
76
90
|
def initialize_dup(other) # :nodoc:
|
77
|
-
@
|
91
|
+
@errors = other.errors.deep_dup
|
78
92
|
super
|
79
93
|
end
|
80
94
|
|
81
|
-
#
|
95
|
+
# Copies the errors from <tt>other</tt>.
|
96
|
+
# For copying errors but keep <tt>@base</tt> as is.
|
82
97
|
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
|
87
|
-
|
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
|
-
|
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
|
190
|
+
# person.errors[:name] # => ["cannot be nil"]
|
125
191
|
# person.errors.delete(:name) # => ["cannot be nil"]
|
126
|
-
# person.errors
|
127
|
-
def delete(
|
128
|
-
|
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
|
-
|
208
|
+
DeprecationHandlingMessageArray.new(messages_for(attribute), self, attribute)
|
138
209
|
end
|
139
210
|
|
140
|
-
#
|
211
|
+
# Iterates through each error object.
|
141
212
|
#
|
142
|
-
# person.errors
|
143
|
-
# person.errors
|
144
|
-
|
145
|
-
|
146
|
-
end
|
147
|
-
|
148
|
-
#
|
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,
|
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,
|
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
|
-
|
164
|
-
|
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
|
-
|
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
|
-
|
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
|
280
|
+
# Returns all error attribute names
|
195
281
|
#
|
196
|
-
# person.errors.
|
197
|
-
# person.errors.
|
198
|
-
|
199
|
-
|
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
|
-
|
256
|
-
|
257
|
-
|
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
|
-
|
265
|
-
|
266
|
-
|
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
|
-
# #
|
270
|
-
# person.errors.add(:name,
|
271
|
-
# #
|
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=>["
|
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 +
|
378
|
+
# If +type+ is a symbol, it will be translated using the appropriate
|
277
379
|
# scope (see +generate_message+).
|
278
380
|
#
|
279
|
-
# If +
|
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,
|
287
|
-
# # => ActiveModel::StrictValidationFailed:
|
288
|
-
# person.errors.add(:name,
|
289
|
-
# # => NameIsInvalid:
|
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,
|
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
|
-
|
300
|
-
|
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
|
410
|
+
raise exception, error.full_message
|
304
411
|
end
|
305
412
|
|
306
|
-
|
307
|
-
end
|
413
|
+
@errors.append(error)
|
308
414
|
|
309
|
-
|
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
|
-
#
|
324
|
-
# is
|
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.
|
327
|
-
# person.errors.
|
328
|
-
#
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
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
|
337
|
-
# present, +false+ otherwise. +
|
338
|
-
#
|
339
|
-
# person.errors.add :
|
340
|
-
# person.errors.
|
341
|
-
|
342
|
-
|
343
|
-
|
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
|
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
|
-
(
|
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
|
-
|
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
|
-
|
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
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
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
|
-
|
421
|
-
|
572
|
+
|
573
|
+
[attribute.to_sym, type, options]
|
422
574
|
end
|
423
575
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
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
|
-
|
430
|
-
|
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
|
-
|
433
|
-
|
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
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
}.merge!(options)
|
599
|
+
class DeprecationHandlingMessageHash < SimpleDelegator
|
600
|
+
def initialize(errors)
|
601
|
+
@errors = errors
|
602
|
+
super(prepare_content)
|
603
|
+
end
|
441
604
|
|
442
|
-
|
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
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
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
|