activemodel 4.2.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +49 -37
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +16 -22
  5. data/lib/active_model/attribute/user_provided_default.rb +51 -0
  6. data/lib/active_model/attribute.rb +248 -0
  7. data/lib/active_model/attribute_assignment.rb +55 -0
  8. data/lib/active_model/attribute_methods.rb +150 -73
  9. data/lib/active_model/attribute_mutation_tracker.rb +181 -0
  10. data/lib/active_model/attribute_set/builder.rb +191 -0
  11. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  12. data/lib/active_model/attribute_set.rb +106 -0
  13. data/lib/active_model/attributes.rb +132 -0
  14. data/lib/active_model/callbacks.rb +31 -25
  15. data/lib/active_model/conversion.rb +20 -9
  16. data/lib/active_model/dirty.rb +142 -116
  17. data/lib/active_model/error.rb +207 -0
  18. data/lib/active_model/errors.rb +436 -202
  19. data/lib/active_model/forbidden_attributes_protection.rb +6 -3
  20. data/lib/active_model/gem_version.rb +5 -3
  21. data/lib/active_model/lint.rb +47 -42
  22. data/lib/active_model/locale/en.yml +2 -1
  23. data/lib/active_model/model.rb +7 -7
  24. data/lib/active_model/naming.rb +36 -18
  25. data/lib/active_model/nested_error.rb +22 -0
  26. data/lib/active_model/railtie.rb +8 -0
  27. data/lib/active_model/secure_password.rb +61 -67
  28. data/lib/active_model/serialization.rb +48 -17
  29. data/lib/active_model/serializers/json.rb +22 -13
  30. data/lib/active_model/translation.rb +5 -4
  31. data/lib/active_model/type/big_integer.rb +14 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +46 -0
  34. data/lib/active_model/type/date.rb +52 -0
  35. data/lib/active_model/type/date_time.rb +46 -0
  36. data/lib/active_model/type/decimal.rb +69 -0
  37. data/lib/active_model/type/float.rb +35 -0
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +49 -0
  39. data/lib/active_model/type/helpers/mutable.rb +20 -0
  40. data/lib/active_model/type/helpers/numeric.rb +48 -0
  41. data/lib/active_model/type/helpers/time_value.rb +90 -0
  42. data/lib/active_model/type/helpers/timezone.rb +19 -0
  43. data/lib/active_model/type/helpers.rb +7 -0
  44. data/lib/active_model/type/immutable_string.rb +35 -0
  45. data/lib/active_model/type/integer.rb +67 -0
  46. data/lib/active_model/type/registry.rb +70 -0
  47. data/lib/active_model/type/string.rb +35 -0
  48. data/lib/active_model/type/time.rb +46 -0
  49. data/lib/active_model/type/value.rb +133 -0
  50. data/lib/active_model/type.rb +53 -0
  51. data/lib/active_model/validations/absence.rb +6 -4
  52. data/lib/active_model/validations/acceptance.rb +72 -14
  53. data/lib/active_model/validations/callbacks.rb +23 -19
  54. data/lib/active_model/validations/clusivity.rb +18 -12
  55. data/lib/active_model/validations/confirmation.rb +27 -14
  56. data/lib/active_model/validations/exclusion.rb +7 -4
  57. data/lib/active_model/validations/format.rb +27 -27
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +8 -7
  60. data/lib/active_model/validations/length.rb +35 -32
  61. data/lib/active_model/validations/numericality.rb +72 -34
  62. data/lib/active_model/validations/presence.rb +3 -3
  63. data/lib/active_model/validations/validates.rb +17 -15
  64. data/lib/active_model/validations/with.rb +6 -12
  65. data/lib/active_model/validations.rb +58 -23
  66. data/lib/active_model/validator.rb +23 -17
  67. data/lib/active_model/version.rb +4 -2
  68. data/lib/active_model.rb +18 -11
  69. metadata +44 -25
  70. data/lib/active_model/serializers/xml.rb +0 -238
  71. data/lib/active_model/test_case.rb +0 -4
@@ -1,6 +1,6 @@
1
- require 'active_support/hash_with_indifferent_access'
2
- require 'active_support/core_ext/object/duplicable'
3
- require 'active_support/core_ext/string/filters'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/attribute_mutation_tracker"
4
4
 
5
5
  module ActiveModel
6
6
  # == Active \Model \Dirty
@@ -13,7 +13,7 @@ module ActiveModel
13
13
  # * <tt>include ActiveModel::Dirty</tt> in your object.
14
14
  # * Call <tt>define_attribute_methods</tt> passing each method you want to
15
15
  # track.
16
- # * Call <tt>attr_name_will_change!</tt> before each change to the tracked
16
+ # * Call <tt>[attr_name]_will_change!</tt> before each change to the tracked
17
17
  # attribute.
18
18
  # * Call <tt>changes_applied</tt> after the changes are persisted.
19
19
  # * Call <tt>clear_changes_information</tt> when you want to reset the changes
@@ -27,6 +27,10 @@ module ActiveModel
27
27
  #
28
28
  # define_attribute_methods :name
29
29
  #
30
+ # def initialize
31
+ # @name = nil
32
+ # end
33
+ #
30
34
  # def name
31
35
  # @name
32
36
  # end
@@ -53,80 +57,106 @@ module ActiveModel
53
57
  # end
54
58
  # end
55
59
  #
56
- # A newly instantiated object is unchanged:
60
+ # A newly instantiated +Person+ object is unchanged:
57
61
  #
58
- # person = Person.find_by(name: 'Uncle Bob')
59
- # person.changed? # => false
62
+ # person = Person.new
63
+ # person.changed? # => false
60
64
  #
61
65
  # Change the name:
62
66
  #
63
67
  # person.name = 'Bob'
64
68
  # person.changed? # => true
65
69
  # person.name_changed? # => true
66
- # person.name_changed?(from: "Uncle Bob", to: "Bob") # => true
67
- # person.name_was # => "Uncle Bob"
68
- # person.name_change # => ["Uncle Bob", "Bob"]
70
+ # person.name_changed?(from: nil, to: "Bob") # => true
71
+ # person.name_was # => nil
72
+ # person.name_change # => [nil, "Bob"]
69
73
  # person.name = 'Bill'
70
- # person.name_change # => ["Uncle Bob", "Bill"]
74
+ # person.name_change # => [nil, "Bill"]
71
75
  #
72
76
  # Save the changes:
73
77
  #
74
78
  # person.save
75
- # person.changed? # => false
76
- # person.name_changed? # => false
79
+ # person.changed? # => false
80
+ # person.name_changed? # => false
77
81
  #
78
82
  # Reset the changes:
79
83
  #
80
- # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
84
+ # person.previous_changes # => {"name" => [nil, "Bill"]}
85
+ # person.name_previously_changed? # => true
86
+ # person.name_previously_changed?(from: nil, to: "Bill") # => true
87
+ # person.name_previous_change # => [nil, "Bill"]
88
+ # person.name_previously_was # => nil
81
89
  # person.reload!
82
- # person.previous_changes # => {}
90
+ # person.previous_changes # => {}
83
91
  #
84
92
  # Rollback the changes:
85
93
  #
86
94
  # person.name = "Uncle Bob"
87
95
  # person.rollback!
88
- # person.name # => "Bill"
89
- # person.name_changed? # => false
96
+ # person.name # => "Bill"
97
+ # person.name_changed? # => false
90
98
  #
91
99
  # Assigning the same value leaves the attribute unchanged:
92
100
  #
93
101
  # person.name = 'Bill'
94
- # person.name_changed? # => false
95
- # person.name_change # => nil
102
+ # person.name_changed? # => false
103
+ # person.name_change # => nil
96
104
  #
97
105
  # Which attributes have changed?
98
106
  #
99
107
  # person.name = 'Bob'
100
- # person.changed # => ["name"]
101
- # person.changes # => {"name" => ["Bill", "Bob"]}
108
+ # person.changed # => ["name"]
109
+ # person.changes # => {"name" => ["Bill", "Bob"]}
102
110
  #
103
111
  # If an attribute is modified in-place then make use of
104
- # +[attribute_name]_will_change!+ to mark that the attribute is changing.
105
- # Otherwise Active Model can't track changes to in-place attributes. Note
112
+ # <tt>[attribute_name]_will_change!</tt> to mark that the attribute is changing.
113
+ # Otherwise \Active \Model can't track changes to in-place attributes. Note
106
114
  # that Active Record can detect in-place modifications automatically. You do
107
- # not need to call +[attribute_name]_will_change!+ on Active Record models.
115
+ # not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models.
108
116
  #
109
117
  # person.name_will_change!
110
- # person.name_change # => ["Bill", "Bill"]
118
+ # person.name_change # => ["Bill", "Bill"]
111
119
  # person.name << 'y'
112
- # person.name_change # => ["Bill", "Billy"]
120
+ # person.name_change # => ["Bill", "Billy"]
113
121
  module Dirty
114
122
  extend ActiveSupport::Concern
115
123
  include ActiveModel::AttributeMethods
116
124
 
117
125
  included do
118
- attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
119
- attribute_method_affix prefix: 'reset_', suffix: '!'
120
- attribute_method_affix prefix: 'restore_', suffix: '!'
126
+ attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
127
+ attribute_method_suffix "_previously_changed?", "_previous_change", "_previously_was"
128
+ attribute_method_affix prefix: "restore_", suffix: "!"
129
+ attribute_method_affix prefix: "clear_", suffix: "_change"
130
+ end
131
+
132
+ def initialize_dup(other) # :nodoc:
133
+ super
134
+ if self.class.respond_to?(:_default_attributes)
135
+ @attributes = self.class._default_attributes.map do |attr|
136
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
137
+ end
138
+ end
139
+ @mutations_from_database = nil
140
+ end
141
+
142
+ # Clears dirty data and moves +changes+ to +previous_changes+ and
143
+ # +mutations_from_database+ to +mutations_before_last_save+ respectively.
144
+ def changes_applied
145
+ unless defined?(@attributes)
146
+ mutations_from_database.finalize_changes
147
+ end
148
+ @mutations_before_last_save = mutations_from_database
149
+ forget_attribute_assignments
150
+ @mutations_from_database = nil
121
151
  end
122
152
 
123
- # Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
153
+ # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise.
124
154
  #
125
155
  # person.changed? # => false
126
156
  # person.name = 'bob'
127
157
  # person.changed? # => true
128
158
  def changed?
129
- changed_attributes.present?
159
+ mutations_from_database.any_changes?
130
160
  end
131
161
 
132
162
  # Returns an array with the name of the attributes with unsaved changes.
@@ -135,7 +165,55 @@ module ActiveModel
135
165
  # person.name = 'bob'
136
166
  # person.changed # => ["name"]
137
167
  def changed
138
- changed_attributes.keys
168
+ mutations_from_database.changed_attribute_names
169
+ end
170
+
171
+ # Dispatch target for <tt>*_changed?</tt> attribute methods.
172
+ def attribute_changed?(attr_name, **options) # :nodoc:
173
+ mutations_from_database.changed?(attr_name.to_s, **options)
174
+ end
175
+
176
+ # Dispatch target for <tt>*_was</tt> attribute methods.
177
+ def attribute_was(attr_name) # :nodoc:
178
+ mutations_from_database.original_value(attr_name.to_s)
179
+ end
180
+
181
+ # Dispatch target for <tt>*_previously_changed?</tt> attribute methods.
182
+ def attribute_previously_changed?(attr_name, **options) # :nodoc:
183
+ mutations_before_last_save.changed?(attr_name.to_s, **options)
184
+ end
185
+
186
+ # Dispatch target for <tt>*_previously_was</tt> attribute methods.
187
+ def attribute_previously_was(attr_name) # :nodoc:
188
+ mutations_before_last_save.original_value(attr_name.to_s)
189
+ end
190
+
191
+ # Restore all previous data of the provided attributes.
192
+ def restore_attributes(attr_names = changed)
193
+ attr_names.each { |attr_name| restore_attribute!(attr_name) }
194
+ end
195
+
196
+ # Clears all dirty data: current changes and previous changes.
197
+ def clear_changes_information
198
+ @mutations_before_last_save = nil
199
+ forget_attribute_assignments
200
+ @mutations_from_database = nil
201
+ end
202
+
203
+ def clear_attribute_changes(attr_names)
204
+ attr_names.each do |attr_name|
205
+ clear_attribute_change(attr_name)
206
+ end
207
+ end
208
+
209
+ # Returns a hash of the attributes with unsaved changes indicating their original
210
+ # values like <tt>attr => original value</tt>.
211
+ #
212
+ # person.name # => "bob"
213
+ # person.name = 'robert'
214
+ # person.changed_attributes # => {"name" => "bob"}
215
+ def changed_attributes
216
+ mutations_from_database.changed_values
139
217
  end
140
218
 
141
219
  # Returns a hash of changed attributes indicating their original
@@ -145,7 +223,7 @@ module ActiveModel
145
223
  # person.name = 'bob'
146
224
  # person.changes # => { "name" => ["bill", "bob"] }
147
225
  def changes
148
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
226
+ mutations_from_database.changes
149
227
  end
150
228
 
151
229
  # Returns a hash of attributes that were changed before the model was saved.
@@ -155,108 +233,56 @@ module ActiveModel
155
233
  # person.save
156
234
  # person.previous_changes # => {"name" => ["bob", "robert"]}
157
235
  def previous_changes
158
- @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
159
- end
160
-
161
- # Returns a hash of the attributes with unsaved changes indicating their original
162
- # values like <tt>attr => original value</tt>.
163
- #
164
- # person.name # => "bob"
165
- # person.name = 'robert'
166
- # person.changed_attributes # => {"name" => "bob"}
167
- def changed_attributes
168
- @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
169
- end
170
-
171
- # Handle <tt>*_changed?</tt> for +method_missing+.
172
- def attribute_changed?(attr, options = {}) #:nodoc:
173
- result = changed_attributes.include?(attr)
174
- result &&= options[:to] == __send__(attr) if options.key?(:to)
175
- result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
176
- result
236
+ mutations_before_last_save.changes
177
237
  end
178
238
 
179
- # Handle <tt>*_was</tt> for +method_missing+.
180
- def attribute_was(attr) # :nodoc:
181
- attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
182
- end
183
-
184
- # Restore all previous data of the provided attributes.
185
- def restore_attributes(attributes = changed)
186
- attributes.each { |attr| restore_attribute! attr }
239
+ def attribute_changed_in_place?(attr_name) # :nodoc:
240
+ mutations_from_database.changed_in_place?(attr_name.to_s)
187
241
  end
188
242
 
189
243
  private
190
-
191
- # Removes current changes and makes them accessible through +previous_changes+.
192
- def changes_applied # :doc:
193
- @previously_changed = changes
194
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
244
+ def clear_attribute_change(attr_name)
245
+ mutations_from_database.forget_change(attr_name.to_s)
195
246
  end
196
247
 
197
- # Clear all dirty data: current changes and previous changes.
198
- def clear_changes_information # :doc:
199
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
200
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
248
+ def mutations_from_database
249
+ @mutations_from_database ||= if defined?(@attributes)
250
+ ActiveModel::AttributeMutationTracker.new(@attributes)
251
+ else
252
+ ActiveModel::ForcedMutationTracker.new(self)
253
+ end
201
254
  end
202
255
 
203
- def reset_changes
204
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
205
- `#reset_changes` is deprecated and will be removed on Rails 5.
206
- Please use `#clear_changes_information` instead.
207
- MSG
208
-
209
- clear_changes_information
256
+ def forget_attribute_assignments
257
+ @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
210
258
  end
211
259
 
212
- # Handle <tt>*_change</tt> for +method_missing+.
213
- def attribute_change(attr)
214
- [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
260
+ def mutations_before_last_save
261
+ @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
215
262
  end
216
263
 
217
- # Handle <tt>*_will_change!</tt> for +method_missing+.
218
- def attribute_will_change!(attr)
219
- return if attribute_changed?(attr)
220
-
221
- begin
222
- value = __send__(attr)
223
- value = value.duplicable? ? value.clone : value
224
- rescue TypeError, NoMethodError
225
- end
226
-
227
- set_attribute_was(attr, value)
264
+ # Dispatch target for <tt>*_change</tt> attribute methods.
265
+ def attribute_change(attr_name)
266
+ mutations_from_database.change_to_attribute(attr_name.to_s)
228
267
  end
229
268
 
230
- # Handle <tt>reset_*!</tt> for +method_missing+.
231
- def reset_attribute!(attr)
232
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
233
- `#reset_#{attr}!` is deprecated and will be removed on Rails 5.
234
- Please use `#restore_#{attr}!` instead.
235
- MSG
236
-
237
- restore_attribute!(attr)
269
+ # Dispatch target for <tt>*_previous_change</tt> attribute methods.
270
+ def attribute_previous_change(attr_name)
271
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
238
272
  end
239
273
 
240
- # Handle <tt>restore_*!</tt> for +method_missing+.
241
- def restore_attribute!(attr)
242
- if attribute_changed?(attr)
243
- __send__("#{attr}=", changed_attributes[attr])
244
- clear_attribute_changes([attr])
245
- end
274
+ # Dispatch target for <tt>*_will_change!</tt> attribute methods.
275
+ def attribute_will_change!(attr_name)
276
+ mutations_from_database.force_change(attr_name.to_s)
246
277
  end
247
278
 
248
- # This is necessary because `changed_attributes` might be overridden in
249
- # other implemntations (e.g. in `ActiveRecord`)
250
- alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
251
-
252
- # Force an attribute to have a particular "before" value
253
- def set_attribute_was(attr, old_value)
254
- attributes_changed_by_setter[attr] = old_value
255
- end
256
-
257
- # Remove changes information for the provided attributes.
258
- def clear_attribute_changes(attributes) # :doc:
259
- attributes_changed_by_setter.except!(*attributes)
279
+ # Dispatch target for <tt>restore_*!</tt> attribute methods.
280
+ def restore_attribute!(attr_name)
281
+ attr_name = attr_name.to_s
282
+ if attribute_changed?(attr_name)
283
+ __send__("#{attr_name}=", attribute_was(attr_name))
284
+ clear_attribute_change(attr_name)
285
+ end
260
286
  end
261
287
  end
262
288
  end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/class/attribute"
4
+
5
+ module ActiveModel
6
+ # == Active \Model \Error
7
+ #
8
+ # Represents one single error
9
+ class Error
10
+ CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
11
+ MESSAGE_OPTIONS = [:message]
12
+
13
+ class_attribute :i18n_customize_full_message, default: false
14
+
15
+ def self.full_message(attribute, message, base) # :nodoc:
16
+ return message if attribute == :base
17
+
18
+ base_class = base.class
19
+ attribute = attribute.to_s
20
+
21
+ if i18n_customize_full_message && base_class.respond_to?(:i18n_scope)
22
+ attribute = attribute.remove(/\[\d+\]/)
23
+ parts = attribute.split(".")
24
+ attribute_name = parts.pop
25
+ namespace = parts.join("/") unless parts.empty?
26
+ attributes_scope = "#{base_class.i18n_scope}.errors.models"
27
+
28
+ if namespace
29
+ defaults = base_class.lookup_ancestors.map do |klass|
30
+ [
31
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
32
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
33
+ ]
34
+ end
35
+ else
36
+ defaults = base_class.lookup_ancestors.map do |klass|
37
+ [
38
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
39
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
40
+ ]
41
+ end
42
+ end
43
+
44
+ defaults.flatten!
45
+ else
46
+ defaults = []
47
+ end
48
+
49
+ defaults << :"errors.format"
50
+ defaults << "%{attribute} %{message}"
51
+
52
+ attr_name = attribute.tr(".", "_").humanize
53
+ attr_name = base_class.human_attribute_name(attribute, {
54
+ default: attr_name,
55
+ base: base,
56
+ })
57
+
58
+ I18n.t(defaults.shift,
59
+ default: defaults,
60
+ attribute: attr_name,
61
+ message: message)
62
+ end
63
+
64
+ def self.generate_message(attribute, type, base, options) # :nodoc:
65
+ type = options.delete(:message) if options[:message].is_a?(Symbol)
66
+ value = (attribute != :base ? base.read_attribute_for_validation(attribute) : nil)
67
+
68
+ options = {
69
+ model: base.model_name.human,
70
+ attribute: base.class.human_attribute_name(attribute, { base: base }),
71
+ value: value,
72
+ object: base
73
+ }.merge!(options)
74
+
75
+ if base.class.respond_to?(:i18n_scope)
76
+ i18n_scope = base.class.i18n_scope.to_s
77
+ attribute = attribute.to_s.remove(/\[\d+\]/)
78
+
79
+ defaults = base.class.lookup_ancestors.flat_map do |klass|
80
+ [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
81
+ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
82
+ end
83
+ defaults << :"#{i18n_scope}.errors.messages.#{type}"
84
+
85
+ catch(:exception) do
86
+ translation = I18n.translate(defaults.first, **options.merge(default: defaults.drop(1), throw: true))
87
+ return translation unless translation.nil?
88
+ end unless options[:message]
89
+ else
90
+ defaults = []
91
+ end
92
+
93
+ defaults << :"errors.attributes.#{attribute}.#{type}"
94
+ defaults << :"errors.messages.#{type}"
95
+
96
+ key = defaults.shift
97
+ defaults = options.delete(:message) if options[:message]
98
+ options[:default] = defaults
99
+
100
+ I18n.translate(key, **options)
101
+ end
102
+
103
+ def initialize(base, attribute, type = :invalid, **options)
104
+ @base = base
105
+ @attribute = attribute
106
+ @raw_type = type
107
+ @type = type || :invalid
108
+ @options = options
109
+ end
110
+
111
+ def initialize_dup(other) # :nodoc:
112
+ @attribute = @attribute.dup
113
+ @raw_type = @raw_type.dup
114
+ @type = @type.dup
115
+ @options = @options.deep_dup
116
+ end
117
+
118
+ # The object which the error belongs to
119
+ attr_reader :base
120
+ # The attribute of +base+ which the error belongs to
121
+ attr_reader :attribute
122
+ # The type of error, defaults to `:invalid` unless specified
123
+ attr_reader :type
124
+ # The raw value provided as the second parameter when calling `errors#add`
125
+ attr_reader :raw_type
126
+ # The options provided when calling `errors#add`
127
+ attr_reader :options
128
+
129
+ # Returns the error message.
130
+ #
131
+ # error = ActiveModel::Error.new(person, :name, :too_short, count: 5)
132
+ # error.message
133
+ # # => "is too short (minimum is 5 characters)"
134
+ def message
135
+ case raw_type
136
+ when Symbol
137
+ self.class.generate_message(attribute, raw_type, @base, options.except(*CALLBACKS_OPTIONS))
138
+ else
139
+ raw_type
140
+ end
141
+ end
142
+
143
+ # Returns the error details.
144
+ #
145
+ # error = ActiveModel::Error.new(person, :name, :too_short, count: 5)
146
+ # error.details
147
+ # # => { error: :too_short, count: 5 }
148
+ def details
149
+ { error: raw_type }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
150
+ end
151
+ alias_method :detail, :details
152
+
153
+ # Returns the full error message.
154
+ #
155
+ # error = ActiveModel::Error.new(person, :name, :too_short, count: 5)
156
+ # error.full_message
157
+ # # => "Name is too short (minimum is 5 characters)"
158
+ def full_message
159
+ self.class.full_message(attribute, message, @base)
160
+ end
161
+
162
+ # See if error matches provided +attribute+, +type+ and +options+.
163
+ #
164
+ # Omitted params are not checked for a match.
165
+ def match?(attribute, type = nil, **options)
166
+ if @attribute != attribute || (type && @type != type)
167
+ return false
168
+ end
169
+
170
+ options.each do |key, value|
171
+ if @options[key] != value
172
+ return false
173
+ end
174
+ end
175
+
176
+ true
177
+ end
178
+
179
+ # See if error matches provided +attribute+, +type+ and +options+ exactly.
180
+ #
181
+ # All params must be equal to Error's own attributes to be considered a
182
+ # strict match.
183
+ def strict_match?(attribute, type, **options)
184
+ return false unless match?(attribute, type)
185
+
186
+ options == @options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)
187
+ end
188
+
189
+ def ==(other) # :nodoc:
190
+ other.is_a?(self.class) && attributes_for_hash == other.attributes_for_hash
191
+ end
192
+ alias eql? ==
193
+
194
+ def hash # :nodoc:
195
+ attributes_for_hash.hash
196
+ end
197
+
198
+ def inspect # :nodoc:
199
+ "#<#{self.class.name} attribute=#{@attribute}, type=#{@type}, options=#{@options.inspect}>"
200
+ end
201
+
202
+ protected
203
+ def attributes_for_hash
204
+ [@base, @attribute, @raw_type, @options.except(*CALLBACKS_OPTIONS)]
205
+ end
206
+ end
207
+ end