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 +4 -4
- data/CHANGELOG.md +21 -0
- data/Gemfile.lock +2 -2
- data/README.md +74 -15
- data/lib/active_model/hints.rb +43 -16
- 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: 99fb1203ae7e46c6f7df8edf84c00a7b55c6901a0dbd43dcfd9eb2b96acbd9dc
|
|
4
|
+
data.tar.gz: c83cbcab470dba35e53f2d991f5b219e4be2ef33b453649df2498f65b1c4f654
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
-
|
|
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,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
|
-
|
|
159
|
-
default:
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -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.1
|
|
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
|