validation_hints 6.0.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f43f9338ea91967565859b93f7a0f884cf4d3999d83abda9d23ac55673a77ec
4
- data.tar.gz: 399657680fb3f0a005aea6034c41a52c1f96b21c051e811b7dbe6f2c0d24fc0c
3
+ metadata.gz: '093064f733917dd331d9ecaa77e6a9204dbff272010de712ae70c05965f5b271'
4
+ data.tar.gz: 0b33c6d54febdd3dab42c30230deb3df6d5a9f3e5dd1ed24110d70e7e616e207
5
5
  SHA512:
6
- metadata.gz: 0c3e3a58ea45b2620e516216d8d5267fefe2bff0c43c2de1956ab2bb3c15174418ed68f867c9d05bb56fa280107b8095afc9520b03f7dc93e231f445edad52cb
7
- data.tar.gz: 602bbff26e2aea34e3117f9223063a8ba461608d8f51da3b9bb357418260dc8abe9e9455772bfbfbed4404e80133cfa2714c67ed3347bb031aed6527d5efc951
6
+ metadata.gz: c74d07e79ea487b1f81559dd0b92fc887f38f3483a0c99593453c61957b82ee3fdabdc0b762c3e28cd40247df788d161b77557e7908a8928a4471888dc188f25
7
+ data.tar.gz: 944ce03510f53c545b27bf9c813649fb8994a7cf64e200693b93a4fbe363b11eb60f14eea93cd7600f032ae5af0719a37614dc1fc39504b59343dbd82f27d860
data/CHANGELOG.md CHANGED
@@ -2,7 +2,32 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
- ## 6.0.0 (unreleased)
5
+ ## 6.1.0
6
+
7
+ Rails 7 compatibility for `ActiveModel::Hints` and expanded test coverage (Phase 1).
8
+
9
+ ### Added
10
+
11
+ - `test/active_model/hints_test.rb` — presence, length, numericality, inclusion, virtual attributes, `empty?`, `add`/`added?`.
12
+ - In-memory SQLite ActiveRecord setup in `test/test_helper.rb`.
13
+ - `normalize_message` for mutable hint APIs (`add`, `added?`).
14
+ - `length.within` locale key with `%{minimum}` / `%{maximum}` interpolation.
15
+ - Default hint messages: `invalid`, `blank`, `empty`.
16
+
17
+ ### Changed
18
+
19
+ - `ActiveModel::Hints` modernized for Rails 7:
20
+ - plain `Hash` instead of `OrderedHash`
21
+ - fixed `empty?` / `blank?`
22
+ - `generate_message` aligned with Rails 7 I18n chain and interpolation
23
+ - validator keys via `demodulize.underscore.delete_suffix("_validator")`
24
+ - attribute discovery includes virtual attributes from validators
25
+ - length min+max combined into a single `within` hint
26
+ - presence hint suppressed when validator has `allow_blank: true`
27
+ - `locale/en.yml` cleaned up (removed stray keys; format copy → “must match the required format”).
28
+ - Documented behavior: conditional validators (`:if`, `:unless`, `:on`) are not evaluated.
29
+
30
+ ## 6.0.0
6
31
 
7
32
  Modernize the gem for **Ruby 3.2+** and **Rails 7.0.x**, targeting use with **inline_forms 7.x**.
8
33
 
@@ -14,20 +39,19 @@ Modernize the gem for **Ruby 3.2+** and **Rails 7.0.x**, targeting use with **in
14
39
 
15
40
  ### Added
16
41
 
17
- - `ValidationHints::Railtie` — registers I18n and patches `ActiveModel::Validations` via `ActiveSupport.on_load(:active_model)` (Rails apps no longer need a manual `require`).
42
+ - `ValidationHints::Railtie` — registers I18n and patches `ActiveModel::Validations` via `ActiveSupport.on_load(:active_model)`.
18
43
  - `ValidationHints::ValidationsPatch` — extracted module for `has_validations*` and `hints`.
19
44
  - `ValidationHints.load_i18n!` — idempotent locale registration.
20
45
  - Runtime dependency on `activerecord` 7.0.x.
21
46
  - Minitest harness (`test/`) and `rake test` as the default Rake task.
22
47
  - `CHANGELOG.md`.
48
+ - `rake release` / gem packaging tasks via `Bundler::GemHelper`.
23
49
 
24
50
  ### Changed
25
51
 
26
- - `ValidationHints::VERSION` set to `6.0.0`.
27
- - Gemspec rewritten for current RubyGems / Bundler (Ruby version, dependencies, file list with non-git fallback).
52
+ - Gemspec rewritten for current RubyGems / Bundler.
28
53
  - `Gemfile` source updated to `https://rubygems.org`.
29
- - Entry point (`lib/validation_hints.rb`) refactored: Railtie in Rails, direct `active_model` load outside Rails.
30
- - I18n locale registration moved behind `ValidationHints.load_i18n!` (was unconditional append at load time).
54
+ - Entry point refactored: Railtie in Rails, direct `active_model` load outside Rails.
31
55
 
32
56
  ### Removed
33
57
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- validation_hints (6.0.0)
4
+ validation_hints (6.1.0)
5
5
  activerecord (>= 7.0, < 7.1)
6
6
 
7
7
  GEM
@@ -264,7 +264,7 @@ CHECKSUMS
264
264
  thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
265
265
  timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
266
266
  tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
267
- validation_hints (6.0.0)
267
+ validation_hints (6.1.0)
268
268
  websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962
269
269
  websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
270
270
  zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
@@ -1,67 +1,37 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveModel
2
- # == Active Model Hints
4
+ # Introspects declared validators and builds proactive hint messages (before +valid?+ fails).
3
5
  #
4
- # p = Person.new
5
- # p.hints
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
- CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
14
- MESSAGES_FOR_VALIDATORS = %w(confirmation acceptance presence uniqueness format associated numericality)
15
- VALIDATORS_WITHOUT_MAIN_KEYS = %w(exclusion format inclusion length numericality)
16
- # and these? validates_with validates_each
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
- # Should virtual element for
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 = base
40
- @messages = ActiveSupport::OrderedHash.new
41
- @base.attributes.keys.each do |a|
42
- @messages[a.to_sym] = hints_for(a.to_sym)
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
- result = Array.new
48
- @base.class.validators_on(attribute).map do |v|
49
- validator = v.class.to_s.split('::').last.underscore.gsub('_validator','')
50
- if v.options[:message].is_a?(Symbol)
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
- # Do the hint messages include an hint with key +hint+?
92
- def include?(hint)
93
- (v = messages[hint]) && v.any?
51
+ def include?(attribute)
52
+ (value = messages[attribute.to_sym]) && value.any?
94
53
  end
95
- alias :has_key? :include?
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? { |k, v| v && v.empty? && !v.is_a?(String) }
103
+ messages.values.all?(&:empty?)
191
104
  end
192
105
  alias_method :blank?, :empty?
193
106
 
194
- # Returns an xml formatted representation of the hints hash.
195
- #
196
- # p.hints.add(:name, "can't be blank")
197
- # p.hints.add(:name, "must be specified")
198
- # p.hints.to_xml
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
- # Adds +message+ to the hint messages on +attribute+. More than one hint can be added to the same
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
- [attributes].flatten.each do |attribute|
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
- [attributes].flatten.each do |attribute|
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
- # Returns true if an hint on the attribute with the given message is present, false otherwise.
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? message
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
- attr_name = attribute.to_s.gsub('.', '_').humanize
280
- attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
281
- I18n.t(:"hints.format",
282
- :default => "%{attribute} %{message}",
283
- :attribute => attr_name,
284
- :message => message
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
- #options.delete(:message) if options[:message].is_a?(Symbol)
290
- if @base.class.respond_to?(:i18n_scope)
291
- defaults = @base.class.lookup_ancestors.map do |klass|
292
- [ :"#{@base.class.i18n_scope}.hints.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
293
- :"#{@base.class.i18n_scope}.hints.models.#{klass.model_name.i18n_key}.#{type}" ]
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
- defaults = []
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
- defaults << options[:message] # defaults << options.delete(:message)
300
- defaults << :"#{@base.class.i18n_scope}.hints.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
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
- defaults.compact!
305
- defaults.flatten!
268
+ value
269
+ end
306
270
 
307
- key = defaults.shift
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
- # options = {
310
- # :default => defaults,
311
- # :model => @base.class.model_name.human,
312
- # :attribute => @base.class.human_attribute_name(attribute),
313
- # }.merge(options)
314
- I18n.translate( key,
315
- default: defaults,
316
- model: @base.class.model_name.human,
317
- attribute: @base.class.human_attribute_name(attribute),
318
- count: options[:count],
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
- is_a_curacao_id_number: 'hello world!'
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: "what's this"
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
- radiolaria: "must be between %{minimum} and %{maximum} characters"
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
-
@@ -1,4 +1,4 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module ValidationHints
3
- VERSION = "6.0.0"
3
+ VERSION = "6.1.0"
4
4
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ActiveModelHintsTest < Minitest::Test
6
+ def setup
7
+ @person = Person.new
8
+ @profile = Profile.new
9
+ end
10
+
11
+ def test_presence_hint
12
+ assert_includes @person.hints[:name], "can't be blank"
13
+ end
14
+
15
+ def test_full_messages_for_includes_human_attribute_name
16
+ messages = @person.hints.full_messages_for(:name)
17
+ assert_equal ["Name can't be blank"], messages
18
+ end
19
+
20
+ def test_has_validations_for
21
+ assert @person.has_validations_for?(:name)
22
+ refute @person.has_validations_for?(:missing)
23
+ end
24
+
25
+ def test_has_validations_on_class_and_instance
26
+ assert Person.has_validations?
27
+ assert @person.has_validations?
28
+ end
29
+
30
+ def test_length_within_hint
31
+ hints = @person.hints[:password]
32
+ assert_includes hints, "must be between 1 and 4 characters"
33
+ end
34
+
35
+ def test_numericality_option_hints
36
+ hints = @person.hints[:age]
37
+ assert_includes hints, "must be an integer"
38
+ assert_includes hints, "must be greater than 19"
39
+ end
40
+
41
+ def test_inclusion_hint_uses_list
42
+ hints = @person.hints[:status]
43
+ assert_equal 1, hints.size
44
+ assert_match(/must be one of/, hints.first)
45
+ assert_match(/active/, hints.first)
46
+ assert_match(/inactive/, hints.first)
47
+ end
48
+
49
+ def test_virtual_attribute_with_validators_is_included
50
+ assert_includes @profile.hints.keys, :nickname
51
+ assert_includes @profile.hints[:nickname], "can't be blank"
52
+ end
53
+
54
+ def test_empty_returns_true_when_no_hint_messages
55
+ person = Class.new do
56
+ include ActiveModel::Model
57
+ include ActiveModel::Validations
58
+
59
+ def self.name
60
+ "EmptyModel"
61
+ end
62
+ end.new
63
+
64
+ assert person.hints.empty?
65
+ end
66
+
67
+ def test_add_and_added_with_symbol_message
68
+ person = Person.new
69
+ person.hints.add(:name, :blank)
70
+ assert person.hints.added?(:name, :blank)
71
+ end
72
+
73
+ def test_validator_keys_for_rails_7_validators
74
+ validators = Person.validators_on(:name).map { |v| v.class.name.demodulize }
75
+ assert_includes validators, "PresenceValidator"
76
+ end
77
+ end
data/test/test_helper.rb CHANGED
@@ -2,5 +2,33 @@
2
2
 
3
3
  require "bundler/setup"
4
4
  require "minitest/autorun"
5
+ require "active_record"
6
+ require "active_model"
5
7
 
6
8
  require "validation_hints"
9
+
10
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
11
+
12
+ ActiveRecord::Schema.define do
13
+ create_table :people, force: true do |t|
14
+ t.string :name
15
+ t.string :password
16
+ t.integer :age
17
+ t.string :status
18
+ end
19
+ end
20
+
21
+ class Person < ActiveRecord::Base
22
+ validates :name, presence: true
23
+ validates :password, length: { within: 1...5 }
24
+ validates :age, numericality: { only_integer: true, greater_than: 19 }
25
+ validates :status, inclusion: { in: %w[active inactive] }
26
+ end
27
+
28
+ class Profile < ActiveRecord::Base
29
+ self.table_name = "people"
30
+
31
+ attr_accessor :nickname
32
+
33
+ validates :nickname, presence: true
34
+ end
@@ -4,11 +4,10 @@ require "test_helper"
4
4
 
5
5
  class ValidationHintsTest < Minitest::Test
6
6
  def test_version
7
- assert_equal "6.0.0", ValidationHints::VERSION
7
+ assert_equal "6.1.0", ValidationHints::VERSION
8
8
  end
9
9
 
10
10
  def test_locale_path_exists
11
- assert File.file?(ValidationHints::LOCALE_PATH),
12
- "expected locale at #{ValidationHints::LOCALE_PATH}"
11
+ assert File.file?(ValidationHints::LOCALE_PATH)
13
12
  end
14
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: validation_hints
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0
4
+ version: 6.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -113,6 +113,7 @@ files:
113
113
  - lib/validation_hints/railtie.rb
114
114
  - lib/validation_hints/validations_patch.rb
115
115
  - lib/validation_hints/version.rb
116
+ - test/active_model/hints_test.rb
116
117
  - test/test_helper.rb
117
118
  - test/validation_hints_test.rb
118
119
  - validation_hints.gemspec