hanami-validations 1.3.7 → 2.0.0.alpha1
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 -9
- data/README.md +260 -568
- data/hanami-validations.gemspec +6 -9
- data/lib/hanami/validations.rb +35 -311
- data/lib/hanami/validations/form.rb +26 -20
- data/lib/hanami/validations/version.rb +1 -2
- data/lib/hanami/validator.rb +9 -0
- metadata +13 -70
- data/lib/hanami-validations.rb +0 -3
- data/lib/hanami/validations/inline_predicate.rb +0 -48
- data/lib/hanami/validations/namespace.rb +0 -67
- data/lib/hanami/validations/predicates.rb +0 -47
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c94b88f4b0a39d33e6ce6d5a27e9c843afc611b3a5d76ac48799c05648c73ed1
|
|
4
|
+
data.tar.gz: 93a768f5a5c977ada49f6f400bade010ebe87be3072ab7b1fdec6e36afe0f00a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01bae680528b53b9564393aaa923f6e66f91d9ccda370e6297c6c458b0fbc127c83a39a1ba61d110bb7c698e28cb95e8ce8bb0f7f174bc151a384bb7b3bdb3d5
|
|
7
|
+
data.tar.gz: 15c2f866bae8f77acca611e4a70aefc6da227fc33aa556e14beff08e2725880e094a6de7fa9a5cbcff22fa8fce80f282f4623627cb206119821ebdab41e5fc6a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
# Hanami::Validations
|
|
2
2
|
Validations mixin for Ruby objects
|
|
3
3
|
|
|
4
|
-
##
|
|
5
|
-
### Fixed
|
|
6
|
-
- [Panagiotis Matsinopoulos] Ensure `predicate` and `predicates` to work together
|
|
7
|
-
|
|
8
|
-
## v1.3.6 - 2020-01-08
|
|
4
|
+
## v2.0.0.alpha1 - 2019-07-26
|
|
9
5
|
### Added
|
|
10
|
-
- [Luca Guidi]
|
|
6
|
+
- [Luca Guidi] Introduced `Hanami::Validator`
|
|
7
|
+
- [Luca Guidi] Added support to validate JSON data
|
|
8
|
+
- [Luca Guidi] Added rules
|
|
9
|
+
- [Luca Guidi] Allow to inherit validations from superclasses (e.g. `ApplicationValidator < Hanami::Validator` => `SignupValidator < ApplicationValidator`)
|
|
10
|
+
- [Luca Guidi] Allow to error messages to receive arbitrary information as a `Hash` (e.g. `error_code: 123`), which will be interpolated in the final error message.
|
|
11
|
+
- [Luca Guidi] Added support for validator external dependencies
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- [
|
|
13
|
+
### Changed
|
|
14
|
+
- [Luca Guidi] Drop support for Ruby: MRI 2.3, JRuby 9.1.
|
|
15
|
+
- [Luca Guidi] New validation syntax
|
|
16
|
+
- [Luca Guidi] Removed custom predicates (`Hanami::Validations.predicate`)
|
|
17
|
+
- [Luca Guidi] Removed global custom predicates (`Hanami::Validations::Predicates`)
|
|
18
|
+
- [Luca Guidi] Removed messages path setting (`Hanami::Validations.messages_path=`)
|
|
19
|
+
- [Luca Guidi] Removed messages namespace setting (`Hanami::Validations.namespace`)
|
|
20
|
+
- [Luca Guidi] Removed messages engine setting (`Hanami::Validations.messages`)
|
|
15
21
|
|
|
16
22
|
## v1.3.4 - 2019-07-26
|
|
17
23
|
### Fixed
|
data/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Hanami::Validations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Data validation library for Ruby
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
7
|
[](https://badge.fury.io/rb/hanami-validations)
|
|
8
|
-
[](https://travis-ci.org/hanami/validations)
|
|
9
|
+
[](https://circleci.com/gh/hanami/validations/tree/master)
|
|
10
|
+
[](https://ci.hanamirb.org/hanami/validations)
|
|
9
11
|
[](https://codecov.io/gh/hanami/validations)
|
|
10
12
|
[](https://depfu.com/github/hanami/validations?project=Bundler)
|
|
11
13
|
[](http://inch-ci.org/github/hanami/validations)
|
|
@@ -23,14 +25,14 @@ Validations mixin for Ruby objects
|
|
|
23
25
|
|
|
24
26
|
## Rubies
|
|
25
27
|
|
|
26
|
-
__Hanami::Validations__ supports Ruby (MRI) 2.
|
|
28
|
+
__Hanami::Validations__ supports Ruby (MRI) 2.4+ and JRuby 9.2+
|
|
27
29
|
|
|
28
30
|
## Installation
|
|
29
31
|
|
|
30
32
|
Add this line to your application's Gemfile:
|
|
31
33
|
|
|
32
34
|
```ruby
|
|
33
|
-
gem
|
|
35
|
+
gem "hanami-validations"
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
And then execute:
|
|
@@ -47,733 +49,426 @@ $ gem install hanami-validations
|
|
|
47
49
|
|
|
48
50
|
## Usage
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
[Hanami](http://hanamirb.org), [ROM](https://rom-rb.org), and [DRY](https://dry-rb.org) projects are working together to create a strong Ruby ecosystem.
|
|
53
|
+
`hanami-validations` is based on [`dry-validation`](https://dry-rb.org/gems/dry-validation), for this reason the documentation explains the basics of this gem, but for advanced topics, it links to `dry-validation` docs.
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
### Overview
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
The main object provided by this gem is `Hanami::Validator`.
|
|
58
|
+
It providers a powerful DSL to define a validation contract, which is made of a schema and optional rules.
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
include Hanami::Validations
|
|
59
|
-
|
|
60
|
-
validations do
|
|
61
|
-
required(:name) { filled? & str? & size?(3..64) }
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
result = Signup.new(name: "Luca").validate
|
|
66
|
-
result.success? # => true
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
There is more that `Hanami::Validations` can do: **type safety**, **composition**, **complex data structures**, **built-in and custom predicates**.
|
|
70
|
-
|
|
71
|
-
But before to dive into advanced topics, we need to understand the basics of _boolean logic_.
|
|
72
|
-
|
|
73
|
-
### Boolean Logic
|
|
74
|
-
|
|
75
|
-
When we check data, we expect only two outcomes: an input can be valid or not. No grey areas, nor fuzzy results. It’s white or black, 1 or 0, `true` or `false` and _boolean logic_ is the perfect tool to express these two states. Indeed, a Ruby _boolean expression_ can only return `true` or `false`.
|
|
76
|
-
|
|
77
|
-
To better recognise the pattern, let’s get back to the example above. This time we will map the natural language rules with programming language rules.
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
A name must be filled and be a string and its size must be included between 3 and 64.
|
|
81
|
-
👇 👇 👇 👇 👇 👇 👇 👇
|
|
82
|
-
required(:name) { filled? & str? & size? (3 .. 64) }
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Now, I hope you’ll never format code like that, but in this case, that formatting serves well our purpose to show how Ruby’s simplicity helps to define complex rules with no effort.
|
|
87
|
-
|
|
88
|
-
From a high level perspective, we can tell that input data for `name` is _valid_ only if **all** the requirements are satisfied. That’s because we used `&`.
|
|
89
|
-
|
|
90
|
-
#### Logic Operators
|
|
91
|
-
|
|
92
|
-
We support four logic operators:
|
|
93
|
-
|
|
94
|
-
* `&` (aliased as `and`) for _conjunction_
|
|
95
|
-
* `|` (aliased as `or`) for _disjunction_
|
|
96
|
-
* `>` (aliased as `then`) for _implication_
|
|
97
|
-
* `^` (aliased as `xor`) for _exclusive disjunction_
|
|
98
|
-
|
|
99
|
-
#### Context Of Execution
|
|
100
|
-
|
|
101
|
-
**Please notice that we used `&` over Ruby's `&&` keyword.**
|
|
102
|
-
That's because the context of execution of these validations isn't a plain lambda, but something richer.
|
|
103
|
-
|
|
104
|
-
For real world projects, we want to support common scenarios without the need of reinventing the wheel ourselves. Scenarios like _password confirmation_, _size check_ are already prepackaged with `Hanami::Validations`.
|
|
60
|
+
A validation **schema** is a set of steps that filters, coerces, and checks the validity of incoming data.
|
|
61
|
+
Validation **rules** are a set of directives, to check if business rules are respected.
|
|
105
62
|
|
|
63
|
+
Only when the input is formally valid (according to the **schema**), validation **rules** are checked.
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
### Predicates
|
|
65
|
+
```ruby
|
|
66
|
+
# frozen_string_literal: true
|
|
110
67
|
|
|
111
|
-
|
|
68
|
+
require "hanami/validations"
|
|
112
69
|
|
|
113
|
-
|
|
70
|
+
class SignupValidator < Hanami::Validator
|
|
71
|
+
schema do
|
|
72
|
+
required(:email).value(:string)
|
|
73
|
+
required(:age).value(:integer)
|
|
74
|
+
end
|
|
114
75
|
|
|
115
|
-
|
|
76
|
+
rule(:age) do
|
|
77
|
+
key.failure("must be greater than 18") if value < 18
|
|
78
|
+
end
|
|
79
|
+
end
|
|
116
80
|
|
|
117
|
-
|
|
81
|
+
validator = SignupValidator.new
|
|
118
82
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```
|
|
83
|
+
result = validator.call(email: "user@hanamirb.test", age: 37)
|
|
84
|
+
result.success? # => true
|
|
122
85
|
|
|
123
|
-
|
|
86
|
+
result = validator.call(email: "user@hanamirb.test", age: "foo")
|
|
87
|
+
result.success? # => false
|
|
88
|
+
result.errors.to_h # => {:age=>["must be an integer"]}
|
|
124
89
|
|
|
125
|
-
|
|
126
|
-
|
|
90
|
+
result = validator.call(email: "user@hanamirb.test", age: 17)
|
|
91
|
+
puts result.success? # => false
|
|
92
|
+
puts result.errors.to_h # => {:age=>["must be greater than 18"]}
|
|
127
93
|
```
|
|
128
94
|
|
|
129
|
-
|
|
95
|
+
### Schemas
|
|
130
96
|
|
|
131
|
-
|
|
97
|
+
A basic schema doesn't apply data coercion, input must already have the right Ruby types.
|
|
132
98
|
|
|
133
99
|
```ruby
|
|
134
|
-
|
|
135
|
-
```
|
|
100
|
+
# frozen_string_literal: true
|
|
136
101
|
|
|
137
|
-
|
|
102
|
+
require "hanami/validations"
|
|
138
103
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
required(:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Ruby types are respected: `23` (an integer) is only equal to `23`, and not to `"23"` (a string). See _Type Safety_ section.
|
|
104
|
+
class SignupValidator < Hanami::Validator
|
|
105
|
+
schema do
|
|
106
|
+
required(:email).value(:string)
|
|
107
|
+
required(:age).value(:integer)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
146
110
|
|
|
147
|
-
|
|
111
|
+
validator = SignupValidator.new
|
|
148
112
|
|
|
149
|
-
|
|
113
|
+
result = validator.call(email: "user@hanamirb.test", age: 37)
|
|
114
|
+
puts result.success? # => true
|
|
150
115
|
|
|
151
|
-
|
|
152
|
-
|
|
116
|
+
result = validator.call(email: "user@hanamirb.test", age: "37")
|
|
117
|
+
puts result.success? # => false
|
|
118
|
+
puts result.errors.to_h # => {:age=>["must be an integer"]}
|
|
153
119
|
```
|
|
154
120
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
This is a predicate that works with a regular expression to match it against data input.
|
|
158
|
-
|
|
159
|
-
```ruby
|
|
160
|
-
require 'uri'
|
|
161
|
-
HTTP_FORMAT = URI.regexp(%w(http https))
|
|
162
|
-
|
|
163
|
-
required(:url) { format?(HTTP_FORMAT) }
|
|
164
|
-
```
|
|
121
|
+
### Params
|
|
165
122
|
|
|
166
|
-
|
|
123
|
+
When used in _params mode_, a schema applies data coercion, before to run validation checks.
|
|
167
124
|
|
|
168
|
-
This
|
|
125
|
+
This is designed for Web form/HTTP params.
|
|
169
126
|
|
|
170
127
|
```ruby
|
|
171
|
-
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
#### Greater Than Equal
|
|
128
|
+
# frozen_string_literal: true
|
|
175
129
|
|
|
176
|
-
|
|
130
|
+
require "bundler/setup"
|
|
131
|
+
require "hanami/validations"
|
|
177
132
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
133
|
+
class SignupValidator < Hanami::Validator
|
|
134
|
+
params do
|
|
135
|
+
required(:email).value(:string)
|
|
136
|
+
required(:age).value(:integer)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
183
139
|
|
|
184
|
-
|
|
140
|
+
validator = SignupValidator.new
|
|
185
141
|
|
|
186
|
-
|
|
187
|
-
|
|
142
|
+
result = validator.call(email: "user@hanamirb.test", age: "37")
|
|
143
|
+
puts result.success? # => true
|
|
144
|
+
puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37}
|
|
188
145
|
```
|
|
189
146
|
|
|
190
|
-
|
|
147
|
+
### JSON
|
|
191
148
|
|
|
192
|
-
|
|
149
|
+
When used in _JSON mode_, data coercions are still applied, but they follow different policies.
|
|
150
|
+
For instance, because JSON supports integers, strings won't be coerced into integers.
|
|
193
151
|
|
|
194
152
|
```ruby
|
|
195
|
-
|
|
196
|
-
```
|
|
153
|
+
# frozen_string_literal: true
|
|
197
154
|
|
|
198
|
-
|
|
155
|
+
require "hanami/validations"
|
|
199
156
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
required(:age)
|
|
204
|
-
|
|
157
|
+
class SignupValidator < Hanami::Validator
|
|
158
|
+
json do
|
|
159
|
+
required(:email).value(:string)
|
|
160
|
+
required(:age).value(:integer)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
205
163
|
|
|
206
|
-
|
|
164
|
+
validator = SignupValidator.new
|
|
207
165
|
|
|
208
|
-
|
|
166
|
+
result = validator.call(email: "user@hanamirb.test", age: 37)
|
|
167
|
+
puts result.success? # => true
|
|
168
|
+
puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37}
|
|
209
169
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
required(:languages) { filled? } # collection
|
|
170
|
+
result = validator.call(email: "user@hanamirb.test", age: "37")
|
|
171
|
+
puts result.success? # => false
|
|
213
172
|
```
|
|
214
173
|
|
|
215
|
-
|
|
174
|
+
### Whitelisting
|
|
216
175
|
|
|
217
|
-
|
|
176
|
+
Unknown keys from incoming data are filtered out:
|
|
218
177
|
|
|
219
178
|
```ruby
|
|
220
|
-
|
|
221
|
-
```
|
|
179
|
+
# frozen_string_literal: true
|
|
222
180
|
|
|
223
|
-
|
|
181
|
+
require "hanami/validations"
|
|
224
182
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
#### None
|
|
183
|
+
class SignupValidator < Hanami::Validator
|
|
184
|
+
schema do
|
|
185
|
+
required(:email).value(:string)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
232
188
|
|
|
233
|
-
|
|
189
|
+
validator = SignupValidator.new
|
|
234
190
|
|
|
235
|
-
|
|
236
|
-
|
|
191
|
+
result = validator.call(email: "user@hanamirb.test", foo: "bar")
|
|
192
|
+
puts result.success? # => true
|
|
193
|
+
puts result.to_h # => {:email=>"user@hanamirb.test"}
|
|
237
194
|
```
|
|
238
195
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
It checks if the size of input data is: a) exactly the same of a given quantity or b) it falls into a range.
|
|
196
|
+
### Custom Types
|
|
242
197
|
|
|
243
198
|
```ruby
|
|
244
|
-
|
|
245
|
-
required(:password) { size?(8..32) } # range
|
|
246
|
-
```
|
|
199
|
+
# frozen_string_literal: true
|
|
247
200
|
|
|
248
|
-
|
|
201
|
+
require "hanami/validations"
|
|
249
202
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
```
|
|
203
|
+
module Types
|
|
204
|
+
include Dry::Types()
|
|
253
205
|
|
|
254
|
-
|
|
206
|
+
StrippedString = Types::String.constructor(&:strip)
|
|
207
|
+
end
|
|
255
208
|
|
|
256
|
-
|
|
209
|
+
class SignupValidator < Hanami::Validator
|
|
210
|
+
params do
|
|
211
|
+
required(:email).value(Types::StrippedString)
|
|
212
|
+
required(:age).value(:integer)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
257
215
|
|
|
258
|
-
|
|
259
|
-
MEGABYTE = 1024 ** 2
|
|
216
|
+
validator = SignupValidator.new
|
|
260
217
|
|
|
261
|
-
|
|
218
|
+
result = validator.call(email: " user@hanamirb.test ", age: "37")
|
|
219
|
+
puts result.success? # => true
|
|
220
|
+
puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37}
|
|
262
221
|
```
|
|
263
222
|
|
|
264
|
-
###
|
|
265
|
-
|
|
266
|
-
We have seen that built-in predicates as an expressive tool to get our job done with common use cases.
|
|
267
|
-
|
|
268
|
-
But what if our case is not common? We can define our own custom predicates.
|
|
269
|
-
|
|
270
|
-
#### Inline Custom Predicates
|
|
223
|
+
### Rules
|
|
271
224
|
|
|
272
|
-
|
|
225
|
+
Rules are performing a set of domain-specific validation checks.
|
|
226
|
+
Rules are executed only after the validations from the schema are satisfied.
|
|
273
227
|
|
|
274
228
|
```ruby
|
|
275
|
-
|
|
229
|
+
# frozen_string_literal: true
|
|
276
230
|
|
|
277
|
-
|
|
278
|
-
include Hanami::Validations
|
|
231
|
+
require "hanami/validations"
|
|
279
232
|
|
|
280
|
-
|
|
281
|
-
|
|
233
|
+
class EventValidator < Hanami::Validator
|
|
234
|
+
params do
|
|
235
|
+
required(:start_date).value(:date)
|
|
282
236
|
end
|
|
283
237
|
|
|
284
|
-
|
|
285
|
-
|
|
238
|
+
rule(:start_date) do
|
|
239
|
+
key.failure("must be in the future") if value <= Date.today
|
|
286
240
|
end
|
|
287
241
|
end
|
|
288
|
-
```
|
|
289
242
|
|
|
290
|
-
|
|
243
|
+
validator = EventValidator.new
|
|
291
244
|
|
|
292
|
-
|
|
245
|
+
result = validator.call(start_date: "foo")
|
|
246
|
+
puts result.success? # => false
|
|
247
|
+
puts result.errors.to_h # => {:start_date=>["must be a date"]}
|
|
293
248
|
|
|
294
|
-
|
|
295
|
-
|
|
249
|
+
result = validator.call(start_date: Date.today)
|
|
250
|
+
puts result.success? # => false
|
|
251
|
+
puts result.errors.to_h # => {:start_date=>["must be in the future"]}
|
|
296
252
|
|
|
297
|
-
|
|
298
|
-
|
|
253
|
+
result = validator.call(start_date: Date.today + 1)
|
|
254
|
+
puts result.success? # => true
|
|
255
|
+
puts result.to_h # => {:start_date=>#<Date: 2019-07-03 ((2458668j,0s,0n),+0s,2299161j)>}
|
|
256
|
+
```
|
|
299
257
|
|
|
300
|
-
|
|
258
|
+
Learn more about rules: https://dry-rb.org/gems/dry-validation/rules/
|
|
301
259
|
|
|
302
|
-
|
|
303
|
-
current.match(/.../)
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
```
|
|
260
|
+
### Inheritance
|
|
307
261
|
|
|
308
|
-
|
|
262
|
+
Schema and rules validations can be inherited and used by subclasses
|
|
309
263
|
|
|
310
264
|
```ruby
|
|
311
|
-
|
|
312
|
-
require_relative 'my_predicates'
|
|
265
|
+
# frozen_string_literal: true
|
|
313
266
|
|
|
314
|
-
|
|
315
|
-
include Hanami::Validations
|
|
316
|
-
predicates MyPredicates
|
|
267
|
+
require "hanami/validations"
|
|
317
268
|
|
|
318
|
-
|
|
319
|
-
|
|
269
|
+
class ApplicationValidator < Hanami::Validator
|
|
270
|
+
params do
|
|
271
|
+
optional(:_csrf_token).filled(:string)
|
|
320
272
|
end
|
|
321
273
|
end
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### Required and Optional keys
|
|
325
|
-
|
|
326
|
-
HTML forms can have required or optional fields. We can express this concept with two methods in our validations: `required` (which we already met in previous examples), and `optional`.
|
|
327
|
-
|
|
328
|
-
```ruby
|
|
329
|
-
require 'hanami/validations'
|
|
330
|
-
|
|
331
|
-
class Signup
|
|
332
|
-
include Hanami::Validations
|
|
333
274
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
275
|
+
class SignupValidator < ApplicationValidator
|
|
276
|
+
params do
|
|
277
|
+
required(:user).hash do
|
|
278
|
+
required(:email).filled(:string)
|
|
279
|
+
end
|
|
337
280
|
end
|
|
338
281
|
end
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
### Type Safety
|
|
342
|
-
|
|
343
|
-
At this point, we need to explicitly tell something really important about built-in predicates. Each of them have expectations about the methods that an input is able to respond to.
|
|
344
282
|
|
|
345
|
-
|
|
283
|
+
validator = SignupValidator.new
|
|
346
284
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
required(:age) { type?(Integer) & gteq?(18) }
|
|
285
|
+
result = validator.call(user: { email: "user@hanamirb.test" }, _csrf_token: "abc123")
|
|
286
|
+
puts result.success? # => true
|
|
287
|
+
puts result.to_h # => {:user=>{:email=>"user@hanamirb.test"}, :_csrf_token=>"abc123"}
|
|
351
288
|
```
|
|
352
289
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
**We suggest to use `#type?` at the beginning of the validations block. This _type safety_ policy is crucial to prevent runtime errors.**
|
|
356
|
-
|
|
357
|
-
`Hanami::Validations` supports the most common Ruby types:
|
|
290
|
+
### Messages
|
|
358
291
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
* `Boolean` (aliased as `bool?`)
|
|
362
|
-
* `Date` (aliased as `date?`)
|
|
363
|
-
* `DateTime` (aliased as `date_time?`)
|
|
364
|
-
* `Float` (aliased as `float?`)
|
|
365
|
-
* `Hash` (aliased as `hash?`)
|
|
366
|
-
* `Integer` (aliased as `int?`)
|
|
367
|
-
* `String` (aliased as `str?`)
|
|
368
|
-
* `Time` (aliased as `time?`)
|
|
292
|
+
Failure messages can be hardcoded or refer to a message template system.
|
|
293
|
+
`hanami-validations` supports natively a default YAML based message template system, or alternatively, `i18n` gem.
|
|
369
294
|
|
|
370
|
-
|
|
295
|
+
We have already seen rule failures set with hardcoded messages, here's an example of how to use keys to refer to interpolated messages.
|
|
371
296
|
|
|
372
297
|
```ruby
|
|
373
|
-
|
|
374
|
-
required(:age) { int? }
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
### Macros
|
|
378
|
-
|
|
379
|
-
Rule composition with blocks is powerful, but it can become verbose.
|
|
380
|
-
To reduce verbosity, `Hanami::Validations` offers convenient _macros_ that are internally _expanded_ (aka interpreted) to an equivalent _block expression_
|
|
381
|
-
|
|
382
|
-
#### Filled
|
|
383
|
-
|
|
384
|
-
To use when we expect a value to be filled:
|
|
385
|
-
|
|
386
|
-
```ruby
|
|
387
|
-
# expands to
|
|
388
|
-
# required(:age) { filled? }
|
|
389
|
-
|
|
390
|
-
required(:age).filled
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
```ruby
|
|
394
|
-
# expands to
|
|
395
|
-
# required(:age) { filled? & type?(Integer) }
|
|
396
|
-
|
|
397
|
-
required(:age).filled(:int?)
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
```ruby
|
|
401
|
-
# expands to
|
|
402
|
-
# required(:age) { filled? & type?(Integer) & gt?(18) }
|
|
403
|
-
|
|
404
|
-
required(:age).filled(:int?, gt?: 18)
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
In the examples above `age` is **always required** as value.
|
|
408
|
-
|
|
409
|
-
#### Maybe
|
|
410
|
-
|
|
411
|
-
To use when a value can be nil:
|
|
412
|
-
|
|
413
|
-
```ruby
|
|
414
|
-
# expands to
|
|
415
|
-
# required(:age) { none? | int? }
|
|
416
|
-
|
|
417
|
-
required(:age).maybe(:int?)
|
|
418
|
-
```
|
|
298
|
+
# frozen_string_literal: true
|
|
419
299
|
|
|
420
|
-
|
|
300
|
+
require "hanami/validations"
|
|
421
301
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
```ruby
|
|
427
|
-
# expands to
|
|
428
|
-
# required(:tags) { array? { each { str? } } }
|
|
429
|
-
|
|
430
|
-
required(:tags).each(:str?)
|
|
302
|
+
class ApplicationValidator < Hanami::Validator
|
|
303
|
+
config.messages.top_namespace = "bookshelf"
|
|
304
|
+
config.messages.load_paths << "config/errors.yml"
|
|
305
|
+
end
|
|
431
306
|
```
|
|
432
307
|
|
|
433
|
-
In the
|
|
308
|
+
In the `ApplicationValidator` there is defined the application namespace (`"bookshelf"`), which is the root of the messages file.
|
|
309
|
+
Below that top name, there is the key `errors`. Everything that is nested here is accessible by the validations.
|
|
434
310
|
|
|
311
|
+
There are two ways to organize messages:
|
|
435
312
|
|
|
436
|
-
|
|
313
|
+
1. Right below `errors`. This is for **general purposes** error messages (e.g. `bookshelf` => `errors` => `taken`)
|
|
314
|
+
2. Below `errors` => `rules` => name of the attribute => custom key (e.g. `bookshelf` => `errors` => `age` => `invalid`). This is for **specific** messages that affect only a specific attribute.
|
|
437
315
|
|
|
438
|
-
|
|
316
|
+
Our **suggestion** is to start with **specific** messages and see if there is a need to generalize them.
|
|
439
317
|
|
|
440
|
-
```
|
|
441
|
-
|
|
318
|
+
```yaml
|
|
319
|
+
# config/errors.yml
|
|
320
|
+
en:
|
|
321
|
+
bookshelf:
|
|
322
|
+
errors:
|
|
323
|
+
taken: "oh noes, it's already taken"
|
|
324
|
+
network: "there is a network error (%{code})"
|
|
325
|
+
rules:
|
|
326
|
+
age:
|
|
327
|
+
invalid: "must be greater than 18"
|
|
328
|
+
email:
|
|
329
|
+
invalid: "not a valid email"
|
|
442
330
|
```
|
|
443
331
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
⚠ **CONVENTION:** For a given key `password`, the _confirmation_ predicate expects another key `password_confirmation`. Easy to tell, it’s the concatenation of the original key with the `_confirmation` suffix. Their values must be equal. ⚠
|
|
447
|
-
|
|
448
|
-
### Forms
|
|
449
|
-
|
|
450
|
-
An important precondition to check before to implement a validator is about the expected input.
|
|
451
|
-
When we use validators for already preprocessed data it's safe to use basic validations from `Hanami::Validations` mixin.
|
|
452
|
-
|
|
453
|
-
If the data is coming directly from user input via a HTTP form, it's advisable to use `Hanami::Validations::Form` instead.
|
|
454
|
-
**The two mixins have the same API, but the latter is able to do low level input preprocessing specific for forms**. For instance, blank inputs are casted to `nil` in order to avoid blank strings in the database.
|
|
455
|
-
|
|
456
|
-
### Rules
|
|
457
|
-
|
|
458
|
-
Predicates and macros are tools to code validations that concern a single key like `first_name` or `email`.
|
|
459
|
-
If the outcome of a validation depends on two or more attributes we can use _rules_.
|
|
460
|
-
|
|
461
|
-
Here's a practical example: a job board.
|
|
462
|
-
We want to validate the form of the job creation with some mandatory fields: `type` (full time, part-time, contract), `title` (eg. Developer), `description`, `company` (just the name) and a `website` (which is optional).
|
|
463
|
-
An user must specify the location: on-site or remote. If it's on site, they must specify the `location`, otherwise they have to tick the checkbox for `remote`.
|
|
464
|
-
|
|
465
|
-
Here's the code:
|
|
332
|
+
#### General purpose messages
|
|
466
333
|
|
|
467
334
|
```ruby
|
|
468
|
-
class
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
validations do
|
|
472
|
-
required(:type).filled(:int?, included_in?: [1, 2, 3])
|
|
473
|
-
|
|
474
|
-
optional(:location).maybe(:str?)
|
|
475
|
-
optional(:remote).maybe(:bool?)
|
|
476
|
-
|
|
477
|
-
required(:title).filled(:str?)
|
|
478
|
-
required(:description).filled(:str?)
|
|
479
|
-
required(:company).filled(:str?)
|
|
480
|
-
|
|
481
|
-
optional(:website).filled(:str?, format?: URI.regexp(%w(http https)))
|
|
482
|
-
|
|
483
|
-
rule(location_presence: [:location, :remote]) do |location, remote|
|
|
484
|
-
(remote.none? | remote.false?).then(location.filled?) &
|
|
485
|
-
remote.true?.then(location.none?)
|
|
486
|
-
end
|
|
335
|
+
class SignupValidator < ApplicationValidator
|
|
336
|
+
schema do
|
|
337
|
+
required(:username).filled(:string)
|
|
487
338
|
end
|
|
488
|
-
end
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
We specify a rule with `rule` method, which takes an arbitrary name and an array of preconditions.
|
|
492
|
-
Only if `:location` and `:remote` are valid according to their validations described above, the `rule` block is evaluated.
|
|
493
|
-
|
|
494
|
-
The block yields the same exact keys that we put in the precondintions.
|
|
495
|
-
So for `[:location, :remote]` it will yield the corresponding values, bound to the `location` and `remote` variables.
|
|
496
|
-
|
|
497
|
-
We can use these variables to define the rule. We covered a few cases:
|
|
498
339
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
### Nested Input Data
|
|
503
|
-
|
|
504
|
-
While we’re building complex web forms, we may find comfortable to organise data in a hierarchy of cohesive input fields. For instance, all the fields related to a customer, may have the `customer` prefix. To reflect this arrangement on the server side, we can group keys.
|
|
505
|
-
|
|
506
|
-
```ruby
|
|
507
|
-
validations do
|
|
508
|
-
required(:customer).schema do
|
|
509
|
-
required(:email) { … }
|
|
510
|
-
required(:name) { … }
|
|
511
|
-
# other validations …
|
|
340
|
+
rule(:username) do
|
|
341
|
+
key.failure(:taken) if values[:username] == "jodosha"
|
|
512
342
|
end
|
|
513
343
|
end
|
|
514
|
-
```
|
|
515
344
|
|
|
516
|
-
|
|
345
|
+
validator = SignupValidator.new
|
|
517
346
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
required(:customer).schema do
|
|
521
|
-
# other validations …
|
|
347
|
+
result = validator.call(username: "foo")
|
|
348
|
+
puts result.success? # => true
|
|
522
349
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
end
|
|
527
|
-
end
|
|
528
|
-
end
|
|
350
|
+
result = validator.call(username: "jodosha")
|
|
351
|
+
puts result.success? # => false
|
|
352
|
+
puts result.errors.to_h # => {:username=>["oh noes, it's already taken"]}
|
|
529
353
|
```
|
|
530
354
|
|
|
531
|
-
|
|
355
|
+
#### Specific messages
|
|
532
356
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
As the code base grows, it’s a good practice to DRY validation rules.
|
|
357
|
+
Please note that the failure key used it's the same for both the attributes (`:invalid`), but thanks to the nesting, the library is able to lookup the right message.
|
|
536
358
|
|
|
537
359
|
```ruby
|
|
538
|
-
class
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
required(:street) { … }
|
|
360
|
+
class SignupValidator < ApplicationValidator
|
|
361
|
+
schema do
|
|
362
|
+
required(:email).filled(:string)
|
|
363
|
+
required(:age).filled(:integer)
|
|
543
364
|
end
|
|
544
|
-
end
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
This validator can be reused by other validators.
|
|
548
365
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
include Hanami::Validations
|
|
552
|
-
|
|
553
|
-
validations do
|
|
554
|
-
required(:email) { … }
|
|
555
|
-
required(:address).schema(AddressValidator)
|
|
366
|
+
rule(:email) do
|
|
367
|
+
key.failure(:invalid) unless values[:email] =~ /@/
|
|
556
368
|
end
|
|
557
|
-
end
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
Again, there is no limit to the nesting levels.
|
|
561
369
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
include Hanami::Validations
|
|
565
|
-
|
|
566
|
-
validations do
|
|
567
|
-
required(:number) { … }
|
|
568
|
-
required(:customer).schema(CustomerValidator)
|
|
370
|
+
rule(:age) do
|
|
371
|
+
key.failure(:invalid) if values[:age] < 18
|
|
569
372
|
end
|
|
570
373
|
end
|
|
571
|
-
```
|
|
572
374
|
|
|
573
|
-
|
|
375
|
+
validator = SignupValidator.new
|
|
574
376
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
customer: {
|
|
579
|
-
email: "user@example.com",
|
|
580
|
-
address: {
|
|
581
|
-
city: "Rome"
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
377
|
+
result = validator.call(email: "foo", age: 17)
|
|
378
|
+
puts result.success? # => false
|
|
379
|
+
puts result.errors.to_h # => {:email=>["not a valid email"], :age=>["must be greater than 18"]}
|
|
585
380
|
```
|
|
586
381
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
Another fundamental role that validators plays in the architecture of our projects is input whitelisting.
|
|
590
|
-
For security reasons, we want to allow known keys to come in and reject everything else.
|
|
591
|
-
|
|
592
|
-
This process happens when we invoke `#validate`.
|
|
593
|
-
Allowed keys are the ones defined with `.required`.
|
|
594
|
-
|
|
595
|
-
**Please note that whitelisting is only available for `Hanami::Validations::Form` mixin.**
|
|
382
|
+
#### Extra information
|
|
596
383
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
When we trigger the validation process with `#validate`, we get a result object in return. It’s able to tell if it’s successful, which rules the input data has violated and an output data bag.
|
|
384
|
+
The interpolation mechanism, accepts extra, arbitrary information expressed as a `Hash` (e.g. `code: "123"`)
|
|
600
385
|
|
|
601
386
|
```ruby
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
#### Messages
|
|
607
|
-
|
|
608
|
-
`result.messages` returns a nested set of validation error messages.
|
|
609
|
-
|
|
610
|
-
Each error carries on informations about a single rule violation.
|
|
611
|
-
|
|
612
|
-
```ruby
|
|
613
|
-
result.messages.fetch(:number) # => ["is missing"]
|
|
614
|
-
result.messages.fetch(:customer) # => ["is missing"]
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
#### Output
|
|
618
|
-
|
|
619
|
-
`result.output` is a `Hash` which is the result of whitelisting and coercions. It’s useful to pass it do other components that may want to persist that data.
|
|
620
|
-
|
|
621
|
-
```ruby
|
|
622
|
-
{
|
|
623
|
-
"number" => "123",
|
|
624
|
-
"unknown" => "foo"
|
|
625
|
-
}
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
If we receive the input above, `output` will look like this.
|
|
629
|
-
|
|
630
|
-
```ruby
|
|
631
|
-
result.output
|
|
632
|
-
# => { :number => 123 }
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
We can observe that:
|
|
636
|
-
|
|
637
|
-
* Keys are _symbolized_
|
|
638
|
-
* Only whitelisted keys are included
|
|
639
|
-
* Data is coerced
|
|
640
|
-
|
|
641
|
-
### Error Messages
|
|
642
|
-
|
|
643
|
-
To pick the right error message is crucial for user experience.
|
|
644
|
-
As usual `Hanami::Validations` comes to the rescue for most common cases and it leaves space to customization of behaviors.
|
|
645
|
-
|
|
646
|
-
We have seen that builtin predicates have default messages, while [inline predicates](#inline-custom-predicates) allow to specify a custom message via the `:message` option.
|
|
647
|
-
|
|
648
|
-
```ruby
|
|
649
|
-
class SignupValidator
|
|
650
|
-
include Hanami::Validations
|
|
651
|
-
|
|
652
|
-
predicate :email?, message: 'must be an email' do |current|
|
|
653
|
-
# ...
|
|
387
|
+
class RefundValidator < ApplicationValidator
|
|
388
|
+
schema do
|
|
389
|
+
required(:refunded_code).filled(:string)
|
|
654
390
|
end
|
|
655
391
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
required(:age).filled(:int?, gt?: 18)
|
|
392
|
+
rule(:refunded_code) do
|
|
393
|
+
key.failure(:network, code: "123") if values[:refunded_code] == "error"
|
|
659
394
|
end
|
|
660
395
|
end
|
|
661
396
|
|
|
662
|
-
|
|
397
|
+
validator = RefundValidator.new
|
|
663
398
|
|
|
664
|
-
result.
|
|
665
|
-
result.
|
|
666
|
-
result.
|
|
399
|
+
result = validator.call(refunded_code: "error")
|
|
400
|
+
puts result.success? # => false
|
|
401
|
+
puts result.errors.to_h # => {:refunded_code=>["there is a network error (123)"]}
|
|
667
402
|
```
|
|
668
403
|
|
|
669
|
-
|
|
404
|
+
Learn more about messages: https://dry-rb.org/gems/dry-validation/messages/
|
|
670
405
|
|
|
671
|
-
|
|
406
|
+
### External dependencies
|
|
672
407
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
en:
|
|
676
|
-
errors:
|
|
677
|
-
email?: "must be an email"
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
To be used like this:
|
|
408
|
+
If the validator needs to plug one or more objects to run the validations, there is a DSL to do so: `:option`.
|
|
409
|
+
When the validator is instantiated, the declared dependencies must be passed.
|
|
681
410
|
|
|
682
411
|
```ruby
|
|
683
|
-
|
|
684
|
-
include Hanami::Validations
|
|
685
|
-
messages_path 'config/messages.yml'
|
|
412
|
+
# frozen_string_literal: true
|
|
686
413
|
|
|
687
|
-
|
|
688
|
-
# ...
|
|
689
|
-
end
|
|
414
|
+
require "hanami/validations"
|
|
690
415
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
416
|
+
class AddressValidator
|
|
417
|
+
def valid?(value)
|
|
418
|
+
value.match(/Rome/)
|
|
694
419
|
end
|
|
695
420
|
end
|
|
696
421
|
|
|
697
|
-
|
|
698
|
-
|
|
422
|
+
class DeliveryValidator < Hanami::Validator
|
|
423
|
+
option :address_validator
|
|
699
424
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
425
|
+
schema do
|
|
426
|
+
required(:address).filled(:string)
|
|
427
|
+
end
|
|
703
428
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
email?: "must be an email"
|
|
429
|
+
rule(:address) do
|
|
430
|
+
key.failure("not a valid address") unless address_validator.valid?(values[:address])
|
|
431
|
+
end
|
|
432
|
+
end
|
|
709
433
|
|
|
710
|
-
|
|
711
|
-
signup:
|
|
712
|
-
age:
|
|
713
|
-
gt?: "must be an adult"
|
|
434
|
+
validator = DeliveryValidator.new(address_validator: AddressValidator.new)
|
|
714
435
|
|
|
436
|
+
result = validator.call(address: "foo")
|
|
437
|
+
puts result.success? # => false
|
|
438
|
+
puts result.errors.to_h # => {:address=>["not a valid address"]}
|
|
715
439
|
```
|
|
716
440
|
|
|
717
|
-
|
|
441
|
+
Read more about external dependencies: https://dry-rb.org/gems/dry-validation/external-dependencies/
|
|
718
442
|
|
|
719
|
-
|
|
720
|
-
result = SignupValidator.new(email: 'foo', age: 1).validate
|
|
443
|
+
### Mixin
|
|
721
444
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
```
|
|
445
|
+
`hanami-validations` 1.x used to ship a mixin `Hanami::Validations` to be included in classes to provide validation rules.
|
|
446
|
+
The 2.x series, still ships this mixin, but it will be probably removed in 3.x.
|
|
725
447
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
⚠ **CONVENTION:** For a given validator named `SignupValidator`, the framework will look for `signup` translation key. ⚠
|
|
448
|
+
```ruby
|
|
449
|
+
# frozen_string_literal: true
|
|
729
450
|
|
|
730
|
-
|
|
451
|
+
require "hanami/validations"
|
|
731
452
|
|
|
732
|
-
|
|
733
|
-
class SignupValidator
|
|
453
|
+
class UserValidator
|
|
734
454
|
include Hanami::Validations
|
|
735
455
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
# ...
|
|
456
|
+
validations do
|
|
457
|
+
required(:number).filled(:integer, eql?: 23)
|
|
458
|
+
end
|
|
740
459
|
end
|
|
741
|
-
```
|
|
742
460
|
|
|
743
|
-
|
|
461
|
+
result = UserValidator.new(number: 23).validate
|
|
744
462
|
|
|
745
|
-
|
|
746
|
-
#
|
|
747
|
-
|
|
748
|
-
# ...
|
|
749
|
-
rules:
|
|
750
|
-
my_signup:
|
|
751
|
-
age:
|
|
752
|
-
gt?: "must be an adult"
|
|
753
|
-
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
#### Internationalization (I18n)
|
|
757
|
-
|
|
758
|
-
If your project already depends on `i18n` gem, `Hanami::Validations` is able to look at the translations defined for that gem and to use them.
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
```ruby
|
|
762
|
-
class SignupValidator
|
|
763
|
-
include Hanami::Validations
|
|
463
|
+
puts result.success? # => true
|
|
464
|
+
puts result.to_h # => {:number=>23}
|
|
465
|
+
puts result.errors.to_h # => {}
|
|
764
466
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
# ...
|
|
768
|
-
end
|
|
769
|
-
```
|
|
467
|
+
result = UserValidator.new(number: 11).validate
|
|
770
468
|
|
|
771
|
-
|
|
772
|
-
#
|
|
773
|
-
|
|
774
|
-
errors:
|
|
775
|
-
signup:
|
|
776
|
-
# ...
|
|
469
|
+
puts result.success? # => true
|
|
470
|
+
puts result.to_h # => {:number=>21}
|
|
471
|
+
puts result.errors.to_h # => {:number=>["must be equal to 23"]}
|
|
777
472
|
```
|
|
778
473
|
|
|
779
474
|
## FAQs
|
|
@@ -784,10 +479,7 @@ Please remember that **uniqueness validation is a huge race condition between ap
|
|
|
784
479
|
|
|
785
480
|
Please read more at: [The Perils of Uniqueness Validations](http://robots.thoughtbot.com/the-perils-of-uniqueness-validations).
|
|
786
481
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
Thanks to [dry-rb](http://dry-rb.org) Community for their priceless support. ❤️
|
|
790
|
-
`hanami-validations` uses [dry-validation](http://dry-rb.org/gems/dry-validation) as powerful low-level engine.
|
|
482
|
+
If you need to implement it, please use the External dependencies feature (see above).
|
|
791
483
|
|
|
792
484
|
## Contributing
|
|
793
485
|
|
|
@@ -799,6 +491,6 @@ Thanks to [dry-rb](http://dry-rb.org) Community for their priceless support. ❤
|
|
|
799
491
|
|
|
800
492
|
## Copyright
|
|
801
493
|
|
|
802
|
-
Copyright © 2014-
|
|
494
|
+
Copyright © 2014-2019 Luca Guidi – Released under MIT License
|
|
803
495
|
|
|
804
496
|
This project was formerly known as Lotus (`lotus-validations`).
|