activemodel 5.2.3

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +114 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +264 -0
  5. data/lib/active_model.rb +77 -0
  6. data/lib/active_model/attribute.rb +248 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +52 -0
  8. data/lib/active_model/attribute_assignment.rb +57 -0
  9. data/lib/active_model/attribute_methods.rb +478 -0
  10. data/lib/active_model/attribute_mutation_tracker.rb +124 -0
  11. data/lib/active_model/attribute_set.rb +114 -0
  12. data/lib/active_model/attribute_set/builder.rb +126 -0
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +41 -0
  14. data/lib/active_model/attributes.rb +111 -0
  15. data/lib/active_model/callbacks.rb +153 -0
  16. data/lib/active_model/conversion.rb +111 -0
  17. data/lib/active_model/dirty.rb +343 -0
  18. data/lib/active_model/errors.rb +517 -0
  19. data/lib/active_model/forbidden_attributes_protection.rb +31 -0
  20. data/lib/active_model/gem_version.rb +17 -0
  21. data/lib/active_model/lint.rb +118 -0
  22. data/lib/active_model/locale/en.yml +36 -0
  23. data/lib/active_model/model.rb +99 -0
  24. data/lib/active_model/naming.rb +318 -0
  25. data/lib/active_model/railtie.rb +14 -0
  26. data/lib/active_model/secure_password.rb +129 -0
  27. data/lib/active_model/serialization.rb +192 -0
  28. data/lib/active_model/serializers/json.rb +146 -0
  29. data/lib/active_model/translation.rb +70 -0
  30. data/lib/active_model/type.rb +53 -0
  31. data/lib/active_model/type/big_integer.rb +15 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +38 -0
  34. data/lib/active_model/type/date.rb +57 -0
  35. data/lib/active_model/type/date_time.rb +51 -0
  36. data/lib/active_model/type/decimal.rb +70 -0
  37. data/lib/active_model/type/float.rb +36 -0
  38. data/lib/active_model/type/helpers.rb +7 -0
  39. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +41 -0
  40. data/lib/active_model/type/helpers/mutable.rb +20 -0
  41. data/lib/active_model/type/helpers/numeric.rb +37 -0
  42. data/lib/active_model/type/helpers/time_value.rb +68 -0
  43. data/lib/active_model/type/helpers/timezone.rb +19 -0
  44. data/lib/active_model/type/immutable_string.rb +32 -0
  45. data/lib/active_model/type/integer.rb +70 -0
  46. data/lib/active_model/type/registry.rb +70 -0
  47. data/lib/active_model/type/string.rb +26 -0
  48. data/lib/active_model/type/time.rb +51 -0
  49. data/lib/active_model/type/value.rb +126 -0
  50. data/lib/active_model/validations.rb +439 -0
  51. data/lib/active_model/validations/absence.rb +33 -0
  52. data/lib/active_model/validations/acceptance.rb +106 -0
  53. data/lib/active_model/validations/callbacks.rb +122 -0
  54. data/lib/active_model/validations/clusivity.rb +54 -0
  55. data/lib/active_model/validations/confirmation.rb +80 -0
  56. data/lib/active_model/validations/exclusion.rb +49 -0
  57. data/lib/active_model/validations/format.rb +114 -0
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +47 -0
  60. data/lib/active_model/validations/length.rb +129 -0
  61. data/lib/active_model/validations/numericality.rb +189 -0
  62. data/lib/active_model/validations/presence.rb +39 -0
  63. data/lib/active_model/validations/validates.rb +174 -0
  64. data/lib/active_model/validations/with.rb +147 -0
  65. data/lib/active_model/validator.rb +183 -0
  66. data/lib/active_model/version.rb +10 -0
  67. metadata +125 -0
@@ -0,0 +1,517 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/conversions"
4
+ require "active_support/core_ext/string/inflections"
5
+ require "active_support/core_ext/object/deep_dup"
6
+ require "active_support/core_ext/string/filters"
7
+
8
+ module ActiveModel
9
+ # == Active \Model \Errors
10
+ #
11
+ # Provides a modified +Hash+ that you can include in your object
12
+ # for handling error messages and interacting with Action View helpers.
13
+ #
14
+ # A minimal implementation could be:
15
+ #
16
+ # class Person
17
+ # # Required dependency for ActiveModel::Errors
18
+ # extend ActiveModel::Naming
19
+ #
20
+ # def initialize
21
+ # @errors = ActiveModel::Errors.new(self)
22
+ # end
23
+ #
24
+ # attr_accessor :name
25
+ # attr_reader :errors
26
+ #
27
+ # def validate!
28
+ # errors.add(:name, :blank, message: "cannot be nil") if name.nil?
29
+ # end
30
+ #
31
+ # # The following methods are needed to be minimally implemented
32
+ #
33
+ # def read_attribute_for_validation(attr)
34
+ # send(attr)
35
+ # end
36
+ #
37
+ # def self.human_attribute_name(attr, options = {})
38
+ # attr
39
+ # end
40
+ #
41
+ # def self.lookup_ancestors
42
+ # [self]
43
+ # end
44
+ # end
45
+ #
46
+ # The last three methods are required in your object for +Errors+ to be
47
+ # able to generate error messages correctly and also handle multiple
48
+ # languages. Of course, if you extend your object with <tt>ActiveModel::Translation</tt>
49
+ # you will not need to implement the last two. Likewise, using
50
+ # <tt>ActiveModel::Validations</tt> will handle the validation related methods
51
+ # for you.
52
+ #
53
+ # The above allows you to do:
54
+ #
55
+ # person = Person.new
56
+ # person.validate! # => ["cannot be nil"]
57
+ # person.errors.full_messages # => ["name cannot be nil"]
58
+ # # etc..
59
+ class Errors
60
+ include Enumerable
61
+
62
+ CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
63
+ MESSAGE_OPTIONS = [:message]
64
+
65
+ attr_reader :messages, :details
66
+
67
+ # Pass in the instance of the object that is using the errors object.
68
+ #
69
+ # class Person
70
+ # def initialize
71
+ # @errors = ActiveModel::Errors.new(self)
72
+ # end
73
+ # end
74
+ def initialize(base)
75
+ @base = base
76
+ @messages = apply_default_array({})
77
+ @details = apply_default_array({})
78
+ end
79
+
80
+ def initialize_dup(other) # :nodoc:
81
+ @messages = other.messages.dup
82
+ @details = other.details.deep_dup
83
+ super
84
+ end
85
+
86
+ # Copies the errors from <tt>other</tt>.
87
+ #
88
+ # other - The ActiveModel::Errors instance.
89
+ #
90
+ # Examples
91
+ #
92
+ # person.errors.copy!(other)
93
+ def copy!(other) # :nodoc:
94
+ @messages = other.messages.dup
95
+ @details = other.details.dup
96
+ end
97
+
98
+ # Merges the errors from <tt>other</tt>.
99
+ #
100
+ # other - The ActiveModel::Errors instance.
101
+ #
102
+ # Examples
103
+ #
104
+ # person.errors.merge!(other)
105
+ def merge!(other)
106
+ @messages.merge!(other.messages) { |_, ary1, ary2| ary1 + ary2 }
107
+ @details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 }
108
+ end
109
+
110
+ # Clear the error messages.
111
+ #
112
+ # person.errors.full_messages # => ["name cannot be nil"]
113
+ # person.errors.clear
114
+ # person.errors.full_messages # => []
115
+ def clear
116
+ messages.clear
117
+ details.clear
118
+ end
119
+
120
+ # Returns +true+ if the error messages include an error for the given key
121
+ # +attribute+, +false+ otherwise.
122
+ #
123
+ # person.errors.messages # => {:name=>["cannot be nil"]}
124
+ # person.errors.include?(:name) # => true
125
+ # person.errors.include?(:age) # => false
126
+ def include?(attribute)
127
+ attribute = attribute.to_sym
128
+ messages.key?(attribute) && messages[attribute].present?
129
+ end
130
+ alias :has_key? :include?
131
+ alias :key? :include?
132
+
133
+ # Delete messages for +key+. Returns the deleted messages.
134
+ #
135
+ # person.errors[:name] # => ["cannot be nil"]
136
+ # person.errors.delete(:name) # => ["cannot be nil"]
137
+ # person.errors[:name] # => []
138
+ def delete(key)
139
+ attribute = key.to_sym
140
+ details.delete(attribute)
141
+ messages.delete(attribute)
142
+ end
143
+
144
+ # When passed a symbol or a name of a method, returns an array of errors
145
+ # for the method.
146
+ #
147
+ # person.errors[:name] # => ["cannot be nil"]
148
+ # person.errors['name'] # => ["cannot be nil"]
149
+ def [](attribute)
150
+ messages[attribute.to_sym]
151
+ end
152
+
153
+ # Iterates through each error key, value pair in the error messages hash.
154
+ # Yields the attribute and the error for that attribute. If the attribute
155
+ # has more than one error message, yields once for each error message.
156
+ #
157
+ # person.errors.add(:name, :blank, message: "can't be blank")
158
+ # person.errors.each do |attribute, error|
159
+ # # Will yield :name and "can't be blank"
160
+ # end
161
+ #
162
+ # person.errors.add(:name, :not_specified, message: "must be specified")
163
+ # person.errors.each do |attribute, error|
164
+ # # Will yield :name and "can't be blank"
165
+ # # then yield :name and "must be specified"
166
+ # end
167
+ def each
168
+ messages.each_key do |attribute|
169
+ messages[attribute].each { |error| yield attribute, error }
170
+ end
171
+ end
172
+
173
+ # Returns the number of error messages.
174
+ #
175
+ # person.errors.add(:name, :blank, message: "can't be blank")
176
+ # person.errors.size # => 1
177
+ # person.errors.add(:name, :not_specified, message: "must be specified")
178
+ # person.errors.size # => 2
179
+ def size
180
+ values.flatten.size
181
+ end
182
+ alias :count :size
183
+
184
+ # Returns all message values.
185
+ #
186
+ # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
187
+ # person.errors.values # => [["cannot be nil", "must be specified"]]
188
+ def values
189
+ messages.select do |key, value|
190
+ !value.empty?
191
+ end.values
192
+ end
193
+
194
+ # Returns all message keys.
195
+ #
196
+ # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
197
+ # person.errors.keys # => [:name]
198
+ def keys
199
+ messages.select do |key, value|
200
+ !value.empty?
201
+ end.keys
202
+ end
203
+
204
+ # Returns +true+ if no errors are found, +false+ otherwise.
205
+ # If the error message is a string it can be empty.
206
+ #
207
+ # person.errors.full_messages # => ["name cannot be nil"]
208
+ # person.errors.empty? # => false
209
+ def empty?
210
+ size.zero?
211
+ end
212
+ alias :blank? :empty?
213
+
214
+ # Returns an xml formatted representation of the Errors hash.
215
+ #
216
+ # person.errors.add(:name, :blank, message: "can't be blank")
217
+ # person.errors.add(:name, :not_specified, message: "must be specified")
218
+ # person.errors.to_xml
219
+ # # =>
220
+ # # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
221
+ # # <errors>
222
+ # # <error>name can't be blank</error>
223
+ # # <error>name must be specified</error>
224
+ # # </errors>
225
+ def to_xml(options = {})
226
+ to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
227
+ end
228
+
229
+ # Returns a Hash that can be used as the JSON representation for this
230
+ # object. You can pass the <tt>:full_messages</tt> option. This determines
231
+ # if the json object should contain full messages or not (false by default).
232
+ #
233
+ # person.errors.as_json # => {:name=>["cannot be nil"]}
234
+ # person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]}
235
+ def as_json(options = nil)
236
+ to_hash(options && options[:full_messages])
237
+ end
238
+
239
+ # Returns a Hash of attributes with their error messages. If +full_messages+
240
+ # is +true+, it will contain full messages (see +full_message+).
241
+ #
242
+ # person.errors.to_hash # => {:name=>["cannot be nil"]}
243
+ # person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
244
+ def to_hash(full_messages = false)
245
+ if full_messages
246
+ messages.each_with_object({}) do |(attribute, array), messages|
247
+ messages[attribute] = array.map { |message| full_message(attribute, message) }
248
+ end
249
+ else
250
+ without_default_proc(messages)
251
+ end
252
+ end
253
+
254
+ # Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
255
+ # More than one error can be added to the same +attribute+.
256
+ # If no +message+ is supplied, <tt>:invalid</tt> is assumed.
257
+ #
258
+ # person.errors.add(:name)
259
+ # # => ["is invalid"]
260
+ # person.errors.add(:name, :not_implemented, message: "must be implemented")
261
+ # # => ["is invalid", "must be implemented"]
262
+ #
263
+ # person.errors.messages
264
+ # # => {:name=>["is invalid", "must be implemented"]}
265
+ #
266
+ # person.errors.details
267
+ # # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
268
+ #
269
+ # If +message+ is a symbol, it will be translated using the appropriate
270
+ # scope (see +generate_message+).
271
+ #
272
+ # If +message+ is a proc, it will be called, allowing for things like
273
+ # <tt>Time.now</tt> to be used within an error.
274
+ #
275
+ # If the <tt>:strict</tt> option is set to +true+, it will raise
276
+ # ActiveModel::StrictValidationFailed instead of adding the error.
277
+ # <tt>:strict</tt> option can also be set to any other exception.
278
+ #
279
+ # person.errors.add(:name, :invalid, strict: true)
280
+ # # => ActiveModel::StrictValidationFailed: Name is invalid
281
+ # person.errors.add(:name, :invalid, strict: NameIsInvalid)
282
+ # # => NameIsInvalid: Name is invalid
283
+ #
284
+ # person.errors.messages # => {}
285
+ #
286
+ # +attribute+ should be set to <tt>:base</tt> if the error is not
287
+ # directly associated with a single attribute.
288
+ #
289
+ # person.errors.add(:base, :name_or_email_blank,
290
+ # message: "either name or email must be present")
291
+ # person.errors.messages
292
+ # # => {:base=>["either name or email must be present"]}
293
+ # person.errors.details
294
+ # # => {:base=>[{error: :name_or_email_blank}]}
295
+ def add(attribute, message = :invalid, options = {})
296
+ message = message.call if message.respond_to?(:call)
297
+ detail = normalize_detail(message, options)
298
+ message = normalize_message(attribute, message, options)
299
+ if exception = options[:strict]
300
+ exception = ActiveModel::StrictValidationFailed if exception == true
301
+ raise exception, full_message(attribute, message)
302
+ end
303
+
304
+ details[attribute.to_sym] << detail
305
+ messages[attribute.to_sym] << message
306
+ end
307
+
308
+ # Returns +true+ if an error on the attribute with the given message is
309
+ # present, or +false+ otherwise. +message+ is treated the same as for +add+.
310
+ #
311
+ # person.errors.add :name, :blank
312
+ # person.errors.added? :name, :blank # => true
313
+ # person.errors.added? :name, "can't be blank" # => true
314
+ #
315
+ # If the error message requires an option, then it returns +true+ with
316
+ # the correct option, or +false+ with an incorrect or missing option.
317
+ #
318
+ # person.errors.add :name, :too_long, { count: 25 }
319
+ # person.errors.added? :name, :too_long, count: 25 # => true
320
+ # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
321
+ # person.errors.added? :name, :too_long, count: 24 # => false
322
+ # person.errors.added? :name, :too_long # => false
323
+ # person.errors.added? :name, "is too long" # => false
324
+ def added?(attribute, message = :invalid, options = {})
325
+ message = message.call if message.respond_to?(:call)
326
+
327
+ if message.is_a? Symbol
328
+ details[attribute.to_sym].include? normalize_detail(message, options)
329
+ else
330
+ self[attribute].include? message
331
+ end
332
+ end
333
+
334
+ # Returns all the full error messages in an array.
335
+ #
336
+ # class Person
337
+ # validates_presence_of :name, :address, :email
338
+ # validates_length_of :name, in: 5..30
339
+ # end
340
+ #
341
+ # person = Person.create(address: '123 First St.')
342
+ # person.errors.full_messages
343
+ # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
344
+ def full_messages
345
+ map { |attribute, message| full_message(attribute, message) }
346
+ end
347
+ alias :to_a :full_messages
348
+
349
+ # Returns all the full error messages for a given attribute in an array.
350
+ #
351
+ # class Person
352
+ # validates_presence_of :name, :email
353
+ # validates_length_of :name, in: 5..30
354
+ # end
355
+ #
356
+ # person = Person.create()
357
+ # person.errors.full_messages_for(:name)
358
+ # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
359
+ def full_messages_for(attribute)
360
+ attribute = attribute.to_sym
361
+ messages[attribute].map { |message| full_message(attribute, message) }
362
+ end
363
+
364
+ # Returns a full message for a given attribute.
365
+ #
366
+ # person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
367
+ def full_message(attribute, message)
368
+ return message if attribute == :base
369
+ attr_name = attribute.to_s.tr(".", "_").humanize
370
+ attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
371
+ I18n.t(:"errors.format",
372
+ default: "%{attribute} %{message}",
373
+ attribute: attr_name,
374
+ message: message)
375
+ end
376
+
377
+ # Translates an error message in its default scope
378
+ # (<tt>activemodel.errors.messages</tt>).
379
+ #
380
+ # Error messages are first looked up in <tt>activemodel.errors.models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>,
381
+ # if it's not there, it's looked up in <tt>activemodel.errors.models.MODEL.MESSAGE</tt> and if
382
+ # that is not there also, it returns the translation of the default message
383
+ # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model
384
+ # name, translated attribute name and the value are available for
385
+ # interpolation.
386
+ #
387
+ # When using inheritance in your models, it will check all the inherited
388
+ # models too, but only if the model itself hasn't been found. Say you have
389
+ # <tt>class Admin < User; end</tt> and you wanted the translation for
390
+ # the <tt>:blank</tt> error message for the <tt>title</tt> attribute,
391
+ # it looks for these translations:
392
+ #
393
+ # * <tt>activemodel.errors.models.admin.attributes.title.blank</tt>
394
+ # * <tt>activemodel.errors.models.admin.blank</tt>
395
+ # * <tt>activemodel.errors.models.user.attributes.title.blank</tt>
396
+ # * <tt>activemodel.errors.models.user.blank</tt>
397
+ # * any default you provided through the +options+ hash (in the <tt>activemodel.errors</tt> scope)
398
+ # * <tt>activemodel.errors.messages.blank</tt>
399
+ # * <tt>errors.attributes.title.blank</tt>
400
+ # * <tt>errors.messages.blank</tt>
401
+ def generate_message(attribute, type = :invalid, options = {})
402
+ type = options.delete(:message) if options[:message].is_a?(Symbol)
403
+
404
+ if @base.class.respond_to?(:i18n_scope)
405
+ i18n_scope = @base.class.i18n_scope.to_s
406
+ defaults = @base.class.lookup_ancestors.flat_map do |klass|
407
+ [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
408
+ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
409
+ end
410
+ defaults << :"#{i18n_scope}.errors.messages.#{type}"
411
+ else
412
+ defaults = []
413
+ end
414
+
415
+ defaults << :"errors.attributes.#{attribute}.#{type}"
416
+ defaults << :"errors.messages.#{type}"
417
+
418
+ key = defaults.shift
419
+ defaults = options.delete(:message) if options[:message]
420
+ value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
421
+
422
+ options = {
423
+ default: defaults,
424
+ model: @base.model_name.human,
425
+ attribute: @base.class.human_attribute_name(attribute),
426
+ value: value,
427
+ object: @base
428
+ }.merge!(options)
429
+
430
+ I18n.translate(key, options)
431
+ end
432
+
433
+ def marshal_dump # :nodoc:
434
+ [@base, without_default_proc(@messages), without_default_proc(@details)]
435
+ end
436
+
437
+ def marshal_load(array) # :nodoc:
438
+ @base, @messages, @details = array
439
+ apply_default_array(@messages)
440
+ apply_default_array(@details)
441
+ end
442
+
443
+ def init_with(coder) # :nodoc:
444
+ coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) }
445
+ @details ||= {}
446
+ apply_default_array(@messages)
447
+ apply_default_array(@details)
448
+ end
449
+
450
+ private
451
+ def normalize_message(attribute, message, options)
452
+ case message
453
+ when Symbol
454
+ generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
455
+ else
456
+ message
457
+ end
458
+ end
459
+
460
+ def normalize_detail(message, options)
461
+ { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
462
+ end
463
+
464
+ def without_default_proc(hash)
465
+ hash.dup.tap do |new_h|
466
+ new_h.default_proc = nil
467
+ end
468
+ end
469
+
470
+ def apply_default_array(hash)
471
+ hash.default_proc = proc { |h, key| h[key] = [] }
472
+ hash
473
+ end
474
+ end
475
+
476
+ # Raised when a validation cannot be corrected by end users and are considered
477
+ # exceptional.
478
+ #
479
+ # class Person
480
+ # include ActiveModel::Validations
481
+ #
482
+ # attr_accessor :name
483
+ #
484
+ # validates_presence_of :name, strict: true
485
+ # end
486
+ #
487
+ # person = Person.new
488
+ # person.name = nil
489
+ # person.valid?
490
+ # # => ActiveModel::StrictValidationFailed: Name can't be blank
491
+ class StrictValidationFailed < StandardError
492
+ end
493
+
494
+ # Raised when attribute values are out of range.
495
+ class RangeError < ::RangeError
496
+ end
497
+
498
+ # Raised when unknown attributes are supplied via mass assignment.
499
+ #
500
+ # class Person
501
+ # include ActiveModel::AttributeAssignment
502
+ # include ActiveModel::Validations
503
+ # end
504
+ #
505
+ # person = Person.new
506
+ # person.assign_attributes(name: 'Gorby')
507
+ # # => ActiveModel::UnknownAttributeError: unknown attribute 'name' for Person.
508
+ class UnknownAttributeError < NoMethodError
509
+ attr_reader :record, :attribute
510
+
511
+ def initialize(record, attribute)
512
+ @record = record
513
+ @attribute = attribute
514
+ super("unknown attribute '#{attribute}' for #{@record.class}.")
515
+ end
516
+ end
517
+ end