omg-activemodel 8.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
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