omg-activemodel 8.0.0.alpha1

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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +67 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +99 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +55 -0
  8. data/lib/active_model/attribute.rb +277 -0
  9. data/lib/active_model/attribute_assignment.rb +78 -0
  10. data/lib/active_model/attribute_methods.rb +592 -0
  11. data/lib/active_model/attribute_mutation_tracker.rb +189 -0
  12. data/lib/active_model/attribute_registration.rb +117 -0
  13. data/lib/active_model/attribute_set/builder.rb +182 -0
  14. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  15. data/lib/active_model/attribute_set.rb +118 -0
  16. data/lib/active_model/attributes.rb +165 -0
  17. data/lib/active_model/callbacks.rb +155 -0
  18. data/lib/active_model/conversion.rb +121 -0
  19. data/lib/active_model/deprecator.rb +7 -0
  20. data/lib/active_model/dirty.rb +416 -0
  21. data/lib/active_model/error.rb +208 -0
  22. data/lib/active_model/errors.rb +547 -0
  23. data/lib/active_model/forbidden_attributes_protection.rb +33 -0
  24. data/lib/active_model/gem_version.rb +17 -0
  25. data/lib/active_model/lint.rb +118 -0
  26. data/lib/active_model/locale/en.yml +38 -0
  27. data/lib/active_model/model.rb +78 -0
  28. data/lib/active_model/naming.rb +359 -0
  29. data/lib/active_model/nested_error.rb +22 -0
  30. data/lib/active_model/railtie.rb +24 -0
  31. data/lib/active_model/secure_password.rb +231 -0
  32. data/lib/active_model/serialization.rb +198 -0
  33. data/lib/active_model/serializers/json.rb +154 -0
  34. data/lib/active_model/translation.rb +78 -0
  35. data/lib/active_model/type/big_integer.rb +36 -0
  36. data/lib/active_model/type/binary.rb +62 -0
  37. data/lib/active_model/type/boolean.rb +48 -0
  38. data/lib/active_model/type/date.rb +78 -0
  39. data/lib/active_model/type/date_time.rb +88 -0
  40. data/lib/active_model/type/decimal.rb +107 -0
  41. data/lib/active_model/type/float.rb +64 -0
  42. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
  43. data/lib/active_model/type/helpers/mutable.rb +24 -0
  44. data/lib/active_model/type/helpers/numeric.rb +61 -0
  45. data/lib/active_model/type/helpers/time_value.rb +127 -0
  46. data/lib/active_model/type/helpers/timezone.rb +23 -0
  47. data/lib/active_model/type/helpers.rb +7 -0
  48. data/lib/active_model/type/immutable_string.rb +71 -0
  49. data/lib/active_model/type/integer.rb +113 -0
  50. data/lib/active_model/type/registry.rb +37 -0
  51. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  52. data/lib/active_model/type/string.rb +43 -0
  53. data/lib/active_model/type/time.rb +87 -0
  54. data/lib/active_model/type/value.rb +157 -0
  55. data/lib/active_model/type.rb +55 -0
  56. data/lib/active_model/validations/absence.rb +33 -0
  57. data/lib/active_model/validations/acceptance.rb +113 -0
  58. data/lib/active_model/validations/callbacks.rb +119 -0
  59. data/lib/active_model/validations/clusivity.rb +54 -0
  60. data/lib/active_model/validations/comparability.rb +18 -0
  61. data/lib/active_model/validations/comparison.rb +90 -0
  62. data/lib/active_model/validations/confirmation.rb +80 -0
  63. data/lib/active_model/validations/exclusion.rb +49 -0
  64. data/lib/active_model/validations/format.rb +112 -0
  65. data/lib/active_model/validations/helper_methods.rb +15 -0
  66. data/lib/active_model/validations/inclusion.rb +47 -0
  67. data/lib/active_model/validations/length.rb +130 -0
  68. data/lib/active_model/validations/numericality.rb +222 -0
  69. data/lib/active_model/validations/presence.rb +39 -0
  70. data/lib/active_model/validations/resolve_value.rb +26 -0
  71. data/lib/active_model/validations/validates.rb +175 -0
  72. data/lib/active_model/validations/with.rb +154 -0
  73. data/lib/active_model/validations.rb +489 -0
  74. data/lib/active_model/validator.rb +190 -0
  75. data/lib/active_model/version.rb +10 -0
  76. data/lib/active_model.rb +84 -0
  77. metadata +139 -0
@@ -0,0 +1,416 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/attribute_mutation_tracker"
4
+
5
+ module ActiveModel
6
+ # = Active \Model \Dirty
7
+ #
8
+ # Provides a way to track changes in your object in the same way as
9
+ # Active Record does.
10
+ #
11
+ # The requirements for implementing ActiveModel::Dirty are:
12
+ #
13
+ # * <tt>include ActiveModel::Dirty</tt> in your object.
14
+ # * Call <tt>define_attribute_methods</tt> passing each method you want to
15
+ # track.
16
+ # * Call <tt>*_will_change!</tt> before each change to the tracked attribute.
17
+ # * Call <tt>changes_applied</tt> after the changes are persisted.
18
+ # * Call <tt>clear_changes_information</tt> when you want to reset the changes
19
+ # information.
20
+ # * Call <tt>restore_attributes</tt> when you want to restore previous data.
21
+ #
22
+ # A minimal implementation could be:
23
+ #
24
+ # class Person
25
+ # include ActiveModel::Dirty
26
+ #
27
+ # define_attribute_methods :name
28
+ #
29
+ # def initialize
30
+ # @name = nil
31
+ # end
32
+ #
33
+ # def name
34
+ # @name
35
+ # end
36
+ #
37
+ # def name=(val)
38
+ # name_will_change! unless val == @name
39
+ # @name = val
40
+ # end
41
+ #
42
+ # def save
43
+ # # do persistence work
44
+ #
45
+ # changes_applied
46
+ # end
47
+ #
48
+ # def reload!
49
+ # # get the values from the persistence layer
50
+ #
51
+ # clear_changes_information
52
+ # end
53
+ #
54
+ # def rollback!
55
+ # restore_attributes
56
+ # end
57
+ # end
58
+ #
59
+ # A newly instantiated +Person+ object is unchanged:
60
+ #
61
+ # person = Person.new
62
+ # person.changed? # => false
63
+ #
64
+ # Change the name:
65
+ #
66
+ # person.name = 'Bob'
67
+ # person.changed? # => true
68
+ # person.name_changed? # => true
69
+ # person.name_changed?(from: nil, to: "Bob") # => true
70
+ # person.name_was # => nil
71
+ # person.name_change # => [nil, "Bob"]
72
+ # person.name = 'Bill'
73
+ # person.name_change # => [nil, "Bill"]
74
+ #
75
+ # Save the changes:
76
+ #
77
+ # person.save
78
+ # person.changed? # => false
79
+ # person.name_changed? # => false
80
+ #
81
+ # Reset the changes:
82
+ #
83
+ # person.previous_changes # => {"name" => [nil, "Bill"]}
84
+ # person.name_previously_changed? # => true
85
+ # person.name_previously_changed?(from: nil, to: "Bill") # => true
86
+ # person.name_previous_change # => [nil, "Bill"]
87
+ # person.name_previously_was # => nil
88
+ # person.reload!
89
+ # person.previous_changes # => {}
90
+ #
91
+ # Rollback the changes:
92
+ #
93
+ # person.name = "Uncle Bob"
94
+ # person.rollback!
95
+ # person.name # => "Bill"
96
+ # person.name_changed? # => false
97
+ #
98
+ # Assigning the same value leaves the attribute unchanged:
99
+ #
100
+ # person.name = 'Bill'
101
+ # person.name_changed? # => false
102
+ # person.name_change # => nil
103
+ #
104
+ # Which attributes have changed?
105
+ #
106
+ # person.name = 'Bob'
107
+ # person.changed # => ["name"]
108
+ # person.changes # => {"name" => ["Bill", "Bob"]}
109
+ #
110
+ # If an attribute is modified in-place then make use of
111
+ # {*_will_change!}[rdoc-label:method-i-2A_will_change-21] to mark that the attribute is changing.
112
+ # Otherwise \Active \Model can't track changes to in-place attributes. Note
113
+ # that Active Record can detect in-place modifications automatically. You do
114
+ # not need to call <tt>*_will_change!</tt> on Active Record models.
115
+ #
116
+ # person.name_will_change!
117
+ # person.name_change # => ["Bill", "Bill"]
118
+ # person.name << 'y'
119
+ # person.name_change # => ["Bill", "Billy"]
120
+ #
121
+ # Methods can be invoked as +name_changed?+ or by passing an argument to the
122
+ # generic method <tt>attribute_changed?("name")</tt>.
123
+ module Dirty
124
+ extend ActiveSupport::Concern
125
+ include ActiveModel::AttributeMethods
126
+
127
+ included do
128
+ ##
129
+ # :method: *_previously_changed?
130
+ #
131
+ # :call-seq: *_previously_changed?(**options)
132
+ #
133
+ # This method is generated for each attribute.
134
+ #
135
+ # Returns true if the attribute previously had unsaved changes.
136
+ #
137
+ # person = Person.new
138
+ # person.name = 'Britanny'
139
+ # person.save
140
+ # person.name_previously_changed? # => true
141
+ # person.name_previously_changed?(from: nil, to: 'Britanny') # => true
142
+
143
+ ##
144
+ # :method: *_changed?
145
+ #
146
+ # This method is generated for each attribute.
147
+ #
148
+ # Returns true if the attribute has unsaved changes.
149
+ #
150
+ # person = Person.new
151
+ # person.name = 'Andrew'
152
+ # person.name_changed? # => true
153
+
154
+ ##
155
+ # :method: *_change
156
+ #
157
+ # This method is generated for each attribute.
158
+ #
159
+ # Returns the old and the new value of the attribute.
160
+ #
161
+ # person = Person.new
162
+ # person.name = 'Nick'
163
+ # person.name_change # => [nil, 'Nick']
164
+
165
+ ##
166
+ # :method: *_will_change!
167
+ #
168
+ # This method is generated for each attribute.
169
+ #
170
+ # If an attribute is modified in-place then make use of
171
+ # <tt>*_will_change!</tt> to mark that the attribute is changing.
172
+ # Otherwise Active Model can’t track changes to in-place attributes. Note
173
+ # that Active Record can detect in-place modifications automatically. You
174
+ # do not need to call <tt>*_will_change!</tt> on Active Record
175
+ # models.
176
+ #
177
+ # person = Person.new('Sandy')
178
+ # person.name_will_change!
179
+ # person.name_change # => ['Sandy', 'Sandy']
180
+
181
+ ##
182
+ # :method: *_was
183
+ #
184
+ # This method is generated for each attribute.
185
+ #
186
+ # Returns the old value of the attribute.
187
+ #
188
+ # person = Person.new(name: 'Steph')
189
+ # person.name = 'Stephanie'
190
+ # person.name_was # => 'Steph'
191
+
192
+ ##
193
+ # :method: *_previous_change
194
+ #
195
+ # This method is generated for each attribute.
196
+ #
197
+ # Returns the old and the new value of the attribute before the last save.
198
+ #
199
+ # person = Person.new
200
+ # person.name = 'Emmanuel'
201
+ # person.save
202
+ # person.name_previous_change # => [nil, 'Emmanuel']
203
+
204
+ ##
205
+ # :method: *_previously_was
206
+ #
207
+ # This method is generated for each attribute.
208
+ #
209
+ # Returns the old value of the attribute before the last save.
210
+ #
211
+ # person = Person.new
212
+ # person.name = 'Sage'
213
+ # person.save
214
+ # person.name_previously_was # => nil
215
+
216
+ ##
217
+ # :method: restore_*!
218
+ #
219
+ # This method is generated for each attribute.
220
+ #
221
+ # Restores the attribute to the old value.
222
+ #
223
+ # person = Person.new
224
+ # person.name = 'Amanda'
225
+ # person.restore_name!
226
+ # person.name # => nil
227
+
228
+ ##
229
+ # :method: clear_*_change
230
+ #
231
+ # This method is generated for each attribute.
232
+ #
233
+ # Clears all dirty data of the attribute: current changes and previous changes.
234
+ #
235
+ # person = Person.new(name: 'Chris')
236
+ # person.name = 'Jason'
237
+ # person.name_change # => ['Chris', 'Jason']
238
+ # person.clear_name_change
239
+ # person.name_change # => nil
240
+
241
+ attribute_method_suffix "_previously_changed?", "_changed?", parameters: "**options"
242
+ attribute_method_suffix "_change", "_will_change!", "_was", parameters: false
243
+ attribute_method_suffix "_previous_change", "_previously_was", parameters: false
244
+ attribute_method_affix prefix: "restore_", suffix: "!", parameters: false
245
+ attribute_method_affix prefix: "clear_", suffix: "_change", parameters: false
246
+ end
247
+
248
+ def initialize_dup(other) # :nodoc:
249
+ super
250
+ if self.class.respond_to?(:_default_attributes)
251
+ @attributes = self.class._default_attributes.map do |attr|
252
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
253
+ end
254
+ end
255
+ @mutations_from_database = nil
256
+ end
257
+
258
+ def as_json(options = {}) # :nodoc:
259
+ except = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
260
+ options = options.merge except: except
261
+ super(options)
262
+ end
263
+
264
+ # Clears dirty data and moves +changes+ to +previous_changes+ and
265
+ # +mutations_from_database+ to +mutations_before_last_save+ respectively.
266
+ def changes_applied
267
+ unless defined?(@attributes)
268
+ mutations_from_database.finalize_changes
269
+ end
270
+ @mutations_before_last_save = mutations_from_database
271
+ forget_attribute_assignments
272
+ @mutations_from_database = nil
273
+ end
274
+
275
+ # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise.
276
+ #
277
+ # person.changed? # => false
278
+ # person.name = 'bob'
279
+ # person.changed? # => true
280
+ def changed?
281
+ mutations_from_database.any_changes?
282
+ end
283
+
284
+ # Returns an array with the name of the attributes with unsaved changes.
285
+ #
286
+ # person.changed # => []
287
+ # person.name = 'bob'
288
+ # person.changed # => ["name"]
289
+ def changed
290
+ mutations_from_database.changed_attribute_names
291
+ end
292
+
293
+ # Dispatch target for {*_changed?}[rdoc-label:method-i-2A_changed-3F] attribute methods.
294
+ def attribute_changed?(attr_name, **options)
295
+ mutations_from_database.changed?(attr_name.to_s, **options)
296
+ end
297
+
298
+ # Dispatch target for {*_was}[rdoc-label:method-i-2A_was] attribute methods.
299
+ def attribute_was(attr_name)
300
+ mutations_from_database.original_value(attr_name.to_s)
301
+ end
302
+
303
+ # Dispatch target for {*_previously_changed?}[rdoc-label:method-i-2A_previously_changed-3F] attribute methods.
304
+ def attribute_previously_changed?(attr_name, **options)
305
+ mutations_before_last_save.changed?(attr_name.to_s, **options)
306
+ end
307
+
308
+ # Dispatch target for {*_previously_was}[rdoc-label:method-i-2A_previously_was] attribute methods.
309
+ def attribute_previously_was(attr_name)
310
+ mutations_before_last_save.original_value(attr_name.to_s)
311
+ end
312
+
313
+ # Restore all previous data of the provided attributes.
314
+ def restore_attributes(attr_names = changed)
315
+ attr_names.each { |attr_name| restore_attribute!(attr_name) }
316
+ end
317
+
318
+ # Clears all dirty data: current changes and previous changes.
319
+ def clear_changes_information
320
+ @mutations_before_last_save = nil
321
+ forget_attribute_assignments
322
+ @mutations_from_database = nil
323
+ end
324
+
325
+ def clear_attribute_changes(attr_names)
326
+ attr_names.each do |attr_name|
327
+ clear_attribute_change(attr_name)
328
+ end
329
+ end
330
+
331
+ # Returns a hash of the attributes with unsaved changes indicating their original
332
+ # values like <tt>attr => original value</tt>.
333
+ #
334
+ # person.name # => "bob"
335
+ # person.name = 'robert'
336
+ # person.changed_attributes # => {"name" => "bob"}
337
+ def changed_attributes
338
+ mutations_from_database.changed_values
339
+ end
340
+
341
+ # Returns a hash of changed attributes indicating their original
342
+ # and new values like <tt>attr => [original value, new value]</tt>.
343
+ #
344
+ # person.changes # => {}
345
+ # person.name = 'bob'
346
+ # person.changes # => { "name" => ["bill", "bob"] }
347
+ def changes
348
+ mutations_from_database.changes
349
+ end
350
+
351
+ # Returns a hash of attributes that were changed before the model was saved.
352
+ #
353
+ # person.name # => "bob"
354
+ # person.name = 'robert'
355
+ # person.save
356
+ # person.previous_changes # => {"name" => ["bob", "robert"]}
357
+ def previous_changes
358
+ mutations_before_last_save.changes
359
+ end
360
+
361
+ def attribute_changed_in_place?(attr_name) # :nodoc:
362
+ mutations_from_database.changed_in_place?(attr_name.to_s)
363
+ end
364
+
365
+ private
366
+ def init_internals
367
+ super
368
+ @mutations_before_last_save = nil
369
+ @mutations_from_database = nil
370
+ end
371
+
372
+ def clear_attribute_change(attr_name)
373
+ mutations_from_database.forget_change(attr_name.to_s)
374
+ end
375
+
376
+ def mutations_from_database
377
+ @mutations_from_database ||= if defined?(@attributes)
378
+ ActiveModel::AttributeMutationTracker.new(@attributes)
379
+ else
380
+ ActiveModel::ForcedMutationTracker.new(self)
381
+ end
382
+ end
383
+
384
+ def forget_attribute_assignments
385
+ @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
386
+ end
387
+
388
+ def mutations_before_last_save
389
+ @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
390
+ end
391
+
392
+ # Dispatch target for <tt>*_change</tt> attribute methods.
393
+ def attribute_change(attr_name)
394
+ mutations_from_database.change_to_attribute(attr_name.to_s)
395
+ end
396
+
397
+ # Dispatch target for <tt>*_previous_change</tt> attribute methods.
398
+ def attribute_previous_change(attr_name)
399
+ mutations_before_last_save.change_to_attribute(attr_name.to_s)
400
+ end
401
+
402
+ # Dispatch target for <tt>*_will_change!</tt> attribute methods.
403
+ def attribute_will_change!(attr_name)
404
+ mutations_from_database.force_change(attr_name.to_s)
405
+ end
406
+
407
+ # Dispatch target for <tt>restore_*!</tt> attribute methods.
408
+ def restore_attribute!(attr_name)
409
+ attr_name = attr_name.to_s
410
+ if attribute_changed?(attr_name)
411
+ __send__("#{attr_name}=", attribute_was(attr_name))
412
+ clear_attribute_change(attr_name)
413
+ end
414
+ end
415
+ end
416
+ end
@@ -0,0 +1,208 @@
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.remove(/\.base\z/).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
125
+ # <tt>errors#add</tt>
126
+ attr_reader :raw_type
127
+ # The options provided when calling <tt>errors#add</tt>
128
+ attr_reader :options
129
+
130
+ # Returns the error message.
131
+ #
132
+ # error = ActiveModel::Error.new(person, :name, :too_short, count: 5)
133
+ # error.message
134
+ # # => "is too short (minimum is 5 characters)"
135
+ def message
136
+ case raw_type
137
+ when Symbol
138
+ self.class.generate_message(attribute, raw_type, @base, options.except(*CALLBACKS_OPTIONS))
139
+ else
140
+ raw_type
141
+ end
142
+ end
143
+
144
+ # Returns the error details.
145
+ #
146
+ # error = ActiveModel::Error.new(person, :name, :too_short, count: 5)
147
+ # error.details
148
+ # # => { error: :too_short, count: 5 }
149
+ def details
150
+ { error: raw_type }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
151
+ end
152
+ alias_method :detail, :details
153
+
154
+ # Returns the full error message.
155
+ #
156
+ # error = ActiveModel::Error.new(person, :name, :too_short, count: 5)
157
+ # error.full_message
158
+ # # => "Name is too short (minimum is 5 characters)"
159
+ def full_message
160
+ self.class.full_message(attribute, message, @base)
161
+ end
162
+
163
+ # See if error matches provided +attribute+, +type+, and +options+.
164
+ #
165
+ # Omitted params are not checked for a match.
166
+ def match?(attribute, type = nil, **options)
167
+ if @attribute != attribute || (type && @type != type)
168
+ return false
169
+ end
170
+
171
+ options.each do |key, value|
172
+ if @options[key] != value
173
+ return false
174
+ end
175
+ end
176
+
177
+ true
178
+ end
179
+
180
+ # See if error matches provided +attribute+, +type+, and +options+ exactly.
181
+ #
182
+ # All params must be equal to Error's own attributes to be considered a
183
+ # strict match.
184
+ def strict_match?(attribute, type, **options)
185
+ return false unless match?(attribute, type)
186
+
187
+ options == @options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)
188
+ end
189
+
190
+ def ==(other) # :nodoc:
191
+ other.is_a?(self.class) && attributes_for_hash == other.attributes_for_hash
192
+ end
193
+ alias eql? ==
194
+
195
+ def hash # :nodoc:
196
+ attributes_for_hash.hash
197
+ end
198
+
199
+ def inspect # :nodoc:
200
+ "#<#{self.class.name} attribute=#{@attribute}, type=#{@type}, options=#{@options.inspect}>"
201
+ end
202
+
203
+ protected
204
+ def attributes_for_hash
205
+ [@base, @attribute, @raw_type, @options.except(*CALLBACKS_OPTIONS)]
206
+ end
207
+ end
208
+ end