activemodel 3.2.22.5 → 4.0.0.beta1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -64
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +61 -24
  5. data/lib/active_model.rb +21 -11
  6. data/lib/active_model/attribute_methods.rb +150 -125
  7. data/lib/active_model/callbacks.rb +49 -34
  8. data/lib/active_model/conversion.rb +39 -19
  9. data/lib/active_model/deprecated_mass_assignment_security.rb +21 -0
  10. data/lib/active_model/dirty.rb +48 -32
  11. data/lib/active_model/errors.rb +176 -88
  12. data/lib/active_model/forbidden_attributes_protection.rb +27 -0
  13. data/lib/active_model/lint.rb +42 -55
  14. data/lib/active_model/locale/en.yml +3 -1
  15. data/lib/active_model/model.rb +97 -0
  16. data/lib/active_model/naming.rb +191 -51
  17. data/lib/active_model/railtie.rb +11 -1
  18. data/lib/active_model/secure_password.rb +55 -25
  19. data/lib/active_model/serialization.rb +51 -27
  20. data/lib/active_model/serializers/json.rb +83 -46
  21. data/lib/active_model/serializers/xml.rb +46 -12
  22. data/lib/active_model/test_case.rb +0 -12
  23. data/lib/active_model/translation.rb +9 -10
  24. data/lib/active_model/validations.rb +154 -52
  25. data/lib/active_model/validations/absence.rb +31 -0
  26. data/lib/active_model/validations/acceptance.rb +10 -22
  27. data/lib/active_model/validations/callbacks.rb +78 -25
  28. data/lib/active_model/validations/clusivity.rb +41 -0
  29. data/lib/active_model/validations/confirmation.rb +13 -23
  30. data/lib/active_model/validations/exclusion.rb +26 -55
  31. data/lib/active_model/validations/format.rb +44 -34
  32. data/lib/active_model/validations/inclusion.rb +22 -52
  33. data/lib/active_model/validations/length.rb +48 -49
  34. data/lib/active_model/validations/numericality.rb +30 -32
  35. data/lib/active_model/validations/presence.rb +12 -22
  36. data/lib/active_model/validations/validates.rb +68 -36
  37. data/lib/active_model/validations/with.rb +28 -23
  38. data/lib/active_model/validator.rb +22 -22
  39. data/lib/active_model/version.rb +4 -4
  40. metadata +23 -24
  41. data/lib/active_model/mass_assignment_security.rb +0 -237
  42. data/lib/active_model/mass_assignment_security/permission_set.rb +0 -40
  43. data/lib/active_model/mass_assignment_security/sanitizer.rb +0 -59
  44. data/lib/active_model/observer_array.rb +0 -147
  45. data/lib/active_model/observing.rb +0 -252
@@ -1,16 +1,12 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- require 'active_support/core_ext/array/wrap'
4
3
  require 'active_support/core_ext/array/conversions'
5
4
  require 'active_support/core_ext/string/inflections'
6
- require 'active_support/core_ext/object/blank'
7
- require 'active_support/core_ext/hash/reverse_merge'
8
- require 'active_support/ordered_hash'
9
5
 
10
6
  module ActiveModel
11
- # == Active Model Errors
7
+ # == Active \Model \Errors
12
8
  #
13
- # Provides a modified +OrderedHash+ that you can include in your object
9
+ # Provides a modified +Hash+ that you can include in your object
14
10
  # for handling error messages and interacting with Action Pack helpers.
15
11
  #
16
12
  # A minimal implementation could be:
@@ -57,8 +53,8 @@ module ActiveModel
57
53
  # The above allows you to do:
58
54
  #
59
55
  # p = Person.new
60
- # p.validate! # => ["can not be nil"]
61
- # p.errors.full_messages # => ["name can not be nil"]
56
+ # person.validate! # => ["can not be nil"]
57
+ # person.errors.full_messages # => ["name can not be nil"]
62
58
  # # etc..
63
59
  class Errors
64
60
  include Enumerable
@@ -76,44 +72,58 @@ module ActiveModel
76
72
  # end
77
73
  def initialize(base)
78
74
  @base = base
79
- @messages = ActiveSupport::OrderedHash.new
75
+ @messages = {}
80
76
  end
81
77
 
82
- def initialize_dup(other)
78
+ def initialize_dup(other) # :nodoc:
83
79
  @messages = other.messages.dup
80
+ super
84
81
  end
85
82
 
86
- # Backport dup from 1.9 so that #initialize_dup gets called
87
- unless Object.respond_to?(:initialize_dup, true)
88
- def dup # :nodoc:
89
- copy = super
90
- copy.initialize_dup(self)
91
- copy
92
- end
93
- end
94
-
95
- # Clear the messages
83
+ # Clear the error messages.
84
+ #
85
+ # person.errors.full_messages # => ["name can not be nil"]
86
+ # person.errors.clear
87
+ # person.errors.full_messages # => []
96
88
  def clear
97
89
  messages.clear
98
90
  end
99
91
 
100
- # Do the error messages include an error with key +error+?
101
- def include?(error)
102
- (v = messages[error]) && v.any?
92
+ # Returns +true+ if the error messages include an error for the given key
93
+ # +attribute+, +false+ otherwise.
94
+ #
95
+ # person.errors.messages # => {:name=>["can not be nil"]}
96
+ # person.errors.include?(:name) # => true
97
+ # person.errors.include?(:age) # => false
98
+ def include?(attribute)
99
+ (v = messages[attribute]) && v.any?
103
100
  end
101
+ # aliases include?
104
102
  alias :has_key? :include?
105
103
 
106
- # Get messages for +key+
104
+ # Get messages for +key+.
105
+ #
106
+ # person.errors.messages # => {:name=>["can not be nil"]}
107
+ # person.errors.get(:name) # => ["can not be nil"]
108
+ # person.errors.get(:age) # => nil
107
109
  def get(key)
108
110
  messages[key]
109
111
  end
110
112
 
111
- # Set messages for +key+ to +value+
113
+ # Set messages for +key+ to +value+.
114
+ #
115
+ # person.errors.get(:name) # => ["can not be nil"]
116
+ # person.errors.set(:name, ["can't be nil"])
117
+ # person.errors.get(:name) # => ["can't be nil"]
112
118
  def set(key, value)
113
119
  messages[key] = value
114
120
  end
115
121
 
116
- # Delete messages for +key+
122
+ # Delete messages for +key+. Returns the deleted messages.
123
+ #
124
+ # person.errors.get(:name) # => ["can not be nil"]
125
+ # person.errors.delete(:name) # => ["can not be nil"]
126
+ # person.errors.get(:name) # => nil
117
127
  def delete(key)
118
128
  messages.delete(key)
119
129
  end
@@ -121,16 +131,16 @@ module ActiveModel
121
131
  # When passed a symbol or a name of a method, returns an array of errors
122
132
  # for the method.
123
133
  #
124
- # p.errors[:name] # => ["can not be nil"]
125
- # p.errors['name'] # => ["can not be nil"]
134
+ # person.errors[:name] # => ["can not be nil"]
135
+ # person.errors['name'] # => ["can not be nil"]
126
136
  def [](attribute)
127
137
  get(attribute.to_sym) || set(attribute.to_sym, [])
128
138
  end
129
139
 
130
140
  # Adds to the supplied attribute the supplied error message.
131
141
  #
132
- # p.errors[:name] = "must be set"
133
- # p.errors[:name] # => ['must be set']
142
+ # person.errors[:name] = "must be set"
143
+ # person.errors[:name] # => ['must be set']
134
144
  def []=(attribute, error)
135
145
  self[attribute] << error
136
146
  end
@@ -139,13 +149,13 @@ module ActiveModel
139
149
  # Yields the attribute and the error for that attribute. If the attribute
140
150
  # has more than one error message, yields once for each error message.
141
151
  #
142
- # p.errors.add(:name, "can't be blank")
143
- # p.errors.each do |attribute, errors_array|
152
+ # person.errors.add(:name, "can't be blank")
153
+ # person.errors.each do |attribute, error|
144
154
  # # Will yield :name and "can't be blank"
145
155
  # end
146
156
  #
147
- # p.errors.add(:name, "must be specified")
148
- # p.errors.each do |attribute, errors_array|
157
+ # person.errors.add(:name, "must be specified")
158
+ # person.errors.each do |attribute, error|
149
159
  # # Will yield :name and "can't be blank"
150
160
  # # then yield :name and "must be specified"
151
161
  # end
@@ -157,54 +167,65 @@ module ActiveModel
157
167
 
158
168
  # Returns the number of error messages.
159
169
  #
160
- # p.errors.add(:name, "can't be blank")
161
- # p.errors.size # => 1
162
- # p.errors.add(:name, "must be specified")
163
- # p.errors.size # => 2
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
164
174
  def size
165
175
  values.flatten.size
166
176
  end
167
177
 
168
- # Returns all message values
178
+ # Returns all message values.
179
+ #
180
+ # person.errors.messages # => {:name=>["can not be nil", "must be specified"]}
181
+ # person.errors.values # => [["can not be nil", "must be specified"]]
169
182
  def values
170
183
  messages.values
171
184
  end
172
185
 
173
- # Returns all message keys
186
+ # Returns all message keys.
187
+ #
188
+ # person.errors.messages # => {:name=>["can not be nil", "must be specified"]}
189
+ # person.errors.keys # => [:name]
174
190
  def keys
175
191
  messages.keys
176
192
  end
177
193
 
178
- # Returns an array of error messages, with the attribute name included
194
+ # Returns an array of error messages, with the attribute name included.
179
195
  #
180
- # p.errors.add(:name, "can't be blank")
181
- # p.errors.add(:name, "must be specified")
182
- # p.errors.to_a # => ["name can't be blank", "name must be specified"]
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"]
183
199
  def to_a
184
200
  full_messages
185
201
  end
186
202
 
187
203
  # Returns the number of error messages.
188
- # p.errors.add(:name, "can't be blank")
189
- # p.errors.count # => 1
190
- # p.errors.add(:name, "must be specified")
191
- # p.errors.count # => 2
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
192
209
  def count
193
210
  to_a.size
194
211
  end
195
212
 
196
- # Returns true if no errors are found, false otherwise.
213
+ # Returns +true+ if no errors are found, +false+ otherwise.
197
214
  # If the error message is a string it can be empty.
215
+ #
216
+ # person.errors.full_messages # => ["name can not be nil"]
217
+ # person.errors.empty? # => false
198
218
  def empty?
199
219
  all? { |k, v| v && v.empty? && !v.is_a?(String) }
200
220
  end
221
+ # aliases empty?
201
222
  alias_method :blank?, :empty?
202
223
 
203
224
  # Returns an xml formatted representation of the Errors hash.
204
225
  #
205
- # p.errors.add(:name, "can't be blank")
206
- # p.errors.add(:name, "must be specified")
207
- # p.errors.to_xml
226
+ # person.errors.add(:name, "can't be blank")
227
+ # person.errors.add(:name, "must be specified")
228
+ # person.errors.to_xml
208
229
  # # =>
209
230
  # # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
210
231
  # # <errors>
@@ -212,54 +233,106 @@ module ActiveModel
212
233
  # # <error>name must be specified</error>
213
234
  # # </errors>
214
235
  def to_xml(options={})
215
- to_a.to_xml options.reverse_merge(:root => "errors", :skip_types => true)
236
+ to_a.to_xml({ :root => "errors", :skip_types => true }.merge!(options))
216
237
  end
217
238
 
218
- # Returns an ActiveSupport::OrderedHash that can be used as the JSON representation for this object.
239
+ # Returns a Hash that can be used as the JSON representation for this
240
+ # object. You can pass the <tt>:full_messages</tt> option. This determines
241
+ # if the json object should contain full messages or not (false by default).
242
+ #
243
+ # person.as_json # => {:name=>["can not be nil"]}
244
+ # person.as_json(full_messages: true) # => {:name=>["name can not be nil"]}
219
245
  def as_json(options=nil)
220
- to_hash
246
+ to_hash(options && options[:full_messages])
221
247
  end
222
248
 
223
- def to_hash
224
- messages.dup
249
+ # Returns a Hash of attributes with their error messages. If +full_messages+
250
+ # is +true+, it will contain full messages (see +full_message+).
251
+ #
252
+ # person.to_hash # => {:name=>["can not be nil"]}
253
+ # person.to_hash(true) # => {:name=>["name can not be nil"]}
254
+ def to_hash(full_messages = false)
255
+ if full_messages
256
+ messages = {}
257
+ self.messages.each do |attribute, array|
258
+ messages[attribute] = array.map { |message| full_message(attribute, message) }
259
+ end
260
+ messages
261
+ else
262
+ self.messages.dup
263
+ end
225
264
  end
226
265
 
227
- # Adds +message+ to the error messages on +attribute+. More than one error can be added to the same
228
- # +attribute+.
229
- # If no +message+ is supplied, <tt>:invalid</tt> is assumed.
266
+ # Adds +message+ to the error messages on +attribute+. More than one error
267
+ # can be added to the same +attribute+. If no +message+ is supplied,
268
+ # <tt>:invalid</tt> is assumed.
269
+ #
270
+ # person.errors.add(:name)
271
+ # # => ["is invalid"]
272
+ # person.errors.add(:name, 'must be implemented')
273
+ # # => ["is invalid", "must be implemented"]
230
274
  #
231
- # If +message+ is a symbol, it will be translated using the appropriate scope (see +translate_error+).
232
- # If +message+ is a proc, it will be called, allowing for things like <tt>Time.now</tt> to be used within an error.
275
+ # person.errors.messages
276
+ # # => {:name=>["must be implemented", "is invalid"]}
277
+ #
278
+ # If +message+ is a symbol, it will be translated using the appropriate
279
+ # scope (see +generate_message+).
280
+ #
281
+ # If +message+ is a proc, it will be called, allowing for things like
282
+ # <tt>Time.now</tt> to be used within an error.
283
+ #
284
+ # If the <tt>:strict</tt> option is set to true will raise
285
+ # ActiveModel::StrictValidationFailed instead of adding the error.
286
+ # <tt>:strict</tt> option can also be set to any other exception.
287
+ #
288
+ # person.errors.add(:name, nil, strict: true)
289
+ # # => ActiveModel::StrictValidationFailed: name is invalid
290
+ # person.errors.add(:name, nil, strict: NameIsInvalid)
291
+ # # => NameIsInvalid: name is invalid
292
+ #
293
+ # person.errors.messages # => {}
233
294
  def add(attribute, message = nil, options = {})
234
295
  message = normalize_message(attribute, message, options)
235
- if options[:strict]
236
- raise ActiveModel::StrictValidationFailed, full_message(attribute, message)
296
+ if exception = options[:strict]
297
+ exception = ActiveModel::StrictValidationFailed if exception == true
298
+ raise exception, full_message(attribute, message)
237
299
  end
238
300
 
239
301
  self[attribute] << message
240
302
  end
241
303
 
242
- # Will add an error message to each of the attributes in +attributes+ that is empty.
304
+ # Will add an error message to each of the attributes in +attributes+
305
+ # that is empty.
306
+ #
307
+ # person.errors.add_on_empty(:name)
308
+ # person.errors.messages
309
+ # # => {:name=>["can't be empty"]}
243
310
  def add_on_empty(attributes, options = {})
244
- [attributes].flatten.each do |attribute|
311
+ Array(attributes).each do |attribute|
245
312
  value = @base.send(:read_attribute_for_validation, attribute)
246
313
  is_empty = value.respond_to?(:empty?) ? value.empty? : false
247
314
  add(attribute, :empty, options) if value.nil? || is_empty
248
315
  end
249
316
  end
250
317
 
251
- # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
318
+ # Will add an error message to each of the attributes in +attributes+ that
319
+ # is blank (using Object#blank?).
320
+ #
321
+ # person.errors.add_on_blank(:name)
322
+ # person.errors.messages
323
+ # # => {:name=>["can't be blank"]}
252
324
  def add_on_blank(attributes, options = {})
253
- [attributes].flatten.each do |attribute|
325
+ Array(attributes).each do |attribute|
254
326
  value = @base.send(:read_attribute_for_validation, attribute)
255
327
  add(attribute, :blank, options) if value.blank?
256
328
  end
257
329
  end
258
330
 
259
- # Returns true if an error on the attribute with the given message is present, false otherwise.
260
- # +message+ is treated the same as for +add+.
261
- # p.errors.add :name, :blank
262
- # p.errors.added? :name, :blank # => true
331
+ # Returns +true+ if an error on the attribute with the given message is
332
+ # present, +false+ otherwise. +message+ is treated the same as for +add+.
333
+ #
334
+ # person.errors.add :name, :blank
335
+ # person.errors.added? :name, :blank # => true
263
336
  def added?(attribute, message = nil, options = {})
264
337
  message = normalize_message(attribute, message, options)
265
338
  self[attribute].include? message
@@ -267,25 +340,24 @@ module ActiveModel
267
340
 
268
341
  # Returns all the full error messages in an array.
269
342
  #
270
- # class Company
343
+ # class Person
271
344
  # validates_presence_of :name, :address, :email
272
- # validates_length_of :name, :in => 5..30
345
+ # validates_length_of :name, in: 5..30
273
346
  # end
274
347
  #
275
- # company = Company.create(:address => '123 First St.')
276
- # company.errors.full_messages # =>
277
- # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
348
+ # person = Person.create(address: '123 First St.')
349
+ # person.errors.full_messages
350
+ # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
278
351
  def full_messages
279
352
  map { |attribute, message| full_message(attribute, message) }
280
353
  end
281
354
 
282
355
  # Returns a full message for a given attribute.
283
356
  #
284
- # company.errors.full_message(:name, "is invalid") # =>
285
- # "Name is invalid"
357
+ # person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
286
358
  def full_message(attribute, message)
287
359
  return message if attribute == :base
288
- attr_name = attribute.to_s.gsub('.', '_').humanize
360
+ attr_name = attribute.to_s.tr('.', '_').humanize
289
361
  attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
290
362
  I18n.t(:"errors.format", {
291
363
  :default => "%{attribute} %{message}",
@@ -298,10 +370,11 @@ module ActiveModel
298
370
  # (<tt>activemodel.errors.messages</tt>).
299
371
  #
300
372
  # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>,
301
- # if it's not there, it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not
302
- # there also, it returns the translation of the default message
303
- # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model name,
304
- # translated attribute name and the value are available for interpolation.
373
+ # if it's not there, it's looked up in <tt>models.MODEL.MESSAGE</tt> and if
374
+ # that is not there also, it returns the translation of the default message
375
+ # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model
376
+ # name, translated attribute name and the value are available for
377
+ # interpolation.
305
378
  #
306
379
  # When using inheritance in your models, it will check all the inherited
307
380
  # models too, but only if the model itself hasn't been found. Say you have
@@ -317,7 +390,6 @@ module ActiveModel
317
390
  # * <tt>activemodel.errors.messages.blank</tt>
318
391
  # * <tt>errors.attributes.title.blank</tt>
319
392
  # * <tt>errors.messages.blank</tt>
320
- #
321
393
  def generate_message(attribute, type = :invalid, options = {})
322
394
  type = options.delete(:message) if options[:message].is_a?(Symbol)
323
395
 
@@ -346,7 +418,7 @@ module ActiveModel
346
418
  :model => @base.class.model_name.human,
347
419
  :attribute => @base.class.human_attribute_name(attribute),
348
420
  :value => value
349
- }.merge(options)
421
+ }.merge!(options)
350
422
 
351
423
  I18n.translate(key, options)
352
424
  end
@@ -355,9 +427,10 @@ module ActiveModel
355
427
  def normalize_message(attribute, message, options)
356
428
  message ||= :invalid
357
429
 
358
- if message.is_a?(Symbol)
430
+ case message
431
+ when Symbol
359
432
  generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
360
- elsif message.is_a?(Proc)
433
+ when Proc
361
434
  message.call
362
435
  else
363
436
  message
@@ -365,6 +438,21 @@ module ActiveModel
365
438
  end
366
439
  end
367
440
 
441
+ # Raised when a validation cannot be corrected by end users and are considered
442
+ # exceptional.
443
+ #
444
+ # class Person
445
+ # include ActiveModel::Validations
446
+ #
447
+ # attr_accessor :name
448
+ #
449
+ # validates_presence_of :name, strict: true
450
+ # end
451
+ #
452
+ # person = Person.new
453
+ # person.name = nil
454
+ # person.valid?
455
+ # # => ActiveModel::StrictValidationFailed: Name can't be blank
368
456
  class StrictValidationFailed < StandardError
369
457
  end
370
458
  end
@@ -0,0 +1,27 @@
1
+ module ActiveModel
2
+ # Raised when forbidden attributes are used for mass assignment.
3
+ #
4
+ # class Person < ActiveRecord::Base
5
+ # end
6
+ #
7
+ # params = ActionController::Parameters.new(name: 'Bob')
8
+ # Person.new(params)
9
+ # # => ActiveModel::ForbiddenAttributesError
10
+ #
11
+ # params.permit!
12
+ # Person.new(params)
13
+ # # => #<Person id: nil, name: "Bob">
14
+ class ForbiddenAttributesError < StandardError
15
+ end
16
+
17
+ module ForbiddenAttributesProtection # :nodoc:
18
+ protected
19
+ def sanitize_for_mass_assignment(attributes)
20
+ if attributes.respond_to?(:permitted?) && !attributes.permitted?
21
+ raise ActiveModel::ForbiddenAttributesError
22
+ else
23
+ attributes
24
+ end
25
+ end
26
+ end
27
+ end