activemodel 4.2.0 → 6.1.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 (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