activemodel 6.0.0

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