validation_hints 6.1.0 → 6.2.1

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: '093064f733917dd331d9ecaa77e6a9204dbff272010de712ae70c05965f5b271'
4
- data.tar.gz: 0b33c6d54febdd3dab42c30230deb3df6d5a9f3e5dd1ed24110d70e7e616e207
3
+ metadata.gz: 99fb1203ae7e46c6f7df8edf84c00a7b55c6901a0dbd43dcfd9eb2b96acbd9dc
4
+ data.tar.gz: c83cbcab470dba35e53f2d991f5b219e4be2ef33b453649df2498f65b1c4f654
5
5
  SHA512:
6
- metadata.gz: c74d07e79ea487b1f81559dd0b92fc887f38f3483a0c99593453c61957b82ee3fdabdc0b762c3e28cd40247df788d161b77557e7908a8928a4471888dc188f25
7
- data.tar.gz: 944ce03510f53c545b27bf9c813649fb8994a7cf64e200693b93a4fbe363b11eb60f14eea93cd7600f032ae5af0719a37614dc1fc39504b59343dbd82f27d860
6
+ metadata.gz: 81b2faea546b51aa257ebb9776993ea9598a8754534deb6f1cce220eb97e9ccb2828bb8eed1d75f11c32073d47d53d20cd4e5685fe2314c34534bbf5f7725454
7
+ data.tar.gz: 67012560801eb03bc7aef5617c04296d518636ab46973a8edb36ae73178765deb347ec36a503c9b7f58be5637345b17d9c4bca5375db42d9f713afc215321193
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## 6.2.1
6
+
7
+ ### Fixed
8
+
9
+ - **`ActiveModel::Hints` and frozen validator options:** duplicate `validator.options` and `generate_message` options before mutation so Rails 7 frozen presence validators (`message: :required`) no longer raise `can't modify frozen Hash`.
10
+
11
+ ## 6.2.0
12
+
13
+ I18n lookup chain and documentation (Phase 2).
14
+
15
+ ### Added
16
+
17
+ - `activemodel.hints` and `activerecord.hints` locale scopes (format + lookup chain).
18
+ - `test/active_model/i18n_test.rb` — app override and `human_attribute_name` coverage.
19
+ - README: requirements, API, I18n lookup order, and `config/locales` override examples.
20
+
21
+ ### Changed
22
+
23
+ - `generate_message` / `full_message` I18n fallback chain mirrors Rails errors (with `hints` namespace).
24
+ - `full_message` passes `base: @base` to `human_attribute_name` and resolves format via scoped `hints.format` keys.
25
+
5
26
  ## 6.1.0
6
27
 
7
28
  Rails 7 compatibility for `ActiveModel::Hints` and expanded test coverage (Phase 1).
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- validation_hints (6.1.0)
4
+ validation_hints (6.2.1)
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.1.0)
267
+ validation_hints (6.2.1)
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
data/README.md CHANGED
@@ -1,34 +1,93 @@
1
+ # validation_hints
1
2
 
2
- ## Validation Hints
3
+ Proactive validation hints derived from model validators — complementary to Rails `errors` (which react after `valid?` fails).
3
4
 
4
- Delivers hints derived from the validation on a model.
5
+ **Requirements:** Ruby >= 3.2, Rails / Active Record 7.0.x (`>= 7.0`, `< 7.1`). Used by [inline_forms](https://github.com/acesuares/inline_forms).
5
6
 
6
- ### Install
7
+ ## Install
7
8
 
8
9
  ```ruby
9
- gem 'validation_hints'
10
+ # Gemfile
11
+ gem "validation_hints", "~> 6.2"
10
12
  ```
11
13
 
12
- ### Example
14
+ Rails apps load the gem via `ValidationHints::Railtie` (no manual `require`).
15
+
16
+ ## Example
13
17
 
14
18
  ```ruby
15
- class Person < ActiveRecord::Base
16
- validates :name, :presence => true
17
- validates :password, :length => { :within => 1...5 }
19
+ class Person < ApplicationRecord
20
+ validates :name, presence: true
21
+ validates :password, length: { within: 1...5 }
18
22
  end
23
+
24
+ Person.new.hints[:name] # => ["can't be blank"]
25
+ Person.new.hints[:password] # => ["must be between 1 and 4 characters"]
26
+
27
+ Person.new.hints.full_messages_for(:name) # => ["Name can't be blank"]
28
+ Person.new.has_validations_for?(:name) # => true
19
29
  ```
20
30
 
21
- ```ruby
22
- Person.new.hints[:name] => ["can't be blank"]
23
- Person.new.hints[:password] => ["must not be shorter than 1 characters", "must not be longer than 4 characters"]
24
- Person.new.hints.messages => {:id=>[], :password=>["must not be shorter than 1 characters", "must not be longer than 4 characters"], :name => ["can't be blank"] }
31
+ ## API (stable)
32
+
33
+ | Method | Description |
34
+ |--------|-------------|
35
+ | `model.hints` | `ActiveModel::Hints` for the instance |
36
+ | `hints[:attribute]` | Short hint strings for an attribute |
37
+ | `hints.full_messages_for(attr)` | Attribute label + hint (used by inline_forms tooltips) |
38
+ | `has_validations?` / `has_validations_for?(attr)` | Whether to show hint UI |
39
+
40
+ ## I18n
41
+
42
+ Default copy lives in the gem at `lib/validation_hints/locale/en.yml` under the `hints` namespace.
43
+
44
+ Lookup order mirrors Rails **errors**, but uses **`hints`** instead of `errors`:
45
+
46
+ 1. `activerecord.hints.models.<model>.attributes.<attr>.<type>`
47
+ 2. `activerecord.hints.models.<model>.<type>`
48
+ 3. `activerecord.hints.messages.<type>`
49
+ 4. `activemodel.hints.messages.<type>`
50
+ 5. `hints.attributes.<attr>.<type>`
51
+ 6. `hints.messages.<type>`
52
+
53
+ Full messages (`full_messages_for`) use `hints.format` with the same scoping (`activerecord.hints.format`, `activemodel.hints.format`, `hints.format`).
54
+
55
+ ### App overrides
56
+
57
+ Add a locale file, e.g. `config/locales/validation_hints.en.yml`:
58
+
59
+ ```yaml
60
+ en:
61
+ activerecord:
62
+ hints:
63
+ format: "%{attribute}: %{message}"
64
+ messages:
65
+ presence: "is required"
66
+ models:
67
+ apartment:
68
+ attributes:
69
+ name:
70
+ presence: "enter a name for this apartment"
25
71
  ```
26
72
 
27
- ### Disclaimer
73
+ Per-attribute overrides without a model block:
74
+
75
+ ```yaml
76
+ en:
77
+ hints:
78
+ attributes:
79
+ name:
80
+ presence: "is required"
81
+ ```
28
82
 
29
- It's work in progress.
30
- validation_hints was for the most part derived from activerecord-3.2.3/lib/active_record/errors.rb
83
+ Attribute labels in full messages come from the normal Rails path (`activerecord.attributes.<model>.<attr>`), same as errors.
31
84
 
85
+ ## Tests
32
86
 
87
+ ```bash
88
+ bundle exec rake test
89
+ ```
33
90
 
91
+ ## History
34
92
 
93
+ See [CHANGELOG.md](CHANGELOG.md).
@@ -153,16 +153,21 @@ module ActiveModel
153
153
  return message if attribute == :base
154
154
 
155
155
  attr_name = attribute.to_s.tr(".", "_").humanize
156
- attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
156
+ attr_name = @base.class.human_attribute_name(attribute, default: attr_name, base: @base)
157
+
158
+ format_defaults = i18n_format_defaults
159
+ format_key = format_defaults.shift
160
+
157
161
  I18n.t(
158
- :"hints.format",
159
- default: "%{attribute} %{message}",
162
+ format_key,
163
+ default: format_defaults,
160
164
  attribute: attr_name,
161
165
  message: message
162
166
  )
163
167
  end
164
168
 
165
169
  def generate_message(attribute, type, options = {})
170
+ options = options.dup
166
171
  type = options.delete(:message) if options[:message].is_a?(Symbol)
167
172
  value = (attribute != :base ? @base.read_attribute_for_validation(attribute) : nil)
168
173
 
@@ -198,7 +203,7 @@ module ActiveModel
198
203
 
199
204
  def messages_for_validator(attribute, validator)
200
205
  key = validator_key(validator)
201
- options = validator.options
206
+ options = validator.options.dup
202
207
  result = []
203
208
 
204
209
  if options[:allow_blank] && key == "presence"
@@ -273,28 +278,50 @@ module ActiveModel
273
278
  end
274
279
 
275
280
  def i18n_defaults(attribute, type, options)
276
- attribute_name = attribute.to_s.delete_suffix("[]").remove(/\[\d+\]/)
281
+ attribute_name = attribute.to_s.remove(/\[\d+\]/)
282
+
283
+ defaults = model_hint_defaults(attribute_name, type)
284
+ defaults << options[:message] if options[:message]
277
285
 
278
286
  if @base.class.respond_to?(:i18n_scope)
279
287
  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
+ defaults << :"#{scope}.hints.messages.#{type}"
288
289
  end
289
290
 
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)
291
+ defaults << :"activemodel.hints.messages.#{type}"
293
292
  defaults << :"hints.attributes.#{attribute_name}.#{type}"
294
293
  defaults << :"hints.messages.#{type}"
295
294
  defaults.compact.flatten
296
295
  end
297
296
 
297
+ def model_hint_defaults(attribute_name, type)
298
+ return [] unless @base.class.respond_to?(:i18n_scope)
299
+
300
+ scope = @base.class.i18n_scope
301
+ @base.class.lookup_ancestors.flat_map do |klass|
302
+ [
303
+ :"#{scope}.hints.models.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.#{type}",
304
+ :"#{scope}.hints.models.#{klass.model_name.i18n_key}.#{type}"
305
+ ]
306
+ end
307
+ end
308
+
309
+ def i18n_format_defaults
310
+ defaults = []
311
+
312
+ if @base.class.respond_to?(:i18n_scope)
313
+ scope = @base.class.i18n_scope
314
+ @base.class.lookup_ancestors.each do |klass|
315
+ defaults << :"#{scope}.hints.models.#{klass.model_name.i18n_key}.format"
316
+ end
317
+ defaults << :"#{scope}.hints.format"
318
+ end
319
+
320
+ defaults << :"activemodel.hints.format"
321
+ defaults << :"hints.format"
322
+ defaults << "%{attribute} %{message}"
323
+ end
324
+
298
325
  def normalize_message(attribute, message, options = {})
299
326
  case message
300
327
  when Symbol
@@ -1,5 +1,13 @@
1
1
  ---
2
2
  en:
3
+ activemodel:
4
+ hints:
5
+ format: "%{attribute} %{message}"
6
+
7
+ activerecord:
8
+ hints:
9
+ format: "%{attribute} %{message}"
10
+
3
11
  hints:
4
12
  format: "%{attribute} %{message}"
5
13
 
@@ -1,4 +1,4 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module ValidationHints
3
- VERSION = "6.1.0"
3
+ VERSION = "6.2.1"
4
4
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ActiveModelHintsI18nTest < Minitest::Test
6
+ def setup
7
+ @person = Person.new
8
+ @backend = I18n.backend
9
+ @kv = I18n::Backend::KeyValue.new({})
10
+ I18n.backend = I18n::Backend::Chain.new(@kv, @backend)
11
+ end
12
+
13
+ def teardown
14
+ I18n.backend = @backend
15
+ end
16
+
17
+ def test_activerecord_hints_messages_override
18
+ @kv.store_translations(:en, activerecord: { hints: { messages: { presence: "is required" } } })
19
+
20
+ assert_includes @person.hints[:name], "is required"
21
+ end
22
+
23
+ def test_activerecord_hints_model_attribute_override
24
+ @kv.store_translations(
25
+ :en,
26
+ activerecord: {
27
+ hints: {
28
+ models: {
29
+ person: {
30
+ attributes: {
31
+ name: { presence: "needs a name" }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ )
38
+
39
+ assert_equal ["needs a name"], @person.hints[:name]
40
+ end
41
+
42
+ def test_hints_attributes_override
43
+ @kv.store_translations(:en, hints: { attributes: { name: { presence: "name hint" } } })
44
+
45
+ assert_equal ["name hint"], @person.hints[:name]
46
+ end
47
+
48
+ def test_top_level_hints_messages_override
49
+ @kv.store_translations(:en, hints: { messages: { presence: "fill this in" } })
50
+
51
+ assert_includes @person.hints[:name], "fill this in"
52
+ end
53
+
54
+ def test_full_message_uses_activerecord_human_attribute_name
55
+ @kv.store_translations(:en, activerecord: { attributes: { person: { name: "Your name" } } })
56
+
57
+ assert_equal ["Your name can't be blank"], @person.hints.full_messages_for(:name)
58
+ end
59
+
60
+ def test_full_message_format_override
61
+ @kv.store_translations(:en, activerecord: { hints: { format: "%{attribute}: %{message}" } })
62
+
63
+ assert_equal ["Name: can't be blank"], @person.hints.full_messages_for(:name)
64
+ end
65
+ end
@@ -4,7 +4,7 @@ require "test_helper"
4
4
 
5
5
  class ValidationHintsTest < Minitest::Test
6
6
  def test_version
7
- assert_equal "6.1.0", ValidationHints::VERSION
7
+ assert_equal "6.2.0", ValidationHints::VERSION
8
8
  end
9
9
 
10
10
  def test_locale_path_exists
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.1.0
4
+ version: 6.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -114,6 +114,7 @@ files:
114
114
  - lib/validation_hints/validations_patch.rb
115
115
  - lib/validation_hints/version.rb
116
116
  - test/active_model/hints_test.rb
117
+ - test/active_model/i18n_test.rb
117
118
  - test/test_helper.rb
118
119
  - test/validation_hints_test.rb
119
120
  - validation_hints.gemspec