validation_hints 0.2.3 → 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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +63 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +273 -0
- data/Rakefile +14 -1
- data/lib/active_model/hints.rb +180 -193
- data/lib/validation_hints/locale/en.yml +5 -10
- data/lib/validation_hints/railtie.rb +13 -0
- data/lib/validation_hints/validations_patch.rb +39 -0
- data/lib/validation_hints/version.rb +1 -1
- data/lib/validation_hints.rb +19 -32
- data/test/active_model/hints_test.rb +77 -0
- data/test/test_helper.rb +34 -0
- data/test/validation_hints_test.rb +13 -0
- data/validation_hints.gemspec +24 -16
- metadata +43 -26
data/lib/active_model/hints.rb
CHANGED
|
@@ -1,67 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ActiveModel
|
|
2
|
-
#
|
|
4
|
+
# Introspects declared validators and builds proactive hint messages (before +valid?+ fails).
|
|
3
5
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# p.hints[:name]
|
|
7
|
-
#
|
|
8
|
-
# more documentation needed
|
|
9
|
-
|
|
6
|
+
# Conditional options (+:if+, +:unless+, +:on+) are not evaluated; hints reflect static rules.
|
|
7
|
+
# For +format+ validators, prefer a custom +message:+ or per-attribute I18n keys.
|
|
10
8
|
class Hints
|
|
11
9
|
include Enumerable
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
MESSAGES_FOR_OPTIONS = %w(within in is minimum maximum greater_than greater_than_or_equal_to equal_to less_than less_than_or_equal_to odd even only_integer)
|
|
18
|
-
OPTIONS_THAT_WE_DONT_USE_YET = {
|
|
19
|
-
:acceptance => :acceptance
|
|
11
|
+
MESSAGES_FOR_OPTIONS = %w[
|
|
12
|
+
within in is minimum maximum greater_than greater_than_or_equal_to
|
|
13
|
+
equal_to less_than less_than_or_equal_to odd even only_integer
|
|
14
|
+
].freeze
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
VALIDATORS_THAT_WE_DONT_KNOW_WHAT_TO_DO_WITH = %w(validates_associated)
|
|
16
|
+
VALIDATORS_WITHOUT_MAIN_KEYS = %w[exclusion format inclusion length numericality].freeze
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
# validates :email, :confirmation => true
|
|
26
|
-
# validates :email_confirmation, :presence => true
|
|
27
|
-
# also have a hint?
|
|
18
|
+
RANGE_OPTIONS = %w[within in].freeze
|
|
28
19
|
|
|
29
20
|
attr_reader :messages
|
|
30
21
|
|
|
31
|
-
# Pass in the instance of the object that is using the errors object.
|
|
32
|
-
#
|
|
33
|
-
# class Person
|
|
34
|
-
# def initialize
|
|
35
|
-
# @errors = ActiveModel::Errors.new(self)
|
|
36
|
-
# end
|
|
37
|
-
# end
|
|
38
22
|
def initialize(base)
|
|
39
|
-
@base
|
|
40
|
-
@messages =
|
|
41
|
-
|
|
42
|
-
@messages[
|
|
23
|
+
@base = base
|
|
24
|
+
@messages = {}
|
|
25
|
+
attribute_names_for_hints.each do |attribute|
|
|
26
|
+
@messages[attribute] = hints_for(attribute)
|
|
43
27
|
end
|
|
44
28
|
end
|
|
45
29
|
|
|
46
30
|
def hints_for(attribute)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
message_key = [validator, v.options[:message]].join('.') # if a message was supplied as a symbol, we use it instead
|
|
52
|
-
result << generate_message(attribute, message_key, v.options)
|
|
53
|
-
else
|
|
54
|
-
message_key = validator
|
|
55
|
-
message_key = [validator, ".must_be_a_number"].join('.') if validator == 'numericality' # create an option for numericality; the way YAML works a key (numericality) with subkeys (greater_than, etc etc) can not have a string itself. So we create a subkey for numericality
|
|
56
|
-
result << generate_message(attribute, message_key, v.options) unless VALIDATORS_WITHOUT_MAIN_KEYS.include?(validator)
|
|
57
|
-
v.options.each do |o|
|
|
58
|
-
if MESSAGES_FOR_OPTIONS.include?(o.first.to_s)
|
|
59
|
-
count = o.last
|
|
60
|
-
count = (o.last.to_sentence if %w(inclusion exclusion).include?(validator)) rescue o.last
|
|
61
|
-
result << generate_message(attribute, [ validator, o.first.to_s ].join('.'), { :count => count } )
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
31
|
+
attribute = attribute.to_sym
|
|
32
|
+
result = []
|
|
33
|
+
@base.class.validators_on(attribute).each do |validator|
|
|
34
|
+
result.concat(messages_for_validator(attribute, validator))
|
|
65
35
|
end
|
|
66
36
|
result
|
|
67
37
|
end
|
|
@@ -71,143 +41,74 @@ module ActiveModel
|
|
|
71
41
|
end
|
|
72
42
|
|
|
73
43
|
def initialize_dup(other)
|
|
74
|
-
@messages = other.messages.dup
|
|
44
|
+
@messages = other.messages.transform_values(&:dup)
|
|
75
45
|
end
|
|
76
46
|
|
|
77
|
-
# Backport dup from 1.9 so that #initialize_dup gets called
|
|
78
|
-
unless Object.respond_to?(:initialize_dup)
|
|
79
|
-
def dup # :nodoc:
|
|
80
|
-
copy = super
|
|
81
|
-
copy.initialize_dup(self)
|
|
82
|
-
copy
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Clear the messages
|
|
87
47
|
def clear
|
|
88
48
|
messages.clear
|
|
89
49
|
end
|
|
90
50
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
(v = messages[hint]) && v.any?
|
|
51
|
+
def include?(attribute)
|
|
52
|
+
(value = messages[attribute.to_sym]) && value.any?
|
|
94
53
|
end
|
|
95
|
-
alias
|
|
54
|
+
alias has_key? include?
|
|
96
55
|
|
|
97
|
-
# Get messages for +key+
|
|
98
56
|
def get(key)
|
|
99
57
|
messages[key]
|
|
100
58
|
end
|
|
101
59
|
|
|
102
|
-
# Set messages for +key+ to +value+
|
|
103
60
|
def set(key, value)
|
|
104
61
|
messages[key] = value
|
|
105
62
|
end
|
|
106
63
|
|
|
107
|
-
# Delete messages for +key+
|
|
108
64
|
def delete(key)
|
|
109
65
|
messages.delete(key)
|
|
110
66
|
end
|
|
111
67
|
|
|
112
|
-
# When passed a symbol or a name of a method, returns an array of hints
|
|
113
|
-
# for the method.
|
|
114
|
-
#
|
|
115
|
-
# p.hints[:name] # => ["can not be nil"]
|
|
116
|
-
# p.hints['name'] # => ["can not be nil"]
|
|
117
68
|
def [](attribute)
|
|
118
69
|
get(attribute.to_sym) || set(attribute.to_sym, [])
|
|
119
70
|
end
|
|
120
71
|
|
|
121
|
-
# Adds to the supplied attribute the supplied hint message.
|
|
122
|
-
#
|
|
123
|
-
# p.hints[:name] = "must be set"
|
|
124
|
-
# p.hints[:name] # => ['must be set']
|
|
125
72
|
def []=(attribute, hint)
|
|
126
73
|
self[attribute] << hint
|
|
127
74
|
end
|
|
128
75
|
|
|
129
|
-
# Iterates through each hint key, value pair in the hint messages hash.
|
|
130
|
-
# Yields the attribute and the hint for that attribute. If the attribute
|
|
131
|
-
# has more than one hint message, yields once for each hint message.
|
|
132
|
-
#
|
|
133
|
-
# p.hints.add(:name, "can't be blank")
|
|
134
|
-
# p.hints.each do |attribute, hints_array|
|
|
135
|
-
# # Will yield :name and "can't be blank"
|
|
136
|
-
# end
|
|
137
|
-
#
|
|
138
|
-
# p.hints.add(:name, "must be specified")
|
|
139
|
-
# p.hints.each do |attribute, hints_array|
|
|
140
|
-
# # Will yield :name and "can't be blank"
|
|
141
|
-
# # then yield :name and "must be specified"
|
|
142
|
-
# end
|
|
143
76
|
def each
|
|
144
77
|
messages.each_key do |attribute|
|
|
145
78
|
self[attribute].each { |hint| yield attribute, hint }
|
|
146
79
|
end
|
|
147
80
|
end
|
|
148
81
|
|
|
149
|
-
# Returns the number of error messages.
|
|
150
|
-
#
|
|
151
|
-
# p.hints.add(:name, "can't be blank")
|
|
152
|
-
# p.hints.size # => 1
|
|
153
|
-
# p.hints.add(:name, "must be specified")
|
|
154
|
-
# p.hints.size # => 2
|
|
155
82
|
def size
|
|
156
83
|
values.flatten.size
|
|
157
84
|
end
|
|
158
85
|
|
|
159
|
-
# Returns all message values
|
|
160
86
|
def values
|
|
161
87
|
messages.values
|
|
162
88
|
end
|
|
163
89
|
|
|
164
|
-
# Returns all message keys
|
|
165
90
|
def keys
|
|
166
91
|
messages.keys
|
|
167
92
|
end
|
|
168
93
|
|
|
169
|
-
# Returns an array of hint messages, with the attribute name included
|
|
170
|
-
#
|
|
171
|
-
# p.hints.add(:name, "can't be blank")
|
|
172
|
-
# p.hints.add(:name, "must be specified")
|
|
173
|
-
# p.hints.to_a # => ["name can't be blank", "name must be specified"]
|
|
174
94
|
def to_a
|
|
175
95
|
full_messages
|
|
176
96
|
end
|
|
177
97
|
|
|
178
|
-
# Returns the number of hint messages.
|
|
179
|
-
# p.hints.add(:name, "can't be blank")
|
|
180
|
-
# p.hints.count # => 1
|
|
181
|
-
# p.hints.add(:name, "must be specified")
|
|
182
|
-
# p.hints.count # => 2
|
|
183
98
|
def count
|
|
184
99
|
to_a.size
|
|
185
100
|
end
|
|
186
101
|
|
|
187
|
-
# Returns true if no hints are found, false otherwise.
|
|
188
|
-
# If the hint message is a string it can be empty.
|
|
189
102
|
def empty?
|
|
190
|
-
all?
|
|
103
|
+
messages.values.all?(&:empty?)
|
|
191
104
|
end
|
|
192
105
|
alias_method :blank?, :empty?
|
|
193
106
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# # =>
|
|
200
|
-
# # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
|
201
|
-
# # <hints>
|
|
202
|
-
# # <hint>name can't be blank</hint>
|
|
203
|
-
# # <hint>name must be specified</hint>
|
|
204
|
-
# # </hints>
|
|
205
|
-
def to_xml(options={})
|
|
206
|
-
to_a.to_xml options.reverse_merge(:root => "hints", :skip_types => true)
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# Returns an ActiveSupport::OrderedHash that can be used as the JSON representation for this object.
|
|
210
|
-
def as_json(options=nil)
|
|
107
|
+
def to_xml(options = {})
|
|
108
|
+
to_a.to_xml(options.reverse_merge(root: "hints", skip_types: true))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def as_json(_options = nil)
|
|
211
112
|
to_hash
|
|
212
113
|
end
|
|
213
114
|
|
|
@@ -215,13 +116,7 @@ module ActiveModel
|
|
|
215
116
|
messages.dup
|
|
216
117
|
end
|
|
217
118
|
|
|
218
|
-
|
|
219
|
-
# +attribute+.
|
|
220
|
-
# If no +message+ is supplied, <tt>:invalid</tt> is assumed.
|
|
221
|
-
#
|
|
222
|
-
# If +message+ is a symbol, it will be translated using the appropriate scope (see +translate_hint+).
|
|
223
|
-
# If +message+ is a proc, it will be called, allowing for things like <tt>Time.now</tt> to be used within an hint.
|
|
224
|
-
def add(attribute, message = nil, options = {})
|
|
119
|
+
def add(attribute, message = :invalid, options = {})
|
|
225
120
|
message = normalize_message(attribute, message, options)
|
|
226
121
|
if options[:strict]
|
|
227
122
|
raise ActiveModel::StrictValidationFailed, full_message(attribute, message)
|
|
@@ -230,95 +125,187 @@ module ActiveModel
|
|
|
230
125
|
self[attribute] << message
|
|
231
126
|
end
|
|
232
127
|
|
|
233
|
-
# Will add an hint message to each of the attributes in +attributes+ that is empty.
|
|
234
128
|
def add_on_empty(attributes, options = {})
|
|
235
|
-
|
|
129
|
+
Array(attributes).each do |attribute|
|
|
236
130
|
value = @base.send(:read_attribute_for_validation, attribute)
|
|
237
131
|
is_empty = value.respond_to?(:empty?) ? value.empty? : false
|
|
238
132
|
add(attribute, :empty, options) if value.nil? || is_empty
|
|
239
133
|
end
|
|
240
134
|
end
|
|
241
135
|
|
|
242
|
-
# Will add an hint message to each of the attributes in +attributes+ that is blank (using Object#blank?).
|
|
243
136
|
def add_on_blank(attributes, options = {})
|
|
244
|
-
|
|
137
|
+
Array(attributes).each do |attribute|
|
|
245
138
|
value = @base.send(:read_attribute_for_validation, attribute)
|
|
246
139
|
add(attribute, :blank, options) if value.blank?
|
|
247
140
|
end
|
|
248
141
|
end
|
|
249
142
|
|
|
250
|
-
|
|
251
|
-
# +message+ is treated the same as for +add+.
|
|
252
|
-
# p.hints.add :name, :blank
|
|
253
|
-
# p.hints.added? :name, :blank # => true
|
|
254
|
-
def added?(attribute, message = nil, options = {})
|
|
143
|
+
def added?(attribute, message = :invalid, options = {})
|
|
255
144
|
message = normalize_message(attribute, message, options)
|
|
256
|
-
self[attribute].include?
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
# Returns all the full hint messages in an array.
|
|
260
|
-
#
|
|
261
|
-
# class Company
|
|
262
|
-
# validates_presence_of :name, :address, :email
|
|
263
|
-
# validates_length_of :name, :in => 5..30
|
|
264
|
-
# end
|
|
265
|
-
#
|
|
266
|
-
# company = Company.create(:address => '123 First St.')
|
|
267
|
-
# company.hints.full_messages # =>
|
|
268
|
-
# ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
|
|
145
|
+
self[attribute].include?(message)
|
|
146
|
+
end
|
|
147
|
+
|
|
269
148
|
def full_messages
|
|
270
149
|
map { |attribute, message| full_message(attribute, message) }
|
|
271
150
|
end
|
|
272
151
|
|
|
273
|
-
# Returns a full message for a given attribute.
|
|
274
|
-
#
|
|
275
|
-
# company.hints.full_message(:name, "is invalid") # =>
|
|
276
|
-
# "Name is invalid"
|
|
277
152
|
def full_message(attribute, message)
|
|
278
153
|
return message if attribute == :base
|
|
279
|
-
|
|
280
|
-
attr_name =
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
154
|
+
|
|
155
|
+
attr_name = attribute.to_s.tr(".", "_").humanize
|
|
156
|
+
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
|
|
157
|
+
I18n.t(
|
|
158
|
+
:"hints.format",
|
|
159
|
+
default: "%{attribute} %{message}",
|
|
160
|
+
attribute: attr_name,
|
|
161
|
+
message: message
|
|
162
|
+
)
|
|
286
163
|
end
|
|
287
164
|
|
|
288
165
|
def generate_message(attribute, type, options = {})
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
166
|
+
type = options.delete(:message) if options[:message].is_a?(Symbol)
|
|
167
|
+
value = (attribute != :base ? @base.read_attribute_for_validation(attribute) : nil)
|
|
168
|
+
|
|
169
|
+
interpolation = {
|
|
170
|
+
model: @base.model_name.human,
|
|
171
|
+
attribute: @base.class.human_attribute_name(attribute, base: @base),
|
|
172
|
+
value: value,
|
|
173
|
+
object: @base,
|
|
174
|
+
count: options[:count],
|
|
175
|
+
minimum: options[:minimum],
|
|
176
|
+
maximum: options[:maximum]
|
|
177
|
+
}.compact
|
|
178
|
+
|
|
179
|
+
defaults = i18n_defaults(attribute, type, options)
|
|
180
|
+
key = defaults.shift
|
|
181
|
+
|
|
182
|
+
I18n.translate(key, **interpolation.merge(default: defaults))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def attribute_names_for_hints
|
|
188
|
+
from_record =
|
|
189
|
+
if @base.respond_to?(:attributes)
|
|
190
|
+
@base.attributes.keys
|
|
191
|
+
else
|
|
192
|
+
[]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
from_validators = @base.class.validators.flat_map(&:attributes).map(&:to_s)
|
|
196
|
+
(from_record + from_validators).map(&:to_sym).uniq
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def messages_for_validator(attribute, validator)
|
|
200
|
+
key = validator_key(validator)
|
|
201
|
+
options = validator.options
|
|
202
|
+
result = []
|
|
203
|
+
|
|
204
|
+
if options[:allow_blank] && key == "presence"
|
|
205
|
+
return result
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if options[:message].is_a?(Symbol)
|
|
209
|
+
message_key = "#{key}.#{options[:message]}"
|
|
210
|
+
result << generate_message(attribute, message_key, options)
|
|
211
|
+
return result
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
message_key = key
|
|
215
|
+
message_key = "numericality.must_be_a_number" if key == "numericality"
|
|
216
|
+
unless VALIDATORS_WITHOUT_MAIN_KEYS.include?(key)
|
|
217
|
+
result << generate_message(attribute, message_key, options)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
if key == "length" && options[:minimum] && options[:maximum]
|
|
221
|
+
result << generate_message(
|
|
222
|
+
attribute,
|
|
223
|
+
"length.within",
|
|
224
|
+
minimum: options[:minimum],
|
|
225
|
+
maximum: options[:maximum]
|
|
226
|
+
)
|
|
227
|
+
return result
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
options.each do |option, value|
|
|
231
|
+
next unless MESSAGES_FOR_OPTIONS.include?(option.to_s)
|
|
232
|
+
|
|
233
|
+
if RANGE_OPTIONS.include?(option.to_s) && value.is_a?(Range)
|
|
234
|
+
result.concat(range_hint_messages(attribute, key, value))
|
|
235
|
+
else
|
|
236
|
+
count = inclusion_exclusion_count(key, value)
|
|
237
|
+
result << generate_message(
|
|
238
|
+
attribute,
|
|
239
|
+
"#{key}.#{option}",
|
|
240
|
+
options.merge(count: count)
|
|
241
|
+
)
|
|
294
242
|
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
result
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def range_hint_messages(attribute, validator_key, range)
|
|
249
|
+
minimum = range.min
|
|
250
|
+
maximum = range.max
|
|
251
|
+
maximum -= 1 if range.exclude_end?
|
|
252
|
+
|
|
253
|
+
if validator_key == "length"
|
|
254
|
+
[
|
|
255
|
+
generate_message(attribute, "#{validator_key}.within", minimum: minimum, maximum: maximum),
|
|
256
|
+
]
|
|
295
257
|
else
|
|
296
|
-
|
|
258
|
+
[
|
|
259
|
+
generate_message(attribute, "#{validator_key}.minimum", count: minimum),
|
|
260
|
+
generate_message(attribute, "#{validator_key}.maximum", count: maximum)
|
|
261
|
+
]
|
|
297
262
|
end
|
|
263
|
+
end
|
|
298
264
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
defaults << :"hints.attributes.#{attribute}.#{type}"
|
|
302
|
-
defaults << :"hints.messages.#{type}"
|
|
265
|
+
def inclusion_exclusion_count(validator_key, value)
|
|
266
|
+
return value.to_sentence if %w[inclusion exclusion].include?(validator_key) && value.respond_to?(:to_sentence)
|
|
303
267
|
|
|
304
|
-
|
|
305
|
-
|
|
268
|
+
value
|
|
269
|
+
end
|
|
306
270
|
|
|
307
|
-
|
|
271
|
+
def validator_key(validator)
|
|
272
|
+
validator.class.name.demodulize.underscore.delete_suffix("_validator")
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def i18n_defaults(attribute, type, options)
|
|
276
|
+
attribute_name = attribute.to_s.delete_suffix("[]").remove(/\[\d+\]/)
|
|
308
277
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
278
|
+
if @base.class.respond_to?(:i18n_scope)
|
|
279
|
+
scope = @base.class.i18n_scope
|
|
280
|
+
model_defaults = @base.class.lookup_ancestors.flat_map do |klass|
|
|
281
|
+
[
|
|
282
|
+
:"#{scope}.hints.models.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.#{type}",
|
|
283
|
+
:"#{scope}.hints.models.#{klass.model_name.i18n_key}.#{type}"
|
|
284
|
+
]
|
|
285
|
+
end
|
|
286
|
+
else
|
|
287
|
+
model_defaults = []
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
defaults = model_defaults
|
|
291
|
+
defaults << options[:message] if options[:message]
|
|
292
|
+
defaults << :"#{@base.class.i18n_scope}.hints.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
|
|
293
|
+
defaults << :"hints.attributes.#{attribute_name}.#{type}"
|
|
294
|
+
defaults << :"hints.messages.#{type}"
|
|
295
|
+
defaults.compact.flatten
|
|
320
296
|
end
|
|
321
297
|
|
|
298
|
+
def normalize_message(attribute, message, options = {})
|
|
299
|
+
case message
|
|
300
|
+
when Symbol
|
|
301
|
+
generate_message(attribute, message, options)
|
|
302
|
+
when Proc
|
|
303
|
+
message.call
|
|
304
|
+
when nil
|
|
305
|
+
generate_message(attribute, :invalid, options)
|
|
306
|
+
else
|
|
307
|
+
message
|
|
308
|
+
end
|
|
309
|
+
end
|
|
322
310
|
end
|
|
323
|
-
|
|
324
311
|
end
|
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
## YAML Template.
|
|
2
1
|
---
|
|
3
2
|
en:
|
|
4
|
-
activerecord:
|
|
5
|
-
less_than_or_equal_to: "must be less than or equal to %{count}"
|
|
6
3
|
hints:
|
|
7
|
-
# The default format to use in full error messages.
|
|
8
4
|
format: "%{attribute} %{message}"
|
|
9
5
|
|
|
10
|
-
# The values :model, :attribute and :value are always available for interpolation
|
|
11
|
-
# The value :count is available when applicable. Can be used for pluralization.
|
|
12
6
|
messages:
|
|
13
|
-
|
|
7
|
+
invalid: "is invalid"
|
|
8
|
+
blank: "can't be blank"
|
|
9
|
+
empty: "can't be empty"
|
|
14
10
|
inclusion:
|
|
15
11
|
in: "must be one of %{count}"
|
|
16
12
|
exclusion:
|
|
17
13
|
in: "must not be one of %{count}"
|
|
18
|
-
format: "
|
|
14
|
+
format: "must match the required format"
|
|
19
15
|
associated: "is invalid"
|
|
20
16
|
uniqueness: "must be unique"
|
|
21
17
|
confirmation: "doesn't match confirmation"
|
|
@@ -25,7 +21,7 @@ en:
|
|
|
25
21
|
maximum: "must not be longer than %{count} characters"
|
|
26
22
|
minimum: "must not be shorter than %{count} characters"
|
|
27
23
|
is: "must be exactly %{count} characters"
|
|
28
|
-
|
|
24
|
+
within: "must be between %{minimum} and %{maximum} characters"
|
|
29
25
|
numericality:
|
|
30
26
|
must_be_a_number: "must be a number"
|
|
31
27
|
only_integer: "must be an integer"
|
|
@@ -36,4 +32,3 @@ en:
|
|
|
36
32
|
less_than_or_equal_to: "must be less than or equal to %{count}"
|
|
37
33
|
odd: "must be odd"
|
|
38
34
|
even: "must be even"
|
|
39
|
-
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ValidationHints
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
initializer "validation_hints.i18n" do
|
|
6
|
+
ValidationHints.load_i18n!
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
ActiveSupport.on_load(:active_model) do
|
|
10
|
+
ValidationHints::ValidationsPatch.apply!
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ValidationHints
|
|
4
|
+
module ValidationsPatch
|
|
5
|
+
def self.apply!
|
|
6
|
+
return if applied?
|
|
7
|
+
|
|
8
|
+
ActiveModel::Validations::ClassMethods.module_eval do
|
|
9
|
+
def has_validations?
|
|
10
|
+
!validators.empty?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def has_validations_for?(attribute)
|
|
14
|
+
!validators_on(attribute).empty?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
ActiveModel::Validations.module_eval do
|
|
19
|
+
def has_validations?
|
|
20
|
+
self.class.has_validations?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def has_validations_for?(attribute)
|
|
24
|
+
self.class.has_validations_for?(attribute)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def hints
|
|
28
|
+
@hints ||= ActiveModel::Hints.new(self)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@applied = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.applied?
|
|
36
|
+
@applied
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/validation_hints.rb
CHANGED
|
@@ -1,39 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "validation_hints/version"
|
|
4
|
+
require "active_model/hints"
|
|
5
5
|
|
|
6
|
-
module
|
|
6
|
+
module ValidationHints
|
|
7
|
+
LOCALE_PATH = File.expand_path("validation_hints/locale/en.yml", __dir__)
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
module ClassMethods
|
|
11
|
-
|
|
12
|
-
def has_validations?
|
|
13
|
-
! self.validators.empty?
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def has_validations_for?(attribute)
|
|
17
|
-
! self.validators_on(attribute).empty?
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def has_validations?
|
|
23
|
-
self.class.has_validations?
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def has_validations_for?(attribute)
|
|
27
|
-
self.class.has_validations_for?(attribute)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def hints
|
|
31
|
-
@hints ||= Hints.new(self)
|
|
32
|
-
end
|
|
9
|
+
def self.load_i18n!
|
|
10
|
+
return if @i18n_loaded
|
|
33
11
|
|
|
12
|
+
require "i18n"
|
|
13
|
+
I18n.load_path << LOCALE_PATH unless I18n.load_path.include?(LOCALE_PATH)
|
|
14
|
+
@i18n_loaded = true
|
|
34
15
|
end
|
|
35
|
-
|
|
36
16
|
end
|
|
37
17
|
|
|
38
|
-
require
|
|
39
|
-
|
|
18
|
+
require "validation_hints/validations_patch"
|
|
19
|
+
|
|
20
|
+
if defined?(Rails::Railtie)
|
|
21
|
+
require "validation_hints/railtie"
|
|
22
|
+
else
|
|
23
|
+
require "active_model"
|
|
24
|
+
ValidationHints::ValidationsPatch.apply!
|
|
25
|
+
ValidationHints.load_i18n!
|
|
26
|
+
end
|