activemodel 5.1.7 → 5.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +32 -93
  3. data/README.rdoc +1 -1
  4. data/lib/active_model.rb +6 -1
  5. data/lib/active_model/attribute.rb +243 -0
  6. data/lib/active_model/attribute/user_provided_default.rb +30 -0
  7. data/lib/active_model/attribute_assignment.rb +8 -5
  8. data/lib/active_model/attribute_methods.rb +12 -10
  9. data/lib/active_model/attribute_mutation_tracker.rb +116 -0
  10. data/lib/active_model/attribute_set.rb +113 -0
  11. data/lib/active_model/attribute_set/builder.rb +124 -0
  12. data/lib/active_model/attribute_set/yaml_encoder.rb +41 -0
  13. data/lib/active_model/attributes.rb +108 -0
  14. data/lib/active_model/callbacks.rb +7 -2
  15. data/lib/active_model/conversion.rb +2 -0
  16. data/lib/active_model/dirty.rb +124 -57
  17. data/lib/active_model/errors.rb +32 -21
  18. data/lib/active_model/forbidden_attributes_protection.rb +2 -0
  19. data/lib/active_model/gem_version.rb +5 -3
  20. data/lib/active_model/lint.rb +2 -0
  21. data/lib/active_model/model.rb +2 -0
  22. data/lib/active_model/naming.rb +5 -3
  23. data/lib/active_model/railtie.rb +2 -0
  24. data/lib/active_model/secure_password.rb +5 -3
  25. data/lib/active_model/serialization.rb +2 -0
  26. data/lib/active_model/serializers/json.rb +3 -2
  27. data/lib/active_model/translation.rb +2 -0
  28. data/lib/active_model/type.rb +6 -0
  29. data/lib/active_model/type/big_integer.rb +2 -0
  30. data/lib/active_model/type/binary.rb +2 -0
  31. data/lib/active_model/type/boolean.rb +2 -0
  32. data/lib/active_model/type/date.rb +2 -0
  33. data/lib/active_model/type/date_time.rb +6 -0
  34. data/lib/active_model/type/decimal.rb +2 -0
  35. data/lib/active_model/type/float.rb +2 -0
  36. data/lib/active_model/type/helpers.rb +2 -0
  37. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +6 -0
  38. data/lib/active_model/type/helpers/mutable.rb +2 -0
  39. data/lib/active_model/type/helpers/numeric.rb +2 -0
  40. data/lib/active_model/type/helpers/time_value.rb +2 -1
  41. data/lib/active_model/type/immutable_string.rb +2 -0
  42. data/lib/active_model/type/integer.rb +3 -1
  43. data/lib/active_model/type/registry.rb +2 -0
  44. data/lib/active_model/type/string.rb +2 -0
  45. data/lib/active_model/type/time.rb +8 -4
  46. data/lib/active_model/type/value.rb +3 -1
  47. data/lib/active_model/validations.rb +7 -3
  48. data/lib/active_model/validations/absence.rb +2 -0
  49. data/lib/active_model/validations/acceptance.rb +2 -0
  50. data/lib/active_model/validations/callbacks.rb +11 -13
  51. data/lib/active_model/validations/clusivity.rb +2 -0
  52. data/lib/active_model/validations/confirmation.rb +3 -1
  53. data/lib/active_model/validations/exclusion.rb +2 -0
  54. data/lib/active_model/validations/format.rb +1 -0
  55. data/lib/active_model/validations/helper_methods.rb +2 -0
  56. data/lib/active_model/validations/inclusion.rb +2 -0
  57. data/lib/active_model/validations/length.rb +10 -2
  58. data/lib/active_model/validations/numericality.rb +3 -1
  59. data/lib/active_model/validations/presence.rb +1 -0
  60. data/lib/active_model/validations/validates.rb +4 -3
  61. data/lib/active_model/validations/with.rb +2 -0
  62. data/lib/active_model/validator.rb +6 -4
  63. data/lib/active_model/version.rb +2 -0
  64. metadata +17 -9
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/deep_dup"
4
+ require "active_model/attribute_set"
5
+ require "active_model/attribute/user_provided_default"
6
+
7
+ module ActiveModel
8
+ module Attributes #:nodoc:
9
+ extend ActiveSupport::Concern
10
+ include ActiveModel::AttributeMethods
11
+
12
+ included do
13
+ attribute_method_suffix "="
14
+ class_attribute :attribute_types, :_default_attributes, instance_accessor: false
15
+ self.attribute_types = Hash.new(Type.default_value)
16
+ self._default_attributes = AttributeSet.new({})
17
+ end
18
+
19
+ module ClassMethods
20
+ def attribute(name, type = Type::Value.new, **options)
21
+ name = name.to_s
22
+ if type.is_a?(Symbol)
23
+ type = ActiveModel::Type.lookup(type, **options.except(:default))
24
+ end
25
+ self.attribute_types = attribute_types.merge(name => type)
26
+ define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type)
27
+ define_attribute_methods(name)
28
+ end
29
+
30
+ private
31
+
32
+ def define_method_attribute=(name)
33
+ safe_name = name.unpack("h*".freeze).first
34
+ ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name
35
+
36
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
37
+ def __temp__#{safe_name}=(value)
38
+ name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name}
39
+ write_attribute(name, value)
40
+ end
41
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
42
+ undef_method :__temp__#{safe_name}=
43
+ STR
44
+ end
45
+
46
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
47
+ private_constant :NO_DEFAULT_PROVIDED
48
+
49
+ def define_default_attribute(name, value, type)
50
+ self._default_attributes = _default_attributes.deep_dup
51
+ if value == NO_DEFAULT_PROVIDED
52
+ default_attribute = _default_attributes[name].with_type(type)
53
+ else
54
+ default_attribute = Attribute::UserProvidedDefault.new(
55
+ name,
56
+ value,
57
+ type,
58
+ _default_attributes.fetch(name.to_s) { nil },
59
+ )
60
+ end
61
+ _default_attributes[name] = default_attribute
62
+ end
63
+ end
64
+
65
+ def initialize(*)
66
+ @attributes = self.class._default_attributes.deep_dup
67
+ super
68
+ end
69
+
70
+ private
71
+
72
+ def write_attribute(attr_name, value)
73
+ name = if self.class.attribute_alias?(attr_name)
74
+ self.class.attribute_alias(attr_name).to_s
75
+ else
76
+ attr_name.to_s
77
+ end
78
+
79
+ @attributes.write_from_user(name, value)
80
+ value
81
+ end
82
+
83
+ def attribute(attr_name)
84
+ name = if self.class.attribute_alias?(attr_name)
85
+ self.class.attribute_alias(attr_name).to_s
86
+ else
87
+ attr_name.to_s
88
+ end
89
+ @attributes.fetch_value(name)
90
+ end
91
+
92
+ # Handle *= for method_missing.
93
+ def attribute=(attribute_name, value)
94
+ write_attribute(attribute_name, value)
95
+ end
96
+ end
97
+
98
+ module AttributeMethods #:nodoc:
99
+ AttrNames = Module.new {
100
+ def self.set_name_cache(name, value)
101
+ const_name = "ATTR_#{name}"
102
+ unless const_defined? const_name
103
+ const_set const_name, value.dup.freeze
104
+ end
105
+ end
106
+ }
107
+ end
108
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/array/extract_options"
2
4
 
3
5
  module ActiveModel
@@ -56,6 +58,9 @@ module ActiveModel
56
58
  #
57
59
  # Would only create the +after_create+ and +before_create+ callback methods in
58
60
  # your class.
61
+ #
62
+ # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
63
+ #
59
64
  module Callbacks
60
65
  def self.extended(base) #:nodoc:
61
66
  base.class_eval do
@@ -98,8 +103,8 @@ module ActiveModel
98
103
  # end
99
104
  # end
100
105
  #
101
- # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
102
- # `!`, `?` or `=`.
106
+ # NOTE: +method_name+ passed to define_model_callbacks must not end with
107
+ # <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
103
108
  def define_model_callbacks(*callbacks)
104
109
  options = callbacks.extract_options!
105
110
  options = {
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveModel
2
4
  # == Active \Model \Conversion
3
5
  #
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/hash_with_indifferent_access"
2
4
  require "active_support/core_ext/object/duplicable"
5
+ require "active_model/attribute_mutation_tracker"
3
6
 
4
7
  module ActiveModel
5
8
  # == Active \Model \Dirty
@@ -128,6 +131,24 @@ module ActiveModel
128
131
  attribute_method_affix prefix: "restore_", suffix: "!"
129
132
  end
130
133
 
134
+ def initialize_dup(other) # :nodoc:
135
+ super
136
+ if self.class.respond_to?(:_default_attributes)
137
+ @attributes = self.class._default_attributes.map do |attr|
138
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
139
+ end
140
+ end
141
+ @mutations_from_database = nil
142
+ end
143
+
144
+ def changes_applied # :nodoc:
145
+ @previously_changed = changes
146
+ @mutations_before_last_save = mutations_from_database
147
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
148
+ forget_attribute_assignments
149
+ @mutations_from_database = nil
150
+ end
151
+
131
152
  # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
132
153
  #
133
154
  # person.changed? # => false
@@ -146,6 +167,60 @@ module ActiveModel
146
167
  changed_attributes.keys
147
168
  end
148
169
 
170
+ # Handles <tt>*_changed?</tt> for +method_missing+.
171
+ def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
172
+ !!changes_include?(attr) &&
173
+ (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
174
+ (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
175
+ end
176
+
177
+ # Handles <tt>*_was</tt> for +method_missing+.
178
+ def attribute_was(attr) # :nodoc:
179
+ attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
180
+ end
181
+
182
+ # Handles <tt>*_previously_changed?</tt> for +method_missing+.
183
+ def attribute_previously_changed?(attr) #:nodoc:
184
+ previous_changes_include?(attr)
185
+ end
186
+
187
+ # Restore all previous data of the provided attributes.
188
+ def restore_attributes(attributes = changed)
189
+ attributes.each { |attr| restore_attribute! attr }
190
+ end
191
+
192
+ # Clears all dirty data: current changes and previous changes.
193
+ def clear_changes_information
194
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
195
+ @mutations_before_last_save = nil
196
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
197
+ forget_attribute_assignments
198
+ @mutations_from_database = nil
199
+ end
200
+
201
+ def clear_attribute_changes(attr_names)
202
+ attributes_changed_by_setter.except!(*attr_names)
203
+ attr_names.each do |attr_name|
204
+ clear_attribute_change(attr_name)
205
+ end
206
+ end
207
+
208
+ # Returns a hash of the attributes with unsaved changes indicating their original
209
+ # values like <tt>attr => original value</tt>.
210
+ #
211
+ # person.name # => "bob"
212
+ # person.name = 'robert'
213
+ # person.changed_attributes # => {"name" => "bob"}
214
+ def changed_attributes
215
+ # This should only be set by methods which will call changed_attributes
216
+ # multiple times when it is known that the computed value cannot change.
217
+ if defined?(@cached_changed_attributes)
218
+ @cached_changed_attributes
219
+ else
220
+ attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
221
+ end
222
+ end
223
+
149
224
  # Returns a hash of changed attributes indicating their original
150
225
  # and new values like <tt>attr => [original value, new value]</tt>.
151
226
  #
@@ -153,7 +228,9 @@ module ActiveModel
153
228
  # person.name = 'bob'
154
229
  # person.changes # => { "name" => ["bill", "bob"] }
155
230
  def changes
156
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
231
+ cache_changed_attributes do
232
+ ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
233
+ end
157
234
  end
158
235
 
159
236
  # Returns a hash of attributes that were changed before the model was saved.
@@ -164,45 +241,51 @@ module ActiveModel
164
241
  # person.previous_changes # => {"name" => ["bob", "robert"]}
165
242
  def previous_changes
166
243
  @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
244
+ @previously_changed.merge(mutations_before_last_save.changes)
167
245
  end
168
246
 
169
- # Returns a hash of the attributes with unsaved changes indicating their original
170
- # values like <tt>attr => original value</tt>.
171
- #
172
- # person.name # => "bob"
173
- # person.name = 'robert'
174
- # person.changed_attributes # => {"name" => "bob"}
175
- def changed_attributes
176
- @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
247
+ def attribute_changed_in_place?(attr_name) # :nodoc:
248
+ mutations_from_database.changed_in_place?(attr_name)
177
249
  end
178
250
 
179
- # Handles <tt>*_changed?</tt> for +method_missing+.
180
- def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
181
- !!changes_include?(attr) &&
182
- (to == OPTION_NOT_GIVEN || to == __send__(attr)) &&
183
- (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
184
- end
251
+ private
252
+ def clear_attribute_change(attr_name)
253
+ mutations_from_database.forget_change(attr_name)
254
+ end
185
255
 
186
- # Handles <tt>*_was</tt> for +method_missing+.
187
- def attribute_was(attr) # :nodoc:
188
- attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
189
- end
256
+ def mutations_from_database
257
+ unless defined?(@mutations_from_database)
258
+ @mutations_from_database = nil
259
+ end
260
+ @mutations_from_database ||= if defined?(@attributes)
261
+ ActiveModel::AttributeMutationTracker.new(@attributes)
262
+ else
263
+ NullMutationTracker.instance
264
+ end
265
+ end
190
266
 
191
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
192
- def attribute_previously_changed?(attr) #:nodoc:
193
- previous_changes_include?(attr)
194
- end
267
+ def forget_attribute_assignments
268
+ @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
269
+ end
195
270
 
196
- # Restore all previous data of the provided attributes.
197
- def restore_attributes(attributes = changed)
198
- attributes.each { |attr| restore_attribute! attr }
199
- end
271
+ def mutations_before_last_save
272
+ @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
273
+ end
200
274
 
201
- private
275
+ def cache_changed_attributes
276
+ @cached_changed_attributes = changed_attributes
277
+ yield
278
+ ensure
279
+ clear_changed_attributes_cache
280
+ end
281
+
282
+ def clear_changed_attributes_cache
283
+ remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
284
+ end
202
285
 
203
286
  # Returns +true+ if attr_name is changed, +false+ otherwise.
204
287
  def changes_include?(attr_name)
205
- attributes_changed_by_setter.include?(attr_name)
288
+ attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
206
289
  end
207
290
  alias attribute_changed_by_setter? changes_include?
208
291
 
@@ -212,21 +295,9 @@ module ActiveModel
212
295
  previous_changes.include?(attr_name)
213
296
  end
214
297
 
215
- # Removes current changes and makes them accessible through +previous_changes+.
216
- def changes_applied # :doc:
217
- @previously_changed = changes
218
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
219
- end
220
-
221
- # Clears all dirty data: current changes and previous changes.
222
- def clear_changes_information # :doc:
223
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
224
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
225
- end
226
-
227
298
  # Handles <tt>*_change</tt> for +method_missing+.
228
299
  def attribute_change(attr)
229
- [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
300
+ [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
230
301
  end
231
302
 
232
303
  # Handles <tt>*_previous_change</tt> for +method_missing+.
@@ -236,15 +307,16 @@ module ActiveModel
236
307
 
237
308
  # Handles <tt>*_will_change!</tt> for +method_missing+.
238
309
  def attribute_will_change!(attr)
239
- return if attribute_changed?(attr)
310
+ unless attribute_changed?(attr)
311
+ begin
312
+ value = _read_attribute(attr)
313
+ value = value.duplicable? ? value.clone : value
314
+ rescue TypeError, NoMethodError
315
+ end
240
316
 
241
- begin
242
- value = __send__(attr)
243
- value = value.duplicable? ? value.clone : value
244
- rescue TypeError, NoMethodError
317
+ set_attribute_was(attr, value)
245
318
  end
246
-
247
- set_attribute_was(attr, value)
319
+ mutations_from_database.force_change(attr)
248
320
  end
249
321
 
250
322
  # Handles <tt>restore_*!</tt> for +method_missing+.
@@ -255,18 +327,13 @@ module ActiveModel
255
327
  end
256
328
  end
257
329
 
258
- # This is necessary because `changed_attributes` might be overridden in
259
- # other implementations (e.g. in `ActiveRecord`)
260
- alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
330
+ def attributes_changed_by_setter
331
+ @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
332
+ end
261
333
 
262
334
  # Force an attribute to have a particular "before" value
263
335
  def set_attribute_was(attr, old_value)
264
336
  attributes_changed_by_setter[attr] = old_value
265
337
  end
266
-
267
- # Remove changes information for the provided attributes.
268
- def clear_attribute_changes(attributes) # :doc:
269
- attributes_changed_by_setter.except!(*attributes)
270
- end
271
338
  end
272
339
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/array/conversions"
2
4
  require "active_support/core_ext/string/inflections"
3
5
  require "active_support/core_ext/object/deep_dup"
@@ -93,6 +95,18 @@ module ActiveModel
93
95
  @details = other.details.dup
94
96
  end
95
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
+
96
110
  # Clear the error messages.
97
111
  #
98
112
  # person.errors.full_messages # => ["name cannot be nil"]
@@ -132,15 +146,6 @@ module ActiveModel
132
146
  #
133
147
  # person.errors[:name] # => ["cannot be nil"]
134
148
  # person.errors['name'] # => ["cannot be nil"]
135
- #
136
- # Note that, if you try to get errors of an attribute which has
137
- # no errors associated with it, this method will instantiate
138
- # an empty error list for it and +keys+ will return an array
139
- # of error keys which includes this attribute.
140
- #
141
- # person.errors.keys # => []
142
- # person.errors[:name] # => []
143
- # person.errors.keys # => [:name]
144
149
  def [](attribute)
145
150
  messages[attribute.to_sym]
146
151
  end
@@ -181,7 +186,9 @@ module ActiveModel
181
186
  # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
182
187
  # person.errors.values # => [["cannot be nil", "must be specified"]]
183
188
  def values
184
- messages.values
189
+ messages.select do |key, value|
190
+ !value.empty?
191
+ end.values
185
192
  end
186
193
 
187
194
  # Returns all message keys.
@@ -189,7 +196,9 @@ module ActiveModel
189
196
  # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
190
197
  # person.errors.keys # => [:name]
191
198
  def keys
192
- messages.keys
199
+ messages.select do |key, value|
200
+ !value.empty?
201
+ end.keys
193
202
  end
194
203
 
195
204
  # Returns +true+ if no errors are found, +false+ otherwise.
@@ -313,9 +322,13 @@ module ActiveModel
313
322
  # person.errors.added? :name, :too_long # => false
314
323
  # person.errors.added? :name, "is too long" # => false
315
324
  def added?(attribute, message = :invalid, options = {})
316
- message = message.call if message.respond_to?(:call)
317
- message = normalize_message(attribute, message, options)
318
- self[attribute].include? message
325
+ if message.is_a? Symbol
326
+ self.details[attribute].map { |e| e[:error] }.include? message
327
+ else
328
+ message = message.call if message.respond_to?(:call)
329
+ message = normalize_message(attribute, message, options)
330
+ self[attribute].include? message
331
+ end
319
332
  end
320
333
 
321
334
  # Returns all the full error messages in an array.
@@ -389,21 +402,19 @@ module ActiveModel
389
402
  type = options.delete(:message) if options[:message].is_a?(Symbol)
390
403
 
391
404
  if @base.class.respond_to?(:i18n_scope)
392
- defaults = @base.class.lookup_ancestors.map do |klass|
393
- [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
394
- :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
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}" ]
395
409
  end
410
+ defaults << :"#{i18n_scope}.errors.messages.#{type}"
396
411
  else
397
412
  defaults = []
398
413
  end
399
414
 
400
- defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
401
415
  defaults << :"errors.attributes.#{attribute}.#{type}"
402
416
  defaults << :"errors.messages.#{type}"
403
417
 
404
- defaults.compact!
405
- defaults.flatten!
406
-
407
418
  key = defaults.shift
408
419
  defaults = options.delete(:message) if options[:message]
409
420
  value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)