activemodel 3.2.22.5 → 4.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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