activemodel 6.0.5.1 → 6.1.7.4

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +95 -160
  3. data/MIT-LICENSE +1 -2
  4. data/README.rdoc +1 -1
  5. data/lib/active_model/attribute.rb +18 -17
  6. data/lib/active_model/attribute_assignment.rb +3 -4
  7. data/lib/active_model/attribute_methods.rb +74 -38
  8. data/lib/active_model/attribute_mutation_tracker.rb +8 -5
  9. data/lib/active_model/attribute_set/builder.rb +80 -13
  10. data/lib/active_model/attribute_set.rb +18 -16
  11. data/lib/active_model/attributes.rb +20 -24
  12. data/lib/active_model/callbacks.rb +1 -1
  13. data/lib/active_model/dirty.rb +17 -4
  14. data/lib/active_model/error.rb +207 -0
  15. data/lib/active_model/errors.rb +316 -208
  16. data/lib/active_model/gem_version.rb +3 -3
  17. data/lib/active_model/lint.rb +1 -1
  18. data/lib/active_model/naming.rb +2 -2
  19. data/lib/active_model/nested_error.rb +22 -0
  20. data/lib/active_model/railtie.rb +1 -1
  21. data/lib/active_model/secure_password.rb +15 -14
  22. data/lib/active_model/serialization.rb +9 -6
  23. data/lib/active_model/serializers/json.rb +7 -0
  24. data/lib/active_model/type/date_time.rb +2 -2
  25. data/lib/active_model/type/float.rb +2 -0
  26. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +11 -7
  27. data/lib/active_model/type/helpers/numeric.rb +8 -3
  28. data/lib/active_model/type/helpers/time_value.rb +27 -17
  29. data/lib/active_model/type/helpers/timezone.rb +1 -1
  30. data/lib/active_model/type/immutable_string.rb +14 -10
  31. data/lib/active_model/type/integer.rb +11 -2
  32. data/lib/active_model/type/registry.rb +12 -9
  33. data/lib/active_model/type/string.rb +12 -2
  34. data/lib/active_model/type/value.rb +9 -1
  35. data/lib/active_model/type.rb +3 -2
  36. data/lib/active_model/validations/absence.rb +1 -1
  37. data/lib/active_model/validations/acceptance.rb +1 -1
  38. data/lib/active_model/validations/callbacks.rb +15 -15
  39. data/lib/active_model/validations/clusivity.rb +5 -1
  40. data/lib/active_model/validations/confirmation.rb +2 -2
  41. data/lib/active_model/validations/exclusion.rb +1 -1
  42. data/lib/active_model/validations/format.rb +2 -2
  43. data/lib/active_model/validations/inclusion.rb +1 -1
  44. data/lib/active_model/validations/length.rb +2 -2
  45. data/lib/active_model/validations/numericality.rb +54 -41
  46. data/lib/active_model/validations/presence.rb +1 -1
  47. data/lib/active_model/validations/validates.rb +6 -4
  48. data/lib/active_model/validations.rb +6 -6
  49. data/lib/active_model/validator.rb +7 -1
  50. data/lib/active_model.rb +2 -1
  51. metadata +9 -7
@@ -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