activemodel 4.2.11.3 → 5.0.7.2

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 (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