activemodel 6.0.0

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 +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