activemodel 4.2.11.3 → 5.0.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +149 -56
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +8 -16
  5. data/lib/active_model/attribute_assignment.rb +52 -0
  6. data/lib/active_model/attribute_methods.rb +16 -16
  7. data/lib/active_model/callbacks.rb +3 -3
  8. data/lib/active_model/conversion.rb +5 -5
  9. data/lib/active_model/dirty.rb +41 -40
  10. data/lib/active_model/errors.rb +175 -68
  11. data/lib/active_model/forbidden_attributes_protection.rb +3 -2
  12. data/lib/active_model/gem_version.rb +5 -5
  13. data/lib/active_model/lint.rb +32 -28
  14. data/lib/active_model/locale/en.yml +2 -1
  15. data/lib/active_model/model.rb +3 -4
  16. data/lib/active_model/naming.rb +5 -4
  17. data/lib/active_model/secure_password.rb +2 -9
  18. data/lib/active_model/serialization.rb +36 -9
  19. data/lib/active_model/type/big_integer.rb +13 -0
  20. data/lib/active_model/type/binary.rb +50 -0
  21. data/lib/active_model/type/boolean.rb +21 -0
  22. data/lib/active_model/type/date.rb +54 -0
  23. data/lib/active_model/type/date_time.rb +44 -0
  24. data/lib/active_model/type/decimal.rb +66 -0
  25. data/lib/active_model/type/decimal_without_scale.rb +11 -0
  26. data/lib/active_model/type/float.rb +25 -0
  27. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +35 -0
  28. data/lib/active_model/type/helpers/mutable.rb +18 -0
  29. data/lib/active_model/type/helpers/numeric.rb +34 -0
  30. data/lib/active_model/type/helpers/time_value.rb +77 -0
  31. data/lib/active_model/type/helpers.rb +4 -0
  32. data/lib/active_model/type/immutable_string.rb +29 -0
  33. data/lib/active_model/type/integer.rb +66 -0
  34. data/lib/active_model/type/registry.rb +64 -0
  35. data/lib/active_model/type/string.rb +24 -0
  36. data/lib/active_model/type/text.rb +11 -0
  37. data/lib/active_model/type/time.rb +42 -0
  38. data/lib/active_model/type/unsigned_integer.rb +15 -0
  39. data/lib/active_model/type/value.rb +116 -0
  40. data/lib/active_model/type.rb +59 -0
  41. data/lib/active_model/validations/absence.rb +1 -1
  42. data/lib/active_model/validations/acceptance.rb +57 -9
  43. data/lib/active_model/validations/callbacks.rb +3 -3
  44. data/lib/active_model/validations/clusivity.rb +4 -3
  45. data/lib/active_model/validations/confirmation.rb +16 -4
  46. data/lib/active_model/validations/exclusion.rb +3 -1
  47. data/lib/active_model/validations/format.rb +1 -1
  48. data/lib/active_model/validations/helper_methods.rb +13 -0
  49. data/lib/active_model/validations/inclusion.rb +3 -3
  50. data/lib/active_model/validations/length.rb +49 -18
  51. data/lib/active_model/validations/numericality.rb +20 -11
  52. data/lib/active_model/validations/validates.rb +1 -1
  53. data/lib/active_model/validations/with.rb +0 -10
  54. data/lib/active_model/validations.rb +34 -1
  55. data/lib/active_model/validator.rb +5 -5
  56. data/lib/active_model/version.rb +1 -1
  57. data/lib/active_model.rb +4 -2
  58. metadata +31 -22
  59. data/lib/active_model/serializers/xml.rb +0 -238
@@ -1,6 +1,5 @@
1
1
  require 'active_support/hash_with_indifferent_access'
2
2
  require 'active_support/core_ext/object/duplicable'
3
- require 'active_support/core_ext/string/filters'
4
3
 
5
4
  module ActiveModel
6
5
  # == Active \Model \Dirty
@@ -13,7 +12,7 @@ module ActiveModel
13
12
  # * <tt>include ActiveModel::Dirty</tt> in your object.
14
13
  # * Call <tt>define_attribute_methods</tt> passing each method you want to
15
14
  # track.
16
- # * Call <tt>attr_name_will_change!</tt> before each change to the tracked
15
+ # * Call <tt>[attr_name]_will_change!</tt> before each change to the tracked
17
16
  # attribute.
18
17
  # * Call <tt>changes_applied</tt> after the changes are persisted.
19
18
  # * Call <tt>clear_changes_information</tt> when you want to reset the changes
@@ -81,9 +80,11 @@ module ActiveModel
81
80
  #
82
81
  # Reset the changes:
83
82
  #
84
- # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
83
+ # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
84
+ # person.name_previously_changed? # => true
85
+ # person.name_previous_change # => ["Uncle Bob", "Bill"]
85
86
  # person.reload!
86
- # person.previous_changes # => {}
87
+ # person.previous_changes # => {}
87
88
  #
88
89
  # Rollback the changes:
89
90
  #
@@ -105,10 +106,10 @@ module ActiveModel
105
106
  # person.changes # => {"name" => ["Bill", "Bob"]}
106
107
  #
107
108
  # If an attribute is modified in-place then make use of
108
- # +[attribute_name]_will_change!+ to mark that the attribute is changing.
109
- # Otherwise Active Model can't track changes to in-place attributes. Note
109
+ # <tt>[attribute_name]_will_change!</tt> to mark that the attribute is changing.
110
+ # Otherwise \Active \Model can't track changes to in-place attributes. Note
110
111
  # that Active Record can detect in-place modifications automatically. You do
111
- # not need to call +[attribute_name]_will_change!+ on Active Record models.
112
+ # not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models.
112
113
  #
113
114
  # person.name_will_change!
114
115
  # person.name_change # => ["Bill", "Bill"]
@@ -118,13 +119,16 @@ module ActiveModel
118
119
  extend ActiveSupport::Concern
119
120
  include ActiveModel::AttributeMethods
120
121
 
122
+ OPTION_NOT_GIVEN = Object.new # :nodoc:
123
+ private_constant :OPTION_NOT_GIVEN
124
+
121
125
  included do
122
126
  attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
123
- attribute_method_affix prefix: 'reset_', suffix: '!'
127
+ attribute_method_suffix '_previously_changed?', '_previous_change'
124
128
  attribute_method_affix prefix: 'restore_', suffix: '!'
125
129
  end
126
130
 
127
- # Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
131
+ # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
128
132
  #
129
133
  # person.changed? # => false
130
134
  # person.name = 'bob'
@@ -172,19 +176,23 @@ module ActiveModel
172
176
  @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
173
177
  end
174
178
 
175
- # Handle <tt>*_changed?</tt> for +method_missing+.
176
- def attribute_changed?(attr, options = {}) #:nodoc:
177
- result = changes_include?(attr)
178
- result &&= options[:to] == __send__(attr) if options.key?(:to)
179
- result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
180
- result
179
+ # Handles <tt>*_changed?</tt> for +method_missing+.
180
+ def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
181
+ !!changes_include?(attr) &&
182
+ (to == OPTION_NOT_GIVEN || to == __send__(attr)) &&
183
+ (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
181
184
  end
182
185
 
183
- # Handle <tt>*_was</tt> for +method_missing+.
186
+ # Handles <tt>*_was</tt> for +method_missing+.
184
187
  def attribute_was(attr) # :nodoc:
185
188
  attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
186
189
  end
187
190
 
191
+ # Handles <tt>*_previously_changed?</tt> for +method_missing+.
192
+ def attribute_previously_changed?(attr) #:nodoc:
193
+ previous_changes_include?(attr)
194
+ end
195
+
188
196
  # Restore all previous data of the provided attributes.
189
197
  def restore_attributes(attributes = changed)
190
198
  attributes.each { |attr| restore_attribute! attr }
@@ -192,38 +200,41 @@ module ActiveModel
192
200
 
193
201
  private
194
202
 
203
+ # Returns +true+ if attr_name is changed, +false+ otherwise.
195
204
  def changes_include?(attr_name)
196
205
  attributes_changed_by_setter.include?(attr_name)
197
206
  end
198
207
  alias attribute_changed_by_setter? changes_include?
199
208
 
209
+ # Returns +true+ if attr_name were changed before the model was saved,
210
+ # +false+ otherwise.
211
+ def previous_changes_include?(attr_name)
212
+ previous_changes.include?(attr_name)
213
+ end
214
+
200
215
  # Removes current changes and makes them accessible through +previous_changes+.
201
216
  def changes_applied # :doc:
202
217
  @previously_changed = changes
203
218
  @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
204
219
  end
205
220
 
206
- # Clear all dirty data: current changes and previous changes.
221
+ # Clears all dirty data: current changes and previous changes.
207
222
  def clear_changes_information # :doc:
208
223
  @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
209
224
  @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
210
225
  end
211
226
 
212
- def reset_changes
213
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
214
- `#reset_changes` is deprecated and will be removed on Rails 5.
215
- Please use `#clear_changes_information` instead.
216
- MSG
217
-
218
- clear_changes_information
219
- end
220
-
221
- # Handle <tt>*_change</tt> for +method_missing+.
227
+ # Handles <tt>*_change</tt> for +method_missing+.
222
228
  def attribute_change(attr)
223
229
  [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
224
230
  end
225
231
 
226
- # Handle <tt>*_will_change!</tt> for +method_missing+.
232
+ # Handles <tt>*_previous_change</tt> for +method_missing+.
233
+ def attribute_previous_change(attr)
234
+ previous_changes[attr] if attribute_previously_changed?(attr)
235
+ end
236
+
237
+ # Handles <tt>*_will_change!</tt> for +method_missing+.
227
238
  def attribute_will_change!(attr)
228
239
  return if attribute_changed?(attr)
229
240
 
@@ -236,17 +247,7 @@ module ActiveModel
236
247
  set_attribute_was(attr, value)
237
248
  end
238
249
 
239
- # Handle <tt>reset_*!</tt> for +method_missing+.
240
- def reset_attribute!(attr)
241
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
242
- `#reset_#{attr}!` is deprecated and will be removed on Rails 5.
243
- Please use `#restore_#{attr}!` instead.
244
- MSG
245
-
246
- restore_attribute!(attr)
247
- end
248
-
249
- # Handle <tt>restore_*!</tt> for +method_missing+.
250
+ # Handles <tt>restore_*!</tt> for +method_missing+.
250
251
  def restore_attribute!(attr)
251
252
  if attribute_changed?(attr)
252
253
  __send__("#{attr}=", changed_attributes[attr])
@@ -255,7 +256,7 @@ module ActiveModel
255
256
  end
256
257
 
257
258
  # This is necessary because `changed_attributes` might be overridden in
258
- # other implemntations (e.g. in `ActiveRecord`)
259
+ # other implementations (e.g. in `ActiveRecord`)
259
260
  alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
260
261
 
261
262
  # Force an attribute to have a particular "before" value
@@ -1,7 +1,7 @@
1
- # -*- coding: utf-8 -*-
2
-
3
1
  require 'active_support/core_ext/array/conversions'
4
2
  require 'active_support/core_ext/string/inflections'
3
+ require 'active_support/core_ext/object/deep_dup'
4
+ require 'active_support/core_ext/string/filters'
5
5
 
6
6
  module ActiveModel
7
7
  # == Active \Model \Errors
@@ -23,7 +23,7 @@ module ActiveModel
23
23
  # attr_reader :errors
24
24
  #
25
25
  # def validate!
26
- # errors.add(:name, "cannot be nil") if name.nil?
26
+ # errors.add(:name, :blank, message: "cannot be nil") if name.nil?
27
27
  # end
28
28
  #
29
29
  # # The following methods are needed to be minimally implemented
@@ -32,20 +32,20 @@ module ActiveModel
32
32
  # send(attr)
33
33
  # end
34
34
  #
35
- # def Person.human_attribute_name(attr, options = {})
35
+ # def self.human_attribute_name(attr, options = {})
36
36
  # attr
37
37
  # end
38
38
  #
39
- # def Person.lookup_ancestors
39
+ # def self.lookup_ancestors
40
40
  # [self]
41
41
  # end
42
42
  # end
43
43
  #
44
- # The last three methods are required in your object for Errors to be
44
+ # The last three methods are required in your object for +Errors+ to be
45
45
  # able to generate error messages correctly and also handle multiple
46
- # languages. Of course, if you extend your object with ActiveModel::Translation
46
+ # languages. Of course, if you extend your object with <tt>ActiveModel::Translation</tt>
47
47
  # you will not need to implement the last two. Likewise, using
48
- # ActiveModel::Validations will handle the validation related methods
48
+ # <tt>ActiveModel::Validations</tt> will handle the validation related methods
49
49
  # for you.
50
50
  #
51
51
  # The above allows you to do:
@@ -58,8 +58,9 @@ module ActiveModel
58
58
  include Enumerable
59
59
 
60
60
  CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
61
+ MESSAGE_OPTIONS = [:message]
61
62
 
62
- attr_reader :messages
63
+ attr_reader :messages, :details
63
64
 
64
65
  # Pass in the instance of the object that is using the errors object.
65
66
  #
@@ -70,14 +71,28 @@ module ActiveModel
70
71
  # end
71
72
  def initialize(base)
72
73
  @base = base
73
- @messages = {}
74
+ @messages = apply_default_array({})
75
+ @details = apply_default_array({})
74
76
  end
75
77
 
76
78
  def initialize_dup(other) # :nodoc:
77
79
  @messages = other.messages.dup
80
+ @details = other.details.deep_dup
78
81
  super
79
82
  end
80
83
 
84
+ # Copies the errors from <tt>other</tt>.
85
+ #
86
+ # other - The ActiveModel::Errors instance.
87
+ #
88
+ # Examples
89
+ #
90
+ # person.errors.copy!(other)
91
+ def copy!(other) # :nodoc:
92
+ @messages = other.messages.dup
93
+ @details = other.details.dup
94
+ end
95
+
81
96
  # Clear the error messages.
82
97
  #
83
98
  # person.errors.full_messages # => ["name cannot be nil"]
@@ -85,6 +100,7 @@ module ActiveModel
85
100
  # person.errors.full_messages # => []
86
101
  def clear
87
102
  messages.clear
103
+ details.clear
88
104
  end
89
105
 
90
106
  # Returns +true+ if the error messages include an error for the given key
@@ -94,37 +110,48 @@ module ActiveModel
94
110
  # person.errors.include?(:name) # => true
95
111
  # person.errors.include?(:age) # => false
96
112
  def include?(attribute)
97
- messages[attribute].present?
113
+ messages.key?(attribute) && messages[attribute].present?
98
114
  end
99
- # aliases include?
100
115
  alias :has_key? :include?
101
- # aliases include?
102
116
  alias :key? :include?
103
117
 
104
118
  # Get messages for +key+.
105
119
  #
106
120
  # person.errors.messages # => {:name=>["cannot be nil"]}
107
121
  # person.errors.get(:name) # => ["cannot be nil"]
108
- # person.errors.get(:age) # => nil
122
+ # person.errors.get(:age) # => []
109
123
  def get(key)
124
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
125
+ ActiveModel::Errors#get is deprecated and will be removed in Rails 5.1.
126
+
127
+ To achieve the same use model.errors[:#{key}].
128
+ MESSAGE
129
+
110
130
  messages[key]
111
131
  end
112
132
 
113
133
  # Set messages for +key+ to +value+.
114
134
  #
115
- # person.errors.get(:name) # => ["cannot be nil"]
135
+ # person.errors[:name] # => ["cannot be nil"]
116
136
  # person.errors.set(:name, ["can't be nil"])
117
- # person.errors.get(:name) # => ["can't be nil"]
137
+ # person.errors[:name] # => ["can't be nil"]
118
138
  def set(key, value)
139
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
140
+ ActiveModel::Errors#set is deprecated and will be removed in Rails 5.1.
141
+
142
+ Use model.errors.add(:#{key}, #{value.inspect}) instead.
143
+ MESSAGE
144
+
119
145
  messages[key] = value
120
146
  end
121
147
 
122
148
  # Delete messages for +key+. Returns the deleted messages.
123
149
  #
124
- # person.errors.get(:name) # => ["cannot be nil"]
150
+ # person.errors[:name] # => ["cannot be nil"]
125
151
  # person.errors.delete(:name) # => ["cannot be nil"]
126
- # person.errors.get(:name) # => nil
152
+ # person.errors[:name] # => []
127
153
  def delete(key)
154
+ details.delete(key)
128
155
  messages.delete(key)
129
156
  end
130
157
 
@@ -133,8 +160,17 @@ module ActiveModel
133
160
  #
134
161
  # person.errors[:name] # => ["cannot be nil"]
135
162
  # person.errors['name'] # => ["cannot be nil"]
163
+ #
164
+ # Note that, if you try to get errors of an attribute which has
165
+ # no errors associated with it, this method will instantiate
166
+ # an empty error list for it and +keys+ will return an array
167
+ # of error keys which includes this attribute.
168
+ #
169
+ # person.errors.keys # => []
170
+ # person.errors[:name] # => []
171
+ # person.errors.keys # => [:name]
136
172
  def [](attribute)
137
- get(attribute.to_sym) || set(attribute.to_sym, [])
173
+ messages[attribute.to_sym]
138
174
  end
139
175
 
140
176
  # Adds to the supplied attribute the supplied error message.
@@ -142,38 +178,45 @@ module ActiveModel
142
178
  # person.errors[:name] = "must be set"
143
179
  # person.errors[:name] # => ['must be set']
144
180
  def []=(attribute, error)
145
- self[attribute] << error
181
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
182
+ ActiveModel::Errors#[]= is deprecated and will be removed in Rails 5.1.
183
+
184
+ Use model.errors.add(:#{attribute}, #{error.inspect}) instead.
185
+ MESSAGE
186
+
187
+ messages[attribute.to_sym] << error
146
188
  end
147
189
 
148
190
  # Iterates through each error key, value pair in the error messages hash.
149
191
  # Yields the attribute and the error for that attribute. If the attribute
150
192
  # has more than one error message, yields once for each error message.
151
193
  #
152
- # person.errors.add(:name, "can't be blank")
194
+ # person.errors.add(:name, :blank, message: "can't be blank")
153
195
  # person.errors.each do |attribute, error|
154
196
  # # Will yield :name and "can't be blank"
155
197
  # end
156
198
  #
157
- # person.errors.add(:name, "must be specified")
199
+ # person.errors.add(:name, :not_specified, message: "must be specified")
158
200
  # person.errors.each do |attribute, error|
159
201
  # # Will yield :name and "can't be blank"
160
202
  # # then yield :name and "must be specified"
161
203
  # end
162
204
  def each
163
205
  messages.each_key do |attribute|
164
- self[attribute].each { |error| yield attribute, error }
206
+ messages[attribute].each { |error| yield attribute, error }
165
207
  end
166
208
  end
167
209
 
168
210
  # Returns the number of error messages.
169
211
  #
170
- # person.errors.add(:name, "can't be blank")
212
+ # person.errors.add(:name, :blank, message: "can't be blank")
171
213
  # person.errors.size # => 1
172
- # person.errors.add(:name, "must be specified")
214
+ # person.errors.add(:name, :not_specified, message: "must be specified")
173
215
  # person.errors.size # => 2
174
216
  def size
175
217
  values.flatten.size
176
218
  end
219
+ alias :count :size
177
220
 
178
221
  # Returns all message values.
179
222
  #
@@ -191,40 +234,20 @@ module ActiveModel
191
234
  messages.keys
192
235
  end
193
236
 
194
- # Returns an array of error messages, with the attribute name included.
195
- #
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
201
- end
202
-
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
237
  # Returns +true+ if no errors are found, +false+ otherwise.
214
238
  # If the error message is a string it can be empty.
215
239
  #
216
240
  # person.errors.full_messages # => ["name cannot be nil"]
217
241
  # person.errors.empty? # => false
218
242
  def empty?
219
- all? { |k, v| v && v.empty? && !v.is_a?(String) }
243
+ size.zero?
220
244
  end
221
- # aliases empty?
222
- alias_method :blank?, :empty?
245
+ alias :blank? :empty?
223
246
 
224
247
  # Returns an xml formatted representation of the Errors hash.
225
248
  #
226
- # person.errors.add(:name, "can't be blank")
227
- # person.errors.add(:name, "must be specified")
249
+ # person.errors.add(:name, :blank, message: "can't be blank")
250
+ # person.errors.add(:name, :not_specified, message: "must be specified")
228
251
  # person.errors.to_xml
229
252
  # # =>
230
253
  # # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
@@ -257,21 +280,24 @@ module ActiveModel
257
280
  messages[attribute] = array.map { |message| full_message(attribute, message) }
258
281
  end
259
282
  else
260
- self.messages.dup
283
+ without_default_proc(self.messages)
261
284
  end
262
285
  end
263
286
 
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.
287
+ # Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
288
+ # More than one error can be added to the same +attribute+.
289
+ # If no +message+ is supplied, <tt>:invalid</tt> is assumed.
267
290
  #
268
291
  # person.errors.add(:name)
269
292
  # # => ["is invalid"]
270
- # person.errors.add(:name, 'must be implemented')
293
+ # person.errors.add(:name, :not_implemented, message: "must be implemented")
271
294
  # # => ["is invalid", "must be implemented"]
272
295
  #
273
296
  # person.errors.messages
274
- # # => {:name=>["must be implemented", "is invalid"]}
297
+ # # => {:name=>["is invalid", "must be implemented"]}
298
+ #
299
+ # person.errors.details
300
+ # # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
275
301
  #
276
302
  # If +message+ is a symbol, it will be translated using the appropriate
277
303
  # scope (see +generate_message+).
@@ -283,9 +309,9 @@ module ActiveModel
283
309
  # ActiveModel::StrictValidationFailed instead of adding the error.
284
310
  # <tt>:strict</tt> option can also be set to any other exception.
285
311
  #
286
- # person.errors.add(:name, nil, strict: true)
312
+ # person.errors.add(:name, :invalid, strict: true)
287
313
  # # => ActiveModel::StrictValidationFailed: name is invalid
288
- # person.errors.add(:name, nil, strict: NameIsInvalid)
314
+ # person.errors.add(:name, :invalid, strict: NameIsInvalid)
289
315
  # # => NameIsInvalid: name is invalid
290
316
  #
291
317
  # person.errors.messages # => {}
@@ -293,17 +319,23 @@ module ActiveModel
293
319
  # +attribute+ should be set to <tt>:base</tt> if the error is not
294
320
  # directly associated with a single attribute.
295
321
  #
296
- # person.errors.add(:base, "either name or email must be present")
322
+ # person.errors.add(:base, :name_or_email_blank,
323
+ # message: "either name or email must be present")
297
324
  # person.errors.messages
298
325
  # # => {:base=>["either name or email must be present"]}
326
+ # person.errors.details
327
+ # # => {:base=>[{error: :name_or_email_blank}]}
299
328
  def add(attribute, message = :invalid, options = {})
329
+ message = message.call if message.respond_to?(:call)
330
+ detail = normalize_detail(message, options)
300
331
  message = normalize_message(attribute, message, options)
301
332
  if exception = options[:strict]
302
333
  exception = ActiveModel::StrictValidationFailed if exception == true
303
334
  raise exception, full_message(attribute, message)
304
335
  end
305
336
 
306
- self[attribute] << message
337
+ details[attribute.to_sym] << detail
338
+ messages[attribute.to_sym] << message
307
339
  end
308
340
 
309
341
  # Will add an error message to each of the attributes in +attributes+
@@ -313,6 +345,14 @@ module ActiveModel
313
345
  # person.errors.messages
314
346
  # # => {:name=>["can't be empty"]}
315
347
  def add_on_empty(attributes, options = {})
348
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
349
+ ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1.
350
+
351
+ To achieve the same use:
352
+
353
+ errors.add(attribute, :empty, options) if value.nil? || value.empty?
354
+ MESSAGE
355
+
316
356
  Array(attributes).each do |attribute|
317
357
  value = @base.send(:read_attribute_for_validation, attribute)
318
358
  is_empty = value.respond_to?(:empty?) ? value.empty? : false
@@ -327,6 +367,14 @@ module ActiveModel
327
367
  # person.errors.messages
328
368
  # # => {:name=>["can't be blank"]}
329
369
  def add_on_blank(attributes, options = {})
370
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
371
+ ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1.
372
+
373
+ To achieve the same use:
374
+
375
+ errors.add(attribute, :blank, options) if value.blank?
376
+ MESSAGE
377
+
330
378
  Array(attributes).each do |attribute|
331
379
  value = @base.send(:read_attribute_for_validation, attribute)
332
380
  add(attribute, :blank, options) if value.blank?
@@ -334,11 +382,23 @@ module ActiveModel
334
382
  end
335
383
 
336
384
  # 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+.
385
+ # present, or +false+ otherwise. +message+ is treated the same as for +add+.
338
386
  #
339
387
  # person.errors.add :name, :blank
340
- # person.errors.added? :name, :blank # => true
388
+ # person.errors.added? :name, :blank # => true
389
+ # person.errors.added? :name, "can't be blank" # => true
390
+ #
391
+ # If the error message requires an option, then it returns +true+ with
392
+ # the correct option, or +false+ with an incorrect or missing option.
393
+ #
394
+ # person.errors.add :name, :too_long, { count: 25 }
395
+ # person.errors.added? :name, :too_long, count: 25 # => true
396
+ # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
397
+ # person.errors.added? :name, :too_long, count: 24 # => false
398
+ # person.errors.added? :name, :too_long # => false
399
+ # person.errors.added? :name, "is too long" # => false
341
400
  def added?(attribute, message = :invalid, options = {})
401
+ message = message.call if message.respond_to?(:call)
342
402
  message = normalize_message(attribute, message, options)
343
403
  self[attribute].include? message
344
404
  end
@@ -356,6 +416,7 @@ module ActiveModel
356
416
  def full_messages
357
417
  map { |attribute, message| full_message(attribute, message) }
358
418
  end
419
+ alias :to_a :full_messages
359
420
 
360
421
  # Returns all the full error messages for a given attribute in an array.
361
422
  #
@@ -368,7 +429,7 @@ module ActiveModel
368
429
  # person.errors.full_messages_for(:name)
369
430
  # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
370
431
  def full_messages_for(attribute)
371
- (get(attribute) || []).map { |message| full_message(attribute, message) }
432
+ messages[attribute].map { |message| full_message(attribute, message) }
372
433
  end
373
434
 
374
435
  # Returns a full message for a given attribute.
@@ -388,8 +449,8 @@ module ActiveModel
388
449
  # Translates an error message in its default scope
389
450
  # (<tt>activemodel.errors.messages</tt>).
390
451
  #
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
452
+ # Error messages are first looked up in <tt>activemodel.errors.models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>,
453
+ # if it's not there, it's looked up in <tt>activemodel.errors.models.MODEL.MESSAGE</tt> and if
393
454
  # that is not there also, it returns the translation of the default message
394
455
  # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model
395
456
  # name, translated attribute name and the value are available for
@@ -421,7 +482,6 @@ module ActiveModel
421
482
  defaults = []
422
483
  end
423
484
 
424
- defaults << options.delete(:message)
425
485
  defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
426
486
  defaults << :"errors.attributes.#{attribute}.#{type}"
427
487
  defaults << :"errors.messages.#{type}"
@@ -430,29 +490,61 @@ module ActiveModel
430
490
  defaults.flatten!
431
491
 
432
492
  key = defaults.shift
493
+ defaults = options.delete(:message) if options[:message]
433
494
  value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
434
495
 
435
496
  options = {
436
497
  default: defaults,
437
498
  model: @base.model_name.human,
438
499
  attribute: @base.class.human_attribute_name(attribute),
439
- value: value
500
+ value: value,
501
+ object: @base
440
502
  }.merge!(options)
441
503
 
442
504
  I18n.translate(key, options)
443
505
  end
444
506
 
507
+ def marshal_dump # :nodoc:
508
+ [@base, without_default_proc(@messages), without_default_proc(@details)]
509
+ end
510
+
511
+ def marshal_load(array) # :nodoc:
512
+ @base, @messages, @details = array
513
+ apply_default_array(@messages)
514
+ apply_default_array(@details)
515
+ end
516
+
517
+ def init_with(coder) # :nodoc:
518
+ coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) }
519
+ @details ||= {}
520
+ apply_default_array(@messages)
521
+ apply_default_array(@details)
522
+ end
523
+
445
524
  private
446
525
  def normalize_message(attribute, message, options)
447
526
  case message
448
527
  when Symbol
449
528
  generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
450
- when Proc
451
- message.call
452
529
  else
453
530
  message
454
531
  end
455
532
  end
533
+
534
+ def normalize_detail(message, options)
535
+ { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
536
+ end
537
+
538
+ def without_default_proc(hash)
539
+ hash.dup.tap do |new_h|
540
+ new_h.default_proc = nil
541
+ end
542
+ end
543
+
544
+ def apply_default_array(hash)
545
+ hash.default_proc = proc { |h, key| h[key] = [] }
546
+ hash
547
+ end
456
548
  end
457
549
 
458
550
  # Raised when a validation cannot be corrected by end users and are considered
@@ -472,4 +564,19 @@ module ActiveModel
472
564
  # # => ActiveModel::StrictValidationFailed: Name can't be blank
473
565
  class StrictValidationFailed < StandardError
474
566
  end
567
+
568
+ # Raised when attribute values are out of range.
569
+ class RangeError < ::RangeError
570
+ end
571
+
572
+ # Raised when unknown attributes are supplied via mass assignment.
573
+ class UnknownAttributeError < NoMethodError
574
+ attr_reader :record, :attribute
575
+
576
+ def initialize(record, attribute)
577
+ @record = record
578
+ @attribute = attribute
579
+ super("unknown attribute '#{attribute}' for #{@record.class}.")
580
+ end
581
+ end
475
582
  end