activemodel 6.0.5.1 → 6.1.7.4

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