validation_hints 6.0.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 +45 -6
- data/Gemfile.lock +2 -2
- data/README.md +74 -15
- data/lib/active_model/hints.rb +205 -192
- data/lib/validation_hints/locale/en.yml +12 -9
- data/lib/validation_hints/version.rb +1 -1
- data/test/active_model/hints_test.rb +77 -0
- data/test/active_model/i18n_test.rb +65 -0
- data/test/test_helper.rb +28 -0
- data/test/validation_hints_test.rb +2 -3
- metadata +3 -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,7 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
-
## 6.
|
|
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
|
+
|
|
20
|
+
## 6.1.0
|
|
21
|
+
|
|
22
|
+
Rails 7 compatibility for `ActiveModel::Hints` and expanded test coverage (Phase 1).
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `test/active_model/hints_test.rb` — presence, length, numericality, inclusion, virtual attributes, `empty?`, `add`/`added?`.
|
|
27
|
+
- In-memory SQLite ActiveRecord setup in `test/test_helper.rb`.
|
|
28
|
+
- `normalize_message` for mutable hint APIs (`add`, `added?`).
|
|
29
|
+
- `length.within` locale key with `%{minimum}` / `%{maximum}` interpolation.
|
|
30
|
+
- Default hint messages: `invalid`, `blank`, `empty`.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- `ActiveModel::Hints` modernized for Rails 7:
|
|
35
|
+
- plain `Hash` instead of `OrderedHash`
|
|
36
|
+
- fixed `empty?` / `blank?`
|
|
37
|
+
- `generate_message` aligned with Rails 7 I18n chain and interpolation
|
|
38
|
+
- validator keys via `demodulize.underscore.delete_suffix("_validator")`
|
|
39
|
+
- attribute discovery includes virtual attributes from validators
|
|
40
|
+
- length min+max combined into a single `within` hint
|
|
41
|
+
- presence hint suppressed when validator has `allow_blank: true`
|
|
42
|
+
- `locale/en.yml` cleaned up (removed stray keys; format copy → “must match the required format”).
|
|
43
|
+
- Documented behavior: conditional validators (`:if`, `:unless`, `:on`) are not evaluated.
|
|
44
|
+
|
|
45
|
+
## 6.0.0
|
|
6
46
|
|
|
7
47
|
Modernize the gem for **Ruby 3.2+** and **Rails 7.0.x**, targeting use with **inline_forms 7.x**.
|
|
8
48
|
|
|
@@ -14,20 +54,19 @@ Modernize the gem for **Ruby 3.2+** and **Rails 7.0.x**, targeting use with **in
|
|
|
14
54
|
|
|
15
55
|
### Added
|
|
16
56
|
|
|
17
|
-
- `ValidationHints::Railtie` — registers I18n and patches `ActiveModel::Validations` via `ActiveSupport.on_load(:active_model)
|
|
57
|
+
- `ValidationHints::Railtie` — registers I18n and patches `ActiveModel::Validations` via `ActiveSupport.on_load(:active_model)`.
|
|
18
58
|
- `ValidationHints::ValidationsPatch` — extracted module for `has_validations*` and `hints`.
|
|
19
59
|
- `ValidationHints.load_i18n!` — idempotent locale registration.
|
|
20
60
|
- Runtime dependency on `activerecord` 7.0.x.
|
|
21
61
|
- Minitest harness (`test/`) and `rake test` as the default Rake task.
|
|
22
62
|
- `CHANGELOG.md`.
|
|
63
|
+
- `rake release` / gem packaging tasks via `Bundler::GemHelper`.
|
|
23
64
|
|
|
24
65
|
### Changed
|
|
25
66
|
|
|
26
|
-
-
|
|
27
|
-
- Gemspec rewritten for current RubyGems / Bundler (Ruby version, dependencies, file list with non-git fallback).
|
|
67
|
+
- Gemspec rewritten for current RubyGems / Bundler.
|
|
28
68
|
- `Gemfile` source updated to `https://rubygems.org`.
|
|
29
|
-
- Entry point
|
|
30
|
-
- I18n locale registration moved behind `ValidationHints.load_i18n!` (was unconditional append at load time).
|
|
69
|
+
- Entry point refactored: Railtie in Rails, direct `active_model` load outside Rails.
|
|
31
70
|
|
|
32
71
|
### Removed
|
|
33
72
|
|
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
|
@@ -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,213 @@ 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, base: @base)
|
|
157
|
+
|
|
158
|
+
format_defaults = i18n_format_defaults
|
|
159
|
+
format_key = format_defaults.shift
|
|
160
|
+
|
|
161
|
+
I18n.t(
|
|
162
|
+
format_key,
|
|
163
|
+
default: format_defaults,
|
|
164
|
+
attribute: attr_name,
|
|
165
|
+
message: message
|
|
166
|
+
)
|
|
286
167
|
end
|
|
287
168
|
|
|
288
169
|
def generate_message(attribute, type, options = {})
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
170
|
+
type = options.delete(:message) if options[:message].is_a?(Symbol)
|
|
171
|
+
value = (attribute != :base ? @base.read_attribute_for_validation(attribute) : nil)
|
|
172
|
+
|
|
173
|
+
interpolation = {
|
|
174
|
+
model: @base.model_name.human,
|
|
175
|
+
attribute: @base.class.human_attribute_name(attribute, base: @base),
|
|
176
|
+
value: value,
|
|
177
|
+
object: @base,
|
|
178
|
+
count: options[:count],
|
|
179
|
+
minimum: options[:minimum],
|
|
180
|
+
maximum: options[:maximum]
|
|
181
|
+
}.compact
|
|
182
|
+
|
|
183
|
+
defaults = i18n_defaults(attribute, type, options)
|
|
184
|
+
key = defaults.shift
|
|
185
|
+
|
|
186
|
+
I18n.translate(key, **interpolation.merge(default: defaults))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def attribute_names_for_hints
|
|
192
|
+
from_record =
|
|
193
|
+
if @base.respond_to?(:attributes)
|
|
194
|
+
@base.attributes.keys
|
|
195
|
+
else
|
|
196
|
+
[]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
from_validators = @base.class.validators.flat_map(&:attributes).map(&:to_s)
|
|
200
|
+
(from_record + from_validators).map(&:to_sym).uniq
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def messages_for_validator(attribute, validator)
|
|
204
|
+
key = validator_key(validator)
|
|
205
|
+
options = validator.options
|
|
206
|
+
result = []
|
|
207
|
+
|
|
208
|
+
if options[:allow_blank] && key == "presence"
|
|
209
|
+
return result
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if options[:message].is_a?(Symbol)
|
|
213
|
+
message_key = "#{key}.#{options[:message]}"
|
|
214
|
+
result << generate_message(attribute, message_key, options)
|
|
215
|
+
return result
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
message_key = key
|
|
219
|
+
message_key = "numericality.must_be_a_number" if key == "numericality"
|
|
220
|
+
unless VALIDATORS_WITHOUT_MAIN_KEYS.include?(key)
|
|
221
|
+
result << generate_message(attribute, message_key, options)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if key == "length" && options[:minimum] && options[:maximum]
|
|
225
|
+
result << generate_message(
|
|
226
|
+
attribute,
|
|
227
|
+
"length.within",
|
|
228
|
+
minimum: options[:minimum],
|
|
229
|
+
maximum: options[:maximum]
|
|
230
|
+
)
|
|
231
|
+
return result
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
options.each do |option, value|
|
|
235
|
+
next unless MESSAGES_FOR_OPTIONS.include?(option.to_s)
|
|
236
|
+
|
|
237
|
+
if RANGE_OPTIONS.include?(option.to_s) && value.is_a?(Range)
|
|
238
|
+
result.concat(range_hint_messages(attribute, key, value))
|
|
239
|
+
else
|
|
240
|
+
count = inclusion_exclusion_count(key, value)
|
|
241
|
+
result << generate_message(
|
|
242
|
+
attribute,
|
|
243
|
+
"#{key}.#{option}",
|
|
244
|
+
options.merge(count: count)
|
|
245
|
+
)
|
|
294
246
|
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
result
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def range_hint_messages(attribute, validator_key, range)
|
|
253
|
+
minimum = range.min
|
|
254
|
+
maximum = range.max
|
|
255
|
+
maximum -= 1 if range.exclude_end?
|
|
256
|
+
|
|
257
|
+
if validator_key == "length"
|
|
258
|
+
[
|
|
259
|
+
generate_message(attribute, "#{validator_key}.within", minimum: minimum, maximum: maximum),
|
|
260
|
+
]
|
|
295
261
|
else
|
|
296
|
-
|
|
262
|
+
[
|
|
263
|
+
generate_message(attribute, "#{validator_key}.minimum", count: minimum),
|
|
264
|
+
generate_message(attribute, "#{validator_key}.maximum", count: maximum)
|
|
265
|
+
]
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def inclusion_exclusion_count(validator_key, value)
|
|
270
|
+
return value.to_sentence if %w[inclusion exclusion].include?(validator_key) && value.respond_to?(:to_sentence)
|
|
271
|
+
|
|
272
|
+
value
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def validator_key(validator)
|
|
276
|
+
validator.class.name.demodulize.underscore.delete_suffix("_validator")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def i18n_defaults(attribute, type, options)
|
|
280
|
+
attribute_name = attribute.to_s.remove(/\[\d+\]/)
|
|
281
|
+
|
|
282
|
+
defaults = model_hint_defaults(attribute_name, type)
|
|
283
|
+
defaults << options[:message] if options[:message]
|
|
284
|
+
|
|
285
|
+
if @base.class.respond_to?(:i18n_scope)
|
|
286
|
+
scope = @base.class.i18n_scope
|
|
287
|
+
defaults << :"#{scope}.hints.messages.#{type}"
|
|
297
288
|
end
|
|
298
289
|
|
|
299
|
-
defaults <<
|
|
300
|
-
defaults << :"
|
|
301
|
-
defaults << :"hints.attributes.#{attribute}.#{type}"
|
|
290
|
+
defaults << :"activemodel.hints.messages.#{type}"
|
|
291
|
+
defaults << :"hints.attributes.#{attribute_name}.#{type}"
|
|
302
292
|
defaults << :"hints.messages.#{type}"
|
|
293
|
+
defaults.compact.flatten
|
|
294
|
+
end
|
|
303
295
|
|
|
304
|
-
|
|
305
|
-
|
|
296
|
+
def model_hint_defaults(attribute_name, type)
|
|
297
|
+
return [] unless @base.class.respond_to?(:i18n_scope)
|
|
306
298
|
|
|
307
|
-
|
|
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 = []
|
|
308
310
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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}"
|
|
320
322
|
end
|
|
321
323
|
|
|
324
|
+
def normalize_message(attribute, message, options = {})
|
|
325
|
+
case message
|
|
326
|
+
when Symbol
|
|
327
|
+
generate_message(attribute, message, options)
|
|
328
|
+
when Proc
|
|
329
|
+
message.call
|
|
330
|
+
when nil
|
|
331
|
+
generate_message(attribute, :invalid, options)
|
|
332
|
+
else
|
|
333
|
+
message
|
|
334
|
+
end
|
|
335
|
+
end
|
|
322
336
|
end
|
|
323
|
-
|
|
324
337
|
end
|
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
## YAML Template.
|
|
2
1
|
---
|
|
3
2
|
en:
|
|
3
|
+
activemodel:
|
|
4
|
+
hints:
|
|
5
|
+
format: "%{attribute} %{message}"
|
|
6
|
+
|
|
4
7
|
activerecord:
|
|
5
|
-
|
|
8
|
+
hints:
|
|
9
|
+
format: "%{attribute} %{message}"
|
|
10
|
+
|
|
6
11
|
hints:
|
|
7
|
-
# The default format to use in full error messages.
|
|
8
12
|
format: "%{attribute} %{message}"
|
|
9
13
|
|
|
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
14
|
messages:
|
|
13
|
-
|
|
15
|
+
invalid: "is invalid"
|
|
16
|
+
blank: "can't be blank"
|
|
17
|
+
empty: "can't be empty"
|
|
14
18
|
inclusion:
|
|
15
19
|
in: "must be one of %{count}"
|
|
16
20
|
exclusion:
|
|
17
21
|
in: "must not be one of %{count}"
|
|
18
|
-
format: "
|
|
22
|
+
format: "must match the required format"
|
|
19
23
|
associated: "is invalid"
|
|
20
24
|
uniqueness: "must be unique"
|
|
21
25
|
confirmation: "doesn't match confirmation"
|
|
@@ -25,7 +29,7 @@ en:
|
|
|
25
29
|
maximum: "must not be longer than %{count} characters"
|
|
26
30
|
minimum: "must not be shorter than %{count} characters"
|
|
27
31
|
is: "must be exactly %{count} characters"
|
|
28
|
-
|
|
32
|
+
within: "must be between %{minimum} and %{maximum} characters"
|
|
29
33
|
numericality:
|
|
30
34
|
must_be_a_number: "must be a number"
|
|
31
35
|
only_integer: "must be an integer"
|
|
@@ -36,4 +40,3 @@ en:
|
|
|
36
40
|
less_than_or_equal_to: "must be less than or equal to %{count}"
|
|
37
41
|
odd: "must be odd"
|
|
38
42
|
even: "must be even"
|
|
39
|
-
|
|
@@ -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
|
|
@@ -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
|
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.
|
|
7
|
+
assert_equal "6.2.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.
|
|
4
|
+
version: 6.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ace Suares
|
|
@@ -113,6 +113,8 @@ 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
|
|
117
|
+
- test/active_model/i18n_test.rb
|
|
116
118
|
- test/test_helper.rb
|
|
117
119
|
- test/validation_hints_test.rb
|
|
118
120
|
- validation_hints.gemspec
|