validation_hints 6.1.0 → 6.2.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/CHANGELOG.md +15 -0
- data/Gemfile.lock +2 -2
- data/README.md +74 -15
- data/lib/active_model/hints.rb +41 -15
- data/lib/validation_hints/locale/en.yml +8 -0
- data/lib/validation_hints/version.rb +1 -1
- data/test/active_model/i18n_test.rb +65 -0
- data/test/validation_hints_test.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 417c0455d8dedf1c33c7cf608af48ec92ecb6b1d913f60f58ac0d7b91d0eb58d
|
|
4
|
+
data.tar.gz: 216254f45fa84b4370828da2830d79689fbd3971da68fa0e4e891873ed40029f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a961271d6a978a874745411c8251e8fb255c974917da3dbcaad7e469caada854ef7f9437d0de8972d266348a3d054e5952bb81cd42f5ed3515d1c45d1a435e6
|
|
7
|
+
data.tar.gz: 974c699d5c1acaf13dd0494156cd8d46f5628f6b9ef9d874c873804d33d75d6df745fd62fc5fa8db41210cca236f9a5642df2da571dd55ecb5e245527417326d
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## 6.2.0
|
|
6
|
+
|
|
7
|
+
I18n lookup chain and documentation (Phase 2).
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `activemodel.hints` and `activerecord.hints` locale scopes (format + lookup chain).
|
|
12
|
+
- `test/active_model/i18n_test.rb` — app override and `human_attribute_name` coverage.
|
|
13
|
+
- README: requirements, API, I18n lookup order, and `config/locales` override examples.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `generate_message` / `full_message` I18n fallback chain mirrors Rails errors (with `hints` namespace).
|
|
18
|
+
- `full_message` passes `base: @base` to `human_attribute_name` and resolves format via scoped `hints.format` keys.
|
|
19
|
+
|
|
5
20
|
## 6.1.0
|
|
6
21
|
|
|
7
22
|
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.
|
|
4
|
+
validation_hints (6.2.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.
|
|
267
|
+
validation_hints (6.2.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
|
data/README.md
CHANGED
|
@@ -1,34 +1,93 @@
|
|
|
1
|
+
# validation_hints
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
Proactive validation hints derived from model validators — complementary to Rails `errors` (which react after `valid?` fails).
|
|
3
4
|
|
|
4
|
-
|
|
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
|
-
|
|
7
|
+
## Install
|
|
7
8
|
|
|
8
9
|
```ruby
|
|
9
|
-
|
|
10
|
+
# Gemfile
|
|
11
|
+
gem "validation_hints", "~> 6.2"
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
Rails apps load the gem via `ValidationHints::Railtie` (no manual `require`).
|
|
15
|
+
|
|
16
|
+
## Example
|
|
13
17
|
|
|
14
18
|
```ruby
|
|
15
|
-
class Person <
|
|
16
|
-
validates :name, :
|
|
17
|
-
validates :password, :
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
data/lib/active_model/hints.rb
CHANGED
|
@@ -153,10 +153,14 @@ 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
|
-
|
|
159
|
-
default:
|
|
162
|
+
format_key,
|
|
163
|
+
default: format_defaults,
|
|
160
164
|
attribute: attr_name,
|
|
161
165
|
message: message
|
|
162
166
|
)
|
|
@@ -273,28 +277,50 @@ module ActiveModel
|
|
|
273
277
|
end
|
|
274
278
|
|
|
275
279
|
def i18n_defaults(attribute, type, options)
|
|
276
|
-
attribute_name = attribute.to_s.
|
|
280
|
+
attribute_name = attribute.to_s.remove(/\[\d+\]/)
|
|
281
|
+
|
|
282
|
+
defaults = model_hint_defaults(attribute_name, type)
|
|
283
|
+
defaults << options[:message] if options[:message]
|
|
277
284
|
|
|
278
285
|
if @base.class.respond_to?(:i18n_scope)
|
|
279
286
|
scope = @base.class.i18n_scope
|
|
280
|
-
|
|
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 = []
|
|
287
|
+
defaults << :"#{scope}.hints.messages.#{type}"
|
|
288
288
|
end
|
|
289
289
|
|
|
290
|
-
defaults
|
|
291
|
-
defaults << options[:message] if options[:message]
|
|
292
|
-
defaults << :"#{@base.class.i18n_scope}.hints.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
|
|
290
|
+
defaults << :"activemodel.hints.messages.#{type}"
|
|
293
291
|
defaults << :"hints.attributes.#{attribute_name}.#{type}"
|
|
294
292
|
defaults << :"hints.messages.#{type}"
|
|
295
293
|
defaults.compact.flatten
|
|
296
294
|
end
|
|
297
295
|
|
|
296
|
+
def model_hint_defaults(attribute_name, type)
|
|
297
|
+
return [] unless @base.class.respond_to?(:i18n_scope)
|
|
298
|
+
|
|
299
|
+
scope = @base.class.i18n_scope
|
|
300
|
+
@base.class.lookup_ancestors.flat_map do |klass|
|
|
301
|
+
[
|
|
302
|
+
:"#{scope}.hints.models.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.#{type}",
|
|
303
|
+
:"#{scope}.hints.models.#{klass.model_name.i18n_key}.#{type}"
|
|
304
|
+
]
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def i18n_format_defaults
|
|
309
|
+
defaults = []
|
|
310
|
+
|
|
311
|
+
if @base.class.respond_to?(:i18n_scope)
|
|
312
|
+
scope = @base.class.i18n_scope
|
|
313
|
+
@base.class.lookup_ancestors.each do |klass|
|
|
314
|
+
defaults << :"#{scope}.hints.models.#{klass.model_name.i18n_key}.format"
|
|
315
|
+
end
|
|
316
|
+
defaults << :"#{scope}.hints.format"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
defaults << :"activemodel.hints.format"
|
|
320
|
+
defaults << :"hints.format"
|
|
321
|
+
defaults << "%{attribute} %{message}"
|
|
322
|
+
end
|
|
323
|
+
|
|
298
324
|
def normalize_message(attribute, message, options = {})
|
|
299
325
|
case message
|
|
300
326
|
when Symbol
|
|
@@ -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
|
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.
|
|
4
|
+
version: 6.2.0
|
|
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
|