activemodel 4.2.11.3 → 5.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activemodel might be problematic. Click here for more details.

Files changed (59) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +84 -93
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +8 -16
  5. data/lib/active_model.rb +3 -2
  6. data/lib/active_model/attribute_assignment.rb +52 -0
  7. data/lib/active_model/attribute_methods.rb +16 -16
  8. data/lib/active_model/callbacks.rb +3 -3
  9. data/lib/active_model/conversion.rb +3 -3
  10. data/lib/active_model/dirty.rb +34 -35
  11. data/lib/active_model/errors.rb +117 -63
  12. data/lib/active_model/forbidden_attributes_protection.rb +3 -2
  13. data/lib/active_model/gem_version.rb +5 -5
  14. data/lib/active_model/lint.rb +32 -28
  15. data/lib/active_model/locale/en.yml +2 -1
  16. data/lib/active_model/model.rb +3 -4
  17. data/lib/active_model/naming.rb +5 -4
  18. data/lib/active_model/secure_password.rb +2 -9
  19. data/lib/active_model/serialization.rb +36 -9
  20. data/lib/active_model/serializers/json.rb +1 -1
  21. data/lib/active_model/type.rb +59 -0
  22. data/lib/active_model/type/big_integer.rb +13 -0
  23. data/lib/active_model/type/binary.rb +50 -0
  24. data/lib/active_model/type/boolean.rb +21 -0
  25. data/lib/active_model/type/date.rb +50 -0
  26. data/lib/active_model/type/date_time.rb +44 -0
  27. data/lib/active_model/type/decimal.rb +52 -0
  28. data/lib/active_model/type/decimal_without_scale.rb +11 -0
  29. data/lib/active_model/type/float.rb +25 -0
  30. data/lib/active_model/type/helpers.rb +4 -0
  31. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +35 -0
  32. data/lib/active_model/type/helpers/mutable.rb +18 -0
  33. data/lib/active_model/type/helpers/numeric.rb +34 -0
  34. data/lib/active_model/type/helpers/time_value.rb +77 -0
  35. data/lib/active_model/type/immutable_string.rb +29 -0
  36. data/lib/active_model/type/integer.rb +66 -0
  37. data/lib/active_model/type/registry.rb +64 -0
  38. data/lib/active_model/type/string.rb +19 -0
  39. data/lib/active_model/type/text.rb +11 -0
  40. data/lib/active_model/type/time.rb +46 -0
  41. data/lib/active_model/type/unsigned_integer.rb +15 -0
  42. data/lib/active_model/type/value.rb +112 -0
  43. data/lib/active_model/validations.rb +35 -3
  44. data/lib/active_model/validations/absence.rb +1 -1
  45. data/lib/active_model/validations/acceptance.rb +61 -9
  46. data/lib/active_model/validations/callbacks.rb +3 -3
  47. data/lib/active_model/validations/confirmation.rb +16 -4
  48. data/lib/active_model/validations/exclusion.rb +3 -1
  49. data/lib/active_model/validations/format.rb +1 -1
  50. data/lib/active_model/validations/helper_methods.rb +13 -0
  51. data/lib/active_model/validations/inclusion.rb +3 -3
  52. data/lib/active_model/validations/length.rb +48 -17
  53. data/lib/active_model/validations/numericality.rb +12 -13
  54. data/lib/active_model/validations/validates.rb +1 -1
  55. data/lib/active_model/validations/with.rb +0 -10
  56. data/lib/active_model/validator.rb +6 -2
  57. data/lib/active_model/version.rb +1 -1
  58. metadata +34 -9
  59. data/lib/active_model/serializers/xml.rb +0 -238
@@ -6,7 +6,7 @@ module ActiveModel
6
6
  # Provides an interface for any class to have Active Record like callbacks.
7
7
  #
8
8
  # Like the Active Record methods, the callback chain is aborted as soon as
9
- # one of the methods in the chain returns +false+.
9
+ # one of the methods throws +:abort+.
10
10
  #
11
11
  # First, extend ActiveModel::Callbacks from the class you are creating:
12
12
  #
@@ -49,7 +49,7 @@ module ActiveModel
49
49
  # puts 'block successfully called.'
50
50
  # end
51
51
  #
52
- # You can choose not to have all three callbacks by passing a hash to the
52
+ # You can choose to have only specific callbacks by passing a hash to the
53
53
  # +define_model_callbacks+ method.
54
54
  #
55
55
  # define_model_callbacks :create, only: [:after, :before]
@@ -103,7 +103,7 @@ module ActiveModel
103
103
  def define_model_callbacks(*callbacks)
104
104
  options = callbacks.extract_options!
105
105
  options = {
106
- terminator: ->(_,result) { result == false },
106
+ terminator: deprecated_false_terminator,
107
107
  skip_after_callbacks_if_terminated: true,
108
108
  scope: [:kind, :name],
109
109
  only: [:before, :around, :after]
@@ -22,7 +22,7 @@ module ActiveModel
22
22
  module Conversion
23
23
  extend ActiveSupport::Concern
24
24
 
25
- # If your object is already designed to implement all of the Active Model
25
+ # If your object is already designed to implement all of the \Active \Model
26
26
  # you can use the default <tt>:to_model</tt> implementation, which simply
27
27
  # returns +self+.
28
28
  #
@@ -33,9 +33,9 @@ module ActiveModel
33
33
  # person = Person.new
34
34
  # person.to_model == person # => true
35
35
  #
36
- # If your model does not act like an Active Model object, then you should
36
+ # If your model does not act like an \Active \Model object, then you should
37
37
  # define <tt>:to_model</tt> yourself returning a proxy object that wraps
38
- # your object with Active Model compliant methods.
38
+ # your object with \Active \Model compliant methods.
39
39
  def to_model
40
40
  self
41
41
  end
@@ -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"]
@@ -120,11 +121,11 @@ module ActiveModel
120
121
 
121
122
  included do
122
123
  attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
123
- attribute_method_affix prefix: 'reset_', suffix: '!'
124
+ attribute_method_suffix '_previously_changed?', '_previous_change'
124
125
  attribute_method_affix prefix: 'restore_', suffix: '!'
125
126
  end
126
127
 
127
- # Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
128
+ # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
128
129
  #
129
130
  # person.changed? # => false
130
131
  # person.name = 'bob'
@@ -172,7 +173,7 @@ module ActiveModel
172
173
  @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
173
174
  end
174
175
 
175
- # Handle <tt>*_changed?</tt> for +method_missing+.
176
+ # Handles <tt>*_changed?</tt> for +method_missing+.
176
177
  def attribute_changed?(attr, options = {}) #:nodoc:
177
178
  result = changes_include?(attr)
178
179
  result &&= options[:to] == __send__(attr) if options.key?(:to)
@@ -180,11 +181,16 @@ module ActiveModel
180
181
  result
181
182
  end
182
183
 
183
- # Handle <tt>*_was</tt> for +method_missing+.
184
+ # Handles <tt>*_was</tt> for +method_missing+.
184
185
  def attribute_was(attr) # :nodoc:
185
186
  attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
186
187
  end
187
188
 
189
+ # Handles <tt>*_previously_changed?</tt> for +method_missing+.
190
+ def attribute_previously_changed?(attr, options = {}) #:nodoc:
191
+ previous_changes_include?(attr)
192
+ end
193
+
188
194
  # Restore all previous data of the provided attributes.
189
195
  def restore_attributes(attributes = changed)
190
196
  attributes.each { |attr| restore_attribute! attr }
@@ -192,38 +198,41 @@ module ActiveModel
192
198
 
193
199
  private
194
200
 
201
+ # Returns +true+ if attr_name is changed, +false+ otherwise.
195
202
  def changes_include?(attr_name)
196
203
  attributes_changed_by_setter.include?(attr_name)
197
204
  end
198
205
  alias attribute_changed_by_setter? changes_include?
199
206
 
207
+ # Returns +true+ if attr_name were changed before the model was saved,
208
+ # +false+ otherwise.
209
+ def previous_changes_include?(attr_name)
210
+ previous_changes.include?(attr_name)
211
+ end
212
+
200
213
  # Removes current changes and makes them accessible through +previous_changes+.
201
214
  def changes_applied # :doc:
202
215
  @previously_changed = changes
203
216
  @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
204
217
  end
205
218
 
206
- # Clear all dirty data: current changes and previous changes.
219
+ # Clears all dirty data: current changes and previous changes.
207
220
  def clear_changes_information # :doc:
208
221
  @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
209
222
  @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
210
223
  end
211
224
 
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+.
225
+ # Handles <tt>*_change</tt> for +method_missing+.
222
226
  def attribute_change(attr)
223
227
  [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
224
228
  end
225
229
 
226
- # Handle <tt>*_will_change!</tt> for +method_missing+.
230
+ # Handles <tt>*_previous_change</tt> for +method_missing+.
231
+ def attribute_previous_change(attr)
232
+ previous_changes[attr] if attribute_previously_changed?(attr)
233
+ end
234
+
235
+ # Handles <tt>*_will_change!</tt> for +method_missing+.
227
236
  def attribute_will_change!(attr)
228
237
  return if attribute_changed?(attr)
229
238
 
@@ -236,17 +245,7 @@ module ActiveModel
236
245
  set_attribute_was(attr, value)
237
246
  end
238
247
 
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+.
248
+ # Handles <tt>restore_*!</tt> for +method_missing+.
250
249
  def restore_attribute!(attr)
251
250
  if attribute_changed?(attr)
252
251
  __send__("#{attr}=", changed_attributes[attr])
@@ -255,7 +254,7 @@ module ActiveModel
255
254
  end
256
255
 
257
256
  # This is necessary because `changed_attributes` might be overridden in
258
- # other implemntations (e.g. in `ActiveRecord`)
257
+ # other implementations (e.g. in `ActiveRecord`)
259
258
  alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
260
259
 
261
260
  # 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 = Hash.new { |messages, attribute| messages[attribute] = [] }
75
+ @details = Hash.new { |details, attribute| details[attribute] = [] }
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
@@ -96,35 +112,46 @@ module ActiveModel
96
112
  def include?(attribute)
97
113
  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
 
@@ -134,7 +161,7 @@ module ActiveModel
134
161
  # person.errors[:name] # => ["cannot be nil"]
135
162
  # person.errors['name'] # => ["cannot be nil"]
136
163
  def [](attribute)
137
- get(attribute.to_sym) || set(attribute.to_sym, [])
164
+ messages[attribute.to_sym]
138
165
  end
139
166
 
140
167
  # Adds to the supplied attribute the supplied error message.
@@ -142,38 +169,45 @@ module ActiveModel
142
169
  # person.errors[:name] = "must be set"
143
170
  # person.errors[:name] # => ['must be set']
144
171
  def []=(attribute, error)
145
- self[attribute] << error
172
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
173
+ ActiveModel::Errors#[]= is deprecated and will be removed in Rails 5.1.
174
+
175
+ Use model.errors.add(:#{attribute}, #{error.inspect}) instead.
176
+ MESSAGE
177
+
178
+ messages[attribute.to_sym] << error
146
179
  end
147
180
 
148
181
  # Iterates through each error key, value pair in the error messages hash.
149
182
  # Yields the attribute and the error for that attribute. If the attribute
150
183
  # has more than one error message, yields once for each error message.
151
184
  #
152
- # person.errors.add(:name, "can't be blank")
185
+ # person.errors.add(:name, :blank, message: "can't be blank")
153
186
  # person.errors.each do |attribute, error|
154
187
  # # Will yield :name and "can't be blank"
155
188
  # end
156
189
  #
157
- # person.errors.add(:name, "must be specified")
190
+ # person.errors.add(:name, :not_specified, message: "must be specified")
158
191
  # person.errors.each do |attribute, error|
159
192
  # # Will yield :name and "can't be blank"
160
193
  # # then yield :name and "must be specified"
161
194
  # end
162
195
  def each
163
196
  messages.each_key do |attribute|
164
- self[attribute].each { |error| yield attribute, error }
197
+ messages[attribute].each { |error| yield attribute, error }
165
198
  end
166
199
  end
167
200
 
168
201
  # Returns the number of error messages.
169
202
  #
170
- # person.errors.add(:name, "can't be blank")
203
+ # person.errors.add(:name, :blank, message: "can't be blank")
171
204
  # person.errors.size # => 1
172
- # person.errors.add(:name, "must be specified")
205
+ # person.errors.add(:name, :not_specified, message: "must be specified")
173
206
  # person.errors.size # => 2
174
207
  def size
175
208
  values.flatten.size
176
209
  end
210
+ alias :count :size
177
211
 
178
212
  # Returns all message values.
179
213
  #
@@ -191,40 +225,20 @@ module ActiveModel
191
225
  messages.keys
192
226
  end
193
227
 
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
228
  # Returns +true+ if no errors are found, +false+ otherwise.
214
229
  # If the error message is a string it can be empty.
215
230
  #
216
231
  # person.errors.full_messages # => ["name cannot be nil"]
217
232
  # person.errors.empty? # => false
218
233
  def empty?
219
- all? { |k, v| v && v.empty? && !v.is_a?(String) }
234
+ size.zero?
220
235
  end
221
- # aliases empty?
222
- alias_method :blank?, :empty?
236
+ alias :blank? :empty?
223
237
 
224
238
  # Returns an xml formatted representation of the Errors hash.
225
239
  #
226
- # person.errors.add(:name, "can't be blank")
227
- # person.errors.add(:name, "must be specified")
240
+ # person.errors.add(:name, :blank, message: "can't be blank")
241
+ # person.errors.add(:name, :not_specified, message: "must be specified")
228
242
  # person.errors.to_xml
229
243
  # # =>
230
244
  # # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
@@ -261,17 +275,20 @@ module ActiveModel
261
275
  end
262
276
  end
263
277
 
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.
278
+ # Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
279
+ # More than one error can be added to the same +attribute+.
280
+ # If no +message+ is supplied, <tt>:invalid</tt> is assumed.
267
281
  #
268
282
  # person.errors.add(:name)
269
283
  # # => ["is invalid"]
270
- # person.errors.add(:name, 'must be implemented')
284
+ # person.errors.add(:name, :not_implemented, message: "must be implemented")
271
285
  # # => ["is invalid", "must be implemented"]
272
286
  #
273
287
  # person.errors.messages
274
- # # => {:name=>["must be implemented", "is invalid"]}
288
+ # # => {:name=>["is invalid", "must be implemented"]}
289
+ #
290
+ # person.errors.details
291
+ # # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
275
292
  #
276
293
  # If +message+ is a symbol, it will be translated using the appropriate
277
294
  # scope (see +generate_message+).
@@ -283,9 +300,9 @@ module ActiveModel
283
300
  # ActiveModel::StrictValidationFailed instead of adding the error.
284
301
  # <tt>:strict</tt> option can also be set to any other exception.
285
302
  #
286
- # person.errors.add(:name, nil, strict: true)
303
+ # person.errors.add(:name, :invalid, strict: true)
287
304
  # # => ActiveModel::StrictValidationFailed: name is invalid
288
- # person.errors.add(:name, nil, strict: NameIsInvalid)
305
+ # person.errors.add(:name, :invalid, strict: NameIsInvalid)
289
306
  # # => NameIsInvalid: name is invalid
290
307
  #
291
308
  # person.errors.messages # => {}
@@ -293,17 +310,23 @@ module ActiveModel
293
310
  # +attribute+ should be set to <tt>:base</tt> if the error is not
294
311
  # directly associated with a single attribute.
295
312
  #
296
- # person.errors.add(:base, "either name or email must be present")
313
+ # person.errors.add(:base, :name_or_email_blank,
314
+ # message: "either name or email must be present")
297
315
  # person.errors.messages
298
316
  # # => {:base=>["either name or email must be present"]}
317
+ # person.errors.details
318
+ # # => {:base=>[{error: :name_or_email_blank}]}
299
319
  def add(attribute, message = :invalid, options = {})
320
+ message = message.call if message.respond_to?(:call)
321
+ detail = normalize_detail(attribute, message, options)
300
322
  message = normalize_message(attribute, message, options)
301
323
  if exception = options[:strict]
302
324
  exception = ActiveModel::StrictValidationFailed if exception == true
303
325
  raise exception, full_message(attribute, message)
304
326
  end
305
327
 
306
- self[attribute] << message
328
+ details[attribute.to_sym] << detail
329
+ messages[attribute.to_sym] << message
307
330
  end
308
331
 
309
332
  # Will add an error message to each of the attributes in +attributes+
@@ -313,6 +336,14 @@ module ActiveModel
313
336
  # person.errors.messages
314
337
  # # => {:name=>["can't be empty"]}
315
338
  def add_on_empty(attributes, options = {})
339
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
340
+ ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1
341
+
342
+ To achieve the same use:
343
+
344
+ errors.add(attribute, :empty, options) if value.nil? || value.empty?
345
+ MESSAGE
346
+
316
347
  Array(attributes).each do |attribute|
317
348
  value = @base.send(:read_attribute_for_validation, attribute)
318
349
  is_empty = value.respond_to?(:empty?) ? value.empty? : false
@@ -327,6 +358,14 @@ module ActiveModel
327
358
  # person.errors.messages
328
359
  # # => {:name=>["can't be blank"]}
329
360
  def add_on_blank(attributes, options = {})
361
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
362
+ ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1
363
+
364
+ To achieve the same use:
365
+
366
+ errors.add(attribute, :empty, options) if value.blank?
367
+ MESSAGE
368
+
330
369
  Array(attributes).each do |attribute|
331
370
  value = @base.send(:read_attribute_for_validation, attribute)
332
371
  add(attribute, :blank, options) if value.blank?
@@ -339,6 +378,7 @@ module ActiveModel
339
378
  # person.errors.add :name, :blank
340
379
  # person.errors.added? :name, :blank # => true
341
380
  def added?(attribute, message = :invalid, options = {})
381
+ message = message.call if message.respond_to?(:call)
342
382
  message = normalize_message(attribute, message, options)
343
383
  self[attribute].include? message
344
384
  end
@@ -356,6 +396,7 @@ module ActiveModel
356
396
  def full_messages
357
397
  map { |attribute, message| full_message(attribute, message) }
358
398
  end
399
+ alias :to_a :full_messages
359
400
 
360
401
  # Returns all the full error messages for a given attribute in an array.
361
402
  #
@@ -368,7 +409,7 @@ module ActiveModel
368
409
  # person.errors.full_messages_for(:name)
369
410
  # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
370
411
  def full_messages_for(attribute)
371
- (get(attribute) || []).map { |message| full_message(attribute, message) }
412
+ messages[attribute].map { |message| full_message(attribute, message) }
372
413
  end
373
414
 
374
415
  # Returns a full message for a given attribute.
@@ -388,8 +429,8 @@ module ActiveModel
388
429
  # Translates an error message in its default scope
389
430
  # (<tt>activemodel.errors.messages</tt>).
390
431
  #
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
432
+ # Error messages are first looked up in <tt>activemodel.errors.models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>,
433
+ # if it's not there, it's looked up in <tt>activemodel.errors.models.MODEL.MESSAGE</tt> and if
393
434
  # that is not there also, it returns the translation of the default message
394
435
  # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model
395
436
  # name, translated attribute name and the value are available for
@@ -421,7 +462,6 @@ module ActiveModel
421
462
  defaults = []
422
463
  end
423
464
 
424
- defaults << options.delete(:message)
425
465
  defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
426
466
  defaults << :"errors.attributes.#{attribute}.#{type}"
427
467
  defaults << :"errors.messages.#{type}"
@@ -430,6 +470,7 @@ module ActiveModel
430
470
  defaults.flatten!
431
471
 
432
472
  key = defaults.shift
473
+ defaults = options.delete(:message) if options[:message]
433
474
  value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
434
475
 
435
476
  options = {
@@ -447,12 +488,14 @@ module ActiveModel
447
488
  case message
448
489
  when Symbol
449
490
  generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
450
- when Proc
451
- message.call
452
491
  else
453
492
  message
454
493
  end
455
494
  end
495
+
496
+ def normalize_detail(attribute, message, options)
497
+ { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
498
+ end
456
499
  end
457
500
 
458
501
  # Raised when a validation cannot be corrected by end users and are considered
@@ -472,4 +515,15 @@ module ActiveModel
472
515
  # # => ActiveModel::StrictValidationFailed: Name can't be blank
473
516
  class StrictValidationFailed < StandardError
474
517
  end
518
+
519
+ # Raised when unknown attributes are supplied via mass assignment.
520
+ class UnknownAttributeError < NoMethodError
521
+ attr_reader :record, :attribute
522
+
523
+ def initialize(record, attribute)
524
+ @record = record
525
+ @attribute = attribute
526
+ super("unknown attribute '#{attribute}' for #{@record.class}.")
527
+ end
528
+ end
475
529
  end