hanami-validations 1.3.6 → 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 -5
- data/README.md +259 -568
- data/hanami-validations.gemspec +17 -17
- data/lib/hanami/validations.rb +37 -311
- data/lib/hanami/validations/form.rb +27 -19
- data/lib/hanami/validations/version.rb +3 -2
- data/lib/hanami/validator.rb +9 -0
- metadata +11 -54
- data/lib/hanami-validations.rb +0 -1
- data/lib/hanami/validations/inline_predicate.rb +0 -46
- data/lib/hanami/validations/namespace.rb +0 -65
- data/lib/hanami/validations/predicates.rb +0 -45
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,13 +1,23 @@
|
|
1
1
|
# Hanami::Validations
|
2
2
|
Validations mixin for Ruby objects
|
3
3
|
|
4
|
-
##
|
4
|
+
## v2.0.0.alpha1 - 2019-07-26
|
5
5
|
### Added
|
6
|
-
- [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
|
7
12
|
|
8
|
-
|
9
|
-
|
10
|
-
- [
|
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`)
|
11
21
|
|
12
22
|
## v1.3.4 - 2019-07-26
|
13
23
|
### Fixed
|
data/README.md
CHANGED
@@ -1,12 +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
9
|
[](https://circleci.com/gh/hanami/validations/tree/master)
|
10
|
+
[](https://ci.hanamirb.org/hanami/validations)
|
10
11
|
[](https://codecov.io/gh/hanami/validations)
|
11
12
|
[](https://depfu.com/github/hanami/validations?project=Bundler)
|
12
13
|
[](http://inch-ci.org/github/hanami/validations)
|
@@ -24,14 +25,14 @@ Validations mixin for Ruby objects
|
|
24
25
|
|
25
26
|
## Rubies
|
26
27
|
|
27
|
-
__Hanami::Validations__ supports Ruby (MRI) 2.
|
28
|
+
__Hanami::Validations__ supports Ruby (MRI) 2.4+ and JRuby 9.2+
|
28
29
|
|
29
30
|
## Installation
|
30
31
|
|
31
32
|
Add this line to your application's Gemfile:
|
32
33
|
|
33
34
|
```ruby
|
34
|
-
gem
|
35
|
+
gem "hanami-validations"
|
35
36
|
```
|
36
37
|
|
37
38
|
And then execute:
|
@@ -48,733 +49,426 @@ $ gem install hanami-validations
|
|
48
49
|
|
49
50
|
## Usage
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
It works with input hashes and lets us to define a set of validation rules **for each** key/value pair. These rules are wrapped by lambdas (or special DSL) that check the input for a specific key to determine if it's valid or not. To do that, we translate business requirements into predicates that are chained together with Ruby _faux boolean logic_ operators (eg. `&` or `|`).
|
54
|
-
|
55
|
-
Think of a signup form. We need to ensure data integrity for the `name` field with the following rules. It is required, it has to be: filled **and** a string **and** its size must be greater than 3 chars, but lesser than 64. Here’s the code, **read it aloud** and notice how it perfectly expresses our needs for `name`.
|
56
|
-
|
57
|
-
```ruby
|
58
|
-
class Signup
|
59
|
-
include Hanami::Validations
|
60
|
-
|
61
|
-
validations do
|
62
|
-
required(:name) { filled? & str? & size?(3..64) }
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
result = Signup.new(name: "Luca").validate
|
67
|
-
result.success? # => true
|
68
|
-
```
|
69
|
-
|
70
|
-
There is more that `Hanami::Validations` can do: **type safety**, **composition**, **complex data structures**, **built-in and custom predicates**.
|
71
|
-
|
72
|
-
But before to dive into advanced topics, we need to understand the basics of _boolean logic_.
|
73
|
-
|
74
|
-
### Boolean Logic
|
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.
|
75
54
|
|
76
|
-
|
55
|
+
### Overview
|
77
56
|
|
78
|
-
|
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.
|
79
59
|
|
80
|
-
|
81
|
-
|
82
|
-
👇 👇 👇 👇 👇 👇 👇 👇
|
83
|
-
required(:name) { filled? & str? & size? (3 .. 64) }
|
84
|
-
|
85
|
-
```
|
86
|
-
|
87
|
-
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.
|
88
|
-
|
89
|
-
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 `&`.
|
90
|
-
|
91
|
-
#### Logic Operators
|
92
|
-
|
93
|
-
We support four logic operators:
|
94
|
-
|
95
|
-
* `&` (aliased as `and`) for _conjunction_
|
96
|
-
* `|` (aliased as `or`) for _disjunction_
|
97
|
-
* `>` (aliased as `then`) for _implication_
|
98
|
-
* `^` (aliased as `xor`) for _exclusive disjunction_
|
99
|
-
|
100
|
-
#### Context Of Execution
|
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.
|
101
62
|
|
102
|
-
|
103
|
-
That's because the context of execution of these validations isn't a plain lambda, but something richer.
|
63
|
+
Only when the input is formally valid (according to the **schema**), validation **rules** are checked.
|
104
64
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
⚠ **For this reason, we don't allow any arbitrary Ruby code to be executed, but only well defined predicates.** ⚠
|
109
|
-
|
110
|
-
### Predicates
|
65
|
+
```ruby
|
66
|
+
# frozen_string_literal: true
|
111
67
|
|
112
|
-
|
68
|
+
require "hanami/validations"
|
113
69
|
|
114
|
-
|
70
|
+
class SignupValidator < Hanami::Validator
|
71
|
+
schema do
|
72
|
+
required(:email).value(:string)
|
73
|
+
required(:age).value(:integer)
|
74
|
+
end
|
115
75
|
|
116
|
-
|
76
|
+
rule(:age) do
|
77
|
+
key.failure("must be greater than 18") if value < 18
|
78
|
+
end
|
79
|
+
end
|
117
80
|
|
118
|
-
|
81
|
+
validator = SignupValidator.new
|
119
82
|
|
120
|
-
|
121
|
-
|
122
|
-
```
|
83
|
+
result = validator.call(email: "user@hanamirb.test", age: 37)
|
84
|
+
result.success? # => true
|
123
85
|
|
124
|
-
|
86
|
+
result = validator.call(email: "user@hanamirb.test", age: "foo")
|
87
|
+
result.success? # => false
|
88
|
+
result.errors.to_h # => {:age=>["must be an integer"]}
|
125
89
|
|
126
|
-
|
127
|
-
|
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"]}
|
128
93
|
```
|
129
94
|
|
130
|
-
|
95
|
+
### Schemas
|
131
96
|
|
132
|
-
|
97
|
+
A basic schema doesn't apply data coercion, input must already have the right Ruby types.
|
133
98
|
|
134
99
|
```ruby
|
135
|
-
|
136
|
-
```
|
100
|
+
# frozen_string_literal: true
|
137
101
|
|
138
|
-
|
139
|
-
|
140
|
-
This predicate tests if the input is equal to a given value.
|
141
|
-
|
142
|
-
```ruby
|
143
|
-
required(:magic_number) { eql?(23) }
|
144
|
-
```
|
102
|
+
require "hanami/validations"
|
145
103
|
|
146
|
-
|
104
|
+
class SignupValidator < Hanami::Validator
|
105
|
+
schema do
|
106
|
+
required(:email).value(:string)
|
107
|
+
required(:age).value(:integer)
|
108
|
+
end
|
109
|
+
end
|
147
110
|
|
148
|
-
|
111
|
+
validator = SignupValidator.new
|
149
112
|
|
150
|
-
|
113
|
+
result = validator.call(email: "user@hanamirb.test", age: 37)
|
114
|
+
puts result.success? # => true
|
151
115
|
|
152
|
-
|
153
|
-
|
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"]}
|
154
119
|
```
|
155
120
|
|
156
|
-
|
157
|
-
|
158
|
-
This is a predicate that works with a regular expression to match it against data input.
|
159
|
-
|
160
|
-
```ruby
|
161
|
-
require 'uri'
|
162
|
-
HTTP_FORMAT = URI.regexp(%w(http https))
|
163
|
-
|
164
|
-
required(:url) { format?(HTTP_FORMAT) }
|
165
|
-
```
|
121
|
+
### Params
|
166
122
|
|
167
|
-
|
123
|
+
When used in _params mode_, a schema applies data coercion, before to run validation checks.
|
168
124
|
|
169
|
-
This
|
125
|
+
This is designed for Web form/HTTP params.
|
170
126
|
|
171
127
|
```ruby
|
172
|
-
|
173
|
-
```
|
174
|
-
|
175
|
-
#### Greater Than Equal
|
176
|
-
|
177
|
-
This is an _open boundary_ variation of `gt?`. It checks if an input is **greater than or equal** of a given number.
|
128
|
+
# frozen_string_literal: true
|
178
129
|
|
179
|
-
|
180
|
-
|
181
|
-
```
|
130
|
+
require "bundler/setup"
|
131
|
+
require "hanami/validations"
|
182
132
|
|
183
|
-
|
133
|
+
class SignupValidator < Hanami::Validator
|
134
|
+
params do
|
135
|
+
required(:email).value(:string)
|
136
|
+
required(:age).value(:integer)
|
137
|
+
end
|
138
|
+
end
|
184
139
|
|
185
|
-
|
140
|
+
validator = SignupValidator.new
|
186
141
|
|
187
|
-
|
188
|
-
|
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}
|
189
145
|
```
|
190
146
|
|
191
|
-
|
147
|
+
### JSON
|
192
148
|
|
193
|
-
|
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.
|
194
151
|
|
195
152
|
```ruby
|
196
|
-
|
197
|
-
```
|
153
|
+
# frozen_string_literal: true
|
198
154
|
|
199
|
-
|
155
|
+
require "hanami/validations"
|
200
156
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
required(:age)
|
205
|
-
|
157
|
+
class SignupValidator < Hanami::Validator
|
158
|
+
json do
|
159
|
+
required(:email).value(:string)
|
160
|
+
required(:age).value(:integer)
|
161
|
+
end
|
162
|
+
end
|
206
163
|
|
207
|
-
|
164
|
+
validator = SignupValidator.new
|
208
165
|
|
209
|
-
|
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}
|
210
169
|
|
211
|
-
|
212
|
-
|
213
|
-
required(:languages) { filled? } # collection
|
170
|
+
result = validator.call(email: "user@hanamirb.test", age: "37")
|
171
|
+
puts result.success? # => false
|
214
172
|
```
|
215
173
|
|
216
|
-
|
174
|
+
### Whitelisting
|
217
175
|
|
218
|
-
|
176
|
+
Unknown keys from incoming data are filtered out:
|
219
177
|
|
220
178
|
```ruby
|
221
|
-
|
222
|
-
```
|
179
|
+
# frozen_string_literal: true
|
223
180
|
|
224
|
-
|
181
|
+
require "hanami/validations"
|
225
182
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
#### None
|
183
|
+
class SignupValidator < Hanami::Validator
|
184
|
+
schema do
|
185
|
+
required(:email).value(:string)
|
186
|
+
end
|
187
|
+
end
|
233
188
|
|
234
|
-
|
189
|
+
validator = SignupValidator.new
|
235
190
|
|
236
|
-
|
237
|
-
|
191
|
+
result = validator.call(email: "user@hanamirb.test", foo: "bar")
|
192
|
+
puts result.success? # => true
|
193
|
+
puts result.to_h # => {:email=>"user@hanamirb.test"}
|
238
194
|
```
|
239
195
|
|
240
|
-
|
241
|
-
|
242
|
-
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
|
243
197
|
|
244
198
|
```ruby
|
245
|
-
|
246
|
-
required(:password) { size?(8..32) } # range
|
247
|
-
```
|
199
|
+
# frozen_string_literal: true
|
248
200
|
|
249
|
-
|
201
|
+
require "hanami/validations"
|
250
202
|
|
251
|
-
|
252
|
-
|
253
|
-
```
|
203
|
+
module Types
|
204
|
+
include Dry::Types()
|
254
205
|
|
255
|
-
|
206
|
+
StrippedString = Types::String.constructor(&:strip)
|
207
|
+
end
|
256
208
|
|
257
|
-
|
209
|
+
class SignupValidator < Hanami::Validator
|
210
|
+
params do
|
211
|
+
required(:email).value(Types::StrippedString)
|
212
|
+
required(:age).value(:integer)
|
213
|
+
end
|
214
|
+
end
|
258
215
|
|
259
|
-
|
260
|
-
MEGABYTE = 1024 ** 2
|
216
|
+
validator = SignupValidator.new
|
261
217
|
|
262
|
-
|
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}
|
263
221
|
```
|
264
222
|
|
265
|
-
###
|
266
|
-
|
267
|
-
We have seen that built-in predicates as an expressive tool to get our job done with common use cases.
|
268
|
-
|
269
|
-
But what if our case is not common? We can define our own custom predicates.
|
270
|
-
|
271
|
-
#### Inline Custom Predicates
|
223
|
+
### Rules
|
272
224
|
|
273
|
-
|
225
|
+
Rules are performing a set of domain-specific validation checks.
|
226
|
+
Rules are executed only after the validations from the schema are satisfied.
|
274
227
|
|
275
228
|
```ruby
|
276
|
-
|
229
|
+
# frozen_string_literal: true
|
277
230
|
|
278
|
-
|
279
|
-
include Hanami::Validations
|
231
|
+
require "hanami/validations"
|
280
232
|
|
281
|
-
|
282
|
-
|
233
|
+
class EventValidator < Hanami::Validator
|
234
|
+
params do
|
235
|
+
required(:start_date).value(:date)
|
283
236
|
end
|
284
237
|
|
285
|
-
|
286
|
-
|
238
|
+
rule(:start_date) do
|
239
|
+
key.failure("must be in the future") if value <= Date.today
|
287
240
|
end
|
288
241
|
end
|
289
|
-
```
|
290
242
|
|
291
|
-
|
243
|
+
validator = EventValidator.new
|
292
244
|
|
293
|
-
|
245
|
+
result = validator.call(start_date: "foo")
|
246
|
+
puts result.success? # => false
|
247
|
+
puts result.errors.to_h # => {:start_date=>["must be a date"]}
|
294
248
|
|
295
|
-
|
296
|
-
|
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"]}
|
297
252
|
|
298
|
-
|
299
|
-
|
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
|
+
```
|
300
257
|
|
301
|
-
|
258
|
+
Learn more about rules: https://dry-rb.org/gems/dry-validation/rules/
|
302
259
|
|
303
|
-
|
304
|
-
current.match(/.../)
|
305
|
-
end
|
306
|
-
end
|
307
|
-
```
|
260
|
+
### Inheritance
|
308
261
|
|
309
|
-
|
262
|
+
Schema and rules validations can be inherited and used by subclasses
|
310
263
|
|
311
264
|
```ruby
|
312
|
-
|
313
|
-
require_relative 'my_predicates'
|
265
|
+
# frozen_string_literal: true
|
314
266
|
|
315
|
-
|
316
|
-
include Hanami::Validations
|
317
|
-
predicates MyPredicates
|
267
|
+
require "hanami/validations"
|
318
268
|
|
319
|
-
|
320
|
-
|
269
|
+
class ApplicationValidator < Hanami::Validator
|
270
|
+
params do
|
271
|
+
optional(:_csrf_token).filled(:string)
|
321
272
|
end
|
322
273
|
end
|
323
|
-
```
|
324
|
-
|
325
|
-
### Required and Optional keys
|
326
|
-
|
327
|
-
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`.
|
328
|
-
|
329
|
-
```ruby
|
330
|
-
require 'hanami/validations'
|
331
274
|
|
332
|
-
class
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
optional(:referral) { ... }
|
275
|
+
class SignupValidator < ApplicationValidator
|
276
|
+
params do
|
277
|
+
required(:user).hash do
|
278
|
+
required(:email).filled(:string)
|
279
|
+
end
|
338
280
|
end
|
339
281
|
end
|
340
|
-
```
|
341
|
-
|
342
|
-
### Type Safety
|
343
|
-
|
344
|
-
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.
|
345
|
-
|
346
|
-
Why this is so important? Because if we try to invoke a method on the input we’ll get a `NoMethodError` if the input doesn’t respond to it. Which isn’t nice, right?
|
347
|
-
|
348
|
-
Before to use a predicate, we want to ensure that the input is an instance of the expected type. Let’s introduce another new predicate for our need: `#type?`.
|
349
|
-
|
350
|
-
```ruby
|
351
|
-
required(:age) { type?(Integer) & gteq?(18) }
|
352
|
-
```
|
353
|
-
|
354
|
-
It takes the input and tries to coerce it. If it fails, the execution stops. If it succeed, the subsequent predicates can trust `#type?` and be sure that the input is an integer.
|
355
|
-
|
356
|
-
**We suggest to use `#type?` at the beginning of the validations block. This _type safety_ policy is crucial to prevent runtime errors.**
|
357
|
-
|
358
|
-
`Hanami::Validations` supports the most common Ruby types:
|
359
|
-
|
360
|
-
* `Array` (aliased as `array?`)
|
361
|
-
* `BigDecimal` (aliased as `decimal?`)
|
362
|
-
* `Boolean` (aliased as `bool?`)
|
363
|
-
* `Date` (aliased as `date?`)
|
364
|
-
* `DateTime` (aliased as `date_time?`)
|
365
|
-
* `Float` (aliased as `float?`)
|
366
|
-
* `Hash` (aliased as `hash?`)
|
367
|
-
* `Integer` (aliased as `int?`)
|
368
|
-
* `String` (aliased as `str?`)
|
369
|
-
* `Time` (aliased as `time?`)
|
370
|
-
|
371
|
-
For each supported type, there a convenient predicate that acts as an alias. For instance, the two lines of code below are **equivalent**.
|
372
282
|
|
373
|
-
|
374
|
-
required(:age) { type?(Integer) }
|
375
|
-
required(:age) { int? }
|
376
|
-
```
|
377
|
-
|
378
|
-
### Macros
|
379
|
-
|
380
|
-
Rule composition with blocks is powerful, but it can become verbose.
|
381
|
-
To reduce verbosity, `Hanami::Validations` offers convenient _macros_ that are internally _expanded_ (aka interpreted) to an equivalent _block expression_
|
382
|
-
|
383
|
-
#### Filled
|
283
|
+
validator = SignupValidator.new
|
384
284
|
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
# expands to
|
389
|
-
# required(:age) { filled? }
|
390
|
-
|
391
|
-
required(:age).filled
|
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"}
|
392
288
|
```
|
393
289
|
|
394
|
-
|
395
|
-
# expands to
|
396
|
-
# required(:age) { filled? & type?(Integer) }
|
290
|
+
### Messages
|
397
291
|
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
```ruby
|
402
|
-
# expands to
|
403
|
-
# required(:age) { filled? & type?(Integer) & gt?(18) }
|
404
|
-
|
405
|
-
required(:age).filled(:int?, gt?: 18)
|
406
|
-
```
|
407
|
-
|
408
|
-
In the examples above `age` is **always required** as value.
|
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.
|
409
294
|
|
410
|
-
|
411
|
-
|
412
|
-
To use when a value can be nil:
|
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.
|
413
296
|
|
414
297
|
```ruby
|
415
|
-
#
|
416
|
-
# required(:age) { none? | int? }
|
417
|
-
|
418
|
-
required(:age).maybe(:int?)
|
419
|
-
```
|
420
|
-
|
421
|
-
In the example above `age` can be `nil`, but if we send the value, it **must** be an integer.
|
422
|
-
|
423
|
-
#### Each
|
298
|
+
# frozen_string_literal: true
|
424
299
|
|
425
|
-
|
426
|
-
|
427
|
-
```ruby
|
428
|
-
# expands to
|
429
|
-
# required(:tags) { array? { each { str? } } }
|
300
|
+
require "hanami/validations"
|
430
301
|
|
431
|
-
|
302
|
+
class ApplicationValidator < Hanami::Validator
|
303
|
+
config.messages.top_namespace = "bookshelf"
|
304
|
+
config.messages.load_paths << "config/errors.yml"
|
305
|
+
end
|
432
306
|
```
|
433
307
|
|
434
|
-
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.
|
435
310
|
|
311
|
+
There are two ways to organize messages:
|
436
312
|
|
437
|
-
|
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.
|
438
315
|
|
439
|
-
|
316
|
+
Our **suggestion** is to start with **specific** messages and see if there is a need to generalize them.
|
440
317
|
|
441
|
-
```
|
442
|
-
|
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"
|
443
330
|
```
|
444
331
|
|
445
|
-
|
446
|
-
|
447
|
-
⚠ **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. ⚠
|
448
|
-
|
449
|
-
### Forms
|
450
|
-
|
451
|
-
An important precondition to check before to implement a validator is about the expected input.
|
452
|
-
When we use validators for already preprocessed data it's safe to use basic validations from `Hanami::Validations` mixin.
|
453
|
-
|
454
|
-
If the data is coming directly from user input via a HTTP form, it's advisable to use `Hanami::Validations::Form` instead.
|
455
|
-
**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.
|
456
|
-
|
457
|
-
### Rules
|
458
|
-
|
459
|
-
Predicates and macros are tools to code validations that concern a single key like `first_name` or `email`.
|
460
|
-
If the outcome of a validation depends on two or more attributes we can use _rules_.
|
461
|
-
|
462
|
-
Here's a practical example: a job board.
|
463
|
-
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).
|
464
|
-
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`.
|
465
|
-
|
466
|
-
Here's the code:
|
332
|
+
#### General purpose messages
|
467
333
|
|
468
334
|
```ruby
|
469
|
-
class
|
470
|
-
|
471
|
-
|
472
|
-
validations do
|
473
|
-
required(:type).filled(:int?, included_in?: [1, 2, 3])
|
474
|
-
|
475
|
-
optional(:location).maybe(:str?)
|
476
|
-
optional(:remote).maybe(:bool?)
|
477
|
-
|
478
|
-
required(:title).filled(:str?)
|
479
|
-
required(:description).filled(:str?)
|
480
|
-
required(:company).filled(:str?)
|
481
|
-
|
482
|
-
optional(:website).filled(:str?, format?: URI.regexp(%w(http https)))
|
483
|
-
|
484
|
-
rule(location_presence: [:location, :remote]) do |location, remote|
|
485
|
-
(remote.none? | remote.false?).then(location.filled?) &
|
486
|
-
remote.true?.then(location.none?)
|
487
|
-
end
|
335
|
+
class SignupValidator < ApplicationValidator
|
336
|
+
schema do
|
337
|
+
required(:username).filled(:string)
|
488
338
|
end
|
489
|
-
end
|
490
|
-
```
|
491
|
-
|
492
|
-
We specify a rule with `rule` method, which takes an arbitrary name and an array of preconditions.
|
493
|
-
Only if `:location` and `:remote` are valid according to their validations described above, the `rule` block is evaluated.
|
494
|
-
|
495
|
-
The block yields the same exact keys that we put in the precondintions.
|
496
|
-
So for `[:location, :remote]` it will yield the corresponding values, bound to the `location` and `remote` variables.
|
497
|
-
|
498
|
-
We can use these variables to define the rule. We covered a few cases:
|
499
|
-
|
500
|
-
* If `remote` is missing or false, then `location` must be filled
|
501
|
-
* If `remote` is true, then `location` must be omitted
|
502
|
-
|
503
|
-
### Nested Input Data
|
504
|
-
|
505
|
-
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.
|
506
339
|
|
507
|
-
|
508
|
-
|
509
|
-
required(:customer).schema do
|
510
|
-
required(:email) { … }
|
511
|
-
required(:name) { … }
|
512
|
-
# other validations …
|
340
|
+
rule(:username) do
|
341
|
+
key.failure(:taken) if values[:username] == "jodosha"
|
513
342
|
end
|
514
343
|
end
|
515
|
-
```
|
516
344
|
|
517
|
-
|
345
|
+
validator = SignupValidator.new
|
518
346
|
|
519
|
-
|
520
|
-
|
521
|
-
required(:customer).schema do
|
522
|
-
# other validations …
|
347
|
+
result = validator.call(username: "foo")
|
348
|
+
puts result.success? # => true
|
523
349
|
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
end
|
528
|
-
end
|
529
|
-
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"]}
|
530
353
|
```
|
531
354
|
|
532
|
-
|
533
|
-
|
534
|
-
Until now, we have seen only small snippets to show specific features. That really close view prevents us to see the big picture of complex real world projects.
|
355
|
+
#### Specific messages
|
535
356
|
|
536
|
-
|
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.
|
537
358
|
|
538
359
|
```ruby
|
539
|
-
class
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
required(:street) { … }
|
360
|
+
class SignupValidator < ApplicationValidator
|
361
|
+
schema do
|
362
|
+
required(:email).filled(:string)
|
363
|
+
required(:age).filled(:integer)
|
544
364
|
end
|
545
|
-
end
|
546
|
-
```
|
547
|
-
|
548
|
-
This validator can be reused by other validators.
|
549
|
-
|
550
|
-
```ruby
|
551
|
-
class CustomerValidator
|
552
|
-
include Hanami::Validations
|
553
365
|
|
554
|
-
|
555
|
-
|
556
|
-
required(:address).schema(AddressValidator)
|
366
|
+
rule(:email) do
|
367
|
+
key.failure(:invalid) unless values[:email] =~ /@/
|
557
368
|
end
|
558
|
-
end
|
559
|
-
```
|
560
369
|
|
561
|
-
|
562
|
-
|
563
|
-
```ruby
|
564
|
-
class OrderValidator
|
565
|
-
include Hanami::Validations
|
566
|
-
|
567
|
-
validations do
|
568
|
-
required(:number) { … }
|
569
|
-
required(:customer).schema(CustomerValidator)
|
370
|
+
rule(:age) do
|
371
|
+
key.failure(:invalid) if values[:age] < 18
|
570
372
|
end
|
571
373
|
end
|
572
|
-
```
|
573
|
-
|
574
|
-
In the end, `OrderValidator` is able to validate a complex data structure like this:
|
575
|
-
|
576
|
-
```ruby
|
577
|
-
{
|
578
|
-
number: "123",
|
579
|
-
customer: {
|
580
|
-
email: "user@example.com",
|
581
|
-
address: {
|
582
|
-
city: "Rome"
|
583
|
-
}
|
584
|
-
}
|
585
|
-
}
|
586
|
-
```
|
587
|
-
|
588
|
-
### Whitelisting
|
589
|
-
|
590
|
-
Another fundamental role that validators plays in the architecture of our projects is input whitelisting.
|
591
|
-
For security reasons, we want to allow known keys to come in and reject everything else.
|
592
|
-
|
593
|
-
This process happens when we invoke `#validate`.
|
594
|
-
Allowed keys are the ones defined with `.required`.
|
595
374
|
|
596
|
-
|
375
|
+
validator = SignupValidator.new
|
597
376
|
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
```ruby
|
603
|
-
result = OrderValidator.new({}).validate
|
604
|
-
result.success? # => false
|
605
|
-
```
|
606
|
-
|
607
|
-
#### Messages
|
608
|
-
|
609
|
-
`result.messages` returns a nested set of validation error messages.
|
610
|
-
|
611
|
-
Each error carries on informations about a single rule violation.
|
612
|
-
|
613
|
-
```ruby
|
614
|
-
result.messages.fetch(:number) # => ["is missing"]
|
615
|
-
result.messages.fetch(:customer) # => ["is missing"]
|
616
|
-
```
|
617
|
-
|
618
|
-
#### Output
|
619
|
-
|
620
|
-
`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.
|
621
|
-
|
622
|
-
```ruby
|
623
|
-
{
|
624
|
-
"number" => "123",
|
625
|
-
"unknown" => "foo"
|
626
|
-
}
|
627
|
-
```
|
628
|
-
|
629
|
-
If we receive the input above, `output` will look like this.
|
630
|
-
|
631
|
-
```ruby
|
632
|
-
result.output
|
633
|
-
# => { :number => 123 }
|
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"]}
|
634
380
|
```
|
635
381
|
|
636
|
-
|
637
|
-
|
638
|
-
* Keys are _symbolized_
|
639
|
-
* Only whitelisted keys are included
|
640
|
-
* Data is coerced
|
641
|
-
|
642
|
-
### Error Messages
|
643
|
-
|
644
|
-
To pick the right error message is crucial for user experience.
|
645
|
-
As usual `Hanami::Validations` comes to the rescue for most common cases and it leaves space to customization of behaviors.
|
382
|
+
#### Extra information
|
646
383
|
|
647
|
-
|
384
|
+
The interpolation mechanism, accepts extra, arbitrary information expressed as a `Hash` (e.g. `code: "123"`)
|
648
385
|
|
649
386
|
```ruby
|
650
|
-
class
|
651
|
-
|
652
|
-
|
653
|
-
predicate :email?, message: 'must be an email' do |current|
|
654
|
-
# ...
|
387
|
+
class RefundValidator < ApplicationValidator
|
388
|
+
schema do
|
389
|
+
required(:refunded_code).filled(:string)
|
655
390
|
end
|
656
391
|
|
657
|
-
|
658
|
-
|
659
|
-
required(:age).filled(:int?, gt?: 18)
|
392
|
+
rule(:refunded_code) do
|
393
|
+
key.failure(:network, code: "123") if values[:refunded_code] == "error"
|
660
394
|
end
|
661
395
|
end
|
662
396
|
|
663
|
-
|
397
|
+
validator = RefundValidator.new
|
664
398
|
|
665
|
-
result.
|
666
|
-
result.
|
667
|
-
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)"]}
|
668
402
|
```
|
669
403
|
|
670
|
-
|
404
|
+
Learn more about messages: https://dry-rb.org/gems/dry-validation/messages/
|
671
405
|
|
672
|
-
|
406
|
+
### External dependencies
|
673
407
|
|
674
|
-
|
675
|
-
|
676
|
-
en:
|
677
|
-
errors:
|
678
|
-
email?: "must be an email"
|
679
|
-
```
|
680
|
-
|
681
|
-
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.
|
682
410
|
|
683
411
|
```ruby
|
684
|
-
|
685
|
-
include Hanami::Validations
|
686
|
-
messages_path 'config/messages.yml'
|
412
|
+
# frozen_string_literal: true
|
687
413
|
|
688
|
-
|
689
|
-
# ...
|
690
|
-
end
|
414
|
+
require "hanami/validations"
|
691
415
|
|
692
|
-
|
693
|
-
|
694
|
-
|
416
|
+
class AddressValidator
|
417
|
+
def valid?(value)
|
418
|
+
value.match(/Rome/)
|
695
419
|
end
|
696
420
|
end
|
697
421
|
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
#### Custom Error Messages
|
422
|
+
class DeliveryValidator < Hanami::Validator
|
423
|
+
option :address_validator
|
702
424
|
|
703
|
-
|
425
|
+
schema do
|
426
|
+
required(:address).filled(:string)
|
427
|
+
end
|
704
428
|
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
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
|
710
433
|
|
711
|
-
|
712
|
-
signup:
|
713
|
-
age:
|
714
|
-
gt?: "must be an adult"
|
434
|
+
validator = DeliveryValidator.new(address_validator: AddressValidator.new)
|
715
435
|
|
436
|
+
result = validator.call(address: "foo")
|
437
|
+
puts result.success? # => false
|
438
|
+
puts result.errors.to_h # => {:address=>["not a valid address"]}
|
716
439
|
```
|
717
440
|
|
718
|
-
|
441
|
+
Read more about external dependencies: https://dry-rb.org/gems/dry-validation/external-dependencies/
|
719
442
|
|
720
|
-
|
721
|
-
result = SignupValidator.new(email: 'foo', age: 1).validate
|
443
|
+
### Mixin
|
722
444
|
|
723
|
-
|
724
|
-
|
725
|
-
```
|
726
|
-
|
727
|
-
##### Custom namespace
|
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.
|
728
447
|
|
729
|
-
|
448
|
+
```ruby
|
449
|
+
# frozen_string_literal: true
|
730
450
|
|
731
|
-
|
451
|
+
require "hanami/validations"
|
732
452
|
|
733
|
-
|
734
|
-
class SignupValidator
|
453
|
+
class UserValidator
|
735
454
|
include Hanami::Validations
|
736
455
|
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
# ...
|
456
|
+
validations do
|
457
|
+
required(:number).filled(:integer, eql?: 23)
|
458
|
+
end
|
741
459
|
end
|
742
|
-
```
|
743
460
|
|
744
|
-
|
461
|
+
result = UserValidator.new(number: 23).validate
|
745
462
|
|
746
|
-
|
747
|
-
#
|
748
|
-
|
749
|
-
# ...
|
750
|
-
rules:
|
751
|
-
my_signup:
|
752
|
-
age:
|
753
|
-
gt?: "must be an adult"
|
754
|
-
|
755
|
-
```
|
463
|
+
puts result.success? # => true
|
464
|
+
puts result.to_h # => {:number=>23}
|
465
|
+
puts result.errors.to_h # => {}
|
756
466
|
|
757
|
-
|
467
|
+
result = UserValidator.new(number: 11).validate
|
758
468
|
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
```ruby
|
763
|
-
class SignupValidator
|
764
|
-
include Hanami::Validations
|
765
|
-
|
766
|
-
messages :i18n
|
767
|
-
|
768
|
-
# ...
|
769
|
-
end
|
770
|
-
```
|
771
|
-
|
772
|
-
```yaml
|
773
|
-
# config/locales/en.yml
|
774
|
-
en:
|
775
|
-
errors:
|
776
|
-
signup:
|
777
|
-
# ...
|
469
|
+
puts result.success? # => true
|
470
|
+
puts result.to_h # => {:number=>21}
|
471
|
+
puts result.errors.to_h # => {:number=>["must be equal to 23"]}
|
778
472
|
```
|
779
473
|
|
780
474
|
## FAQs
|
@@ -785,10 +479,7 @@ Please remember that **uniqueness validation is a huge race condition between ap
|
|
785
479
|
|
786
480
|
Please read more at: [The Perils of Uniqueness Validations](http://robots.thoughtbot.com/the-perils-of-uniqueness-validations).
|
787
481
|
|
788
|
-
|
789
|
-
|
790
|
-
Thanks to [dry-rb](http://dry-rb.org) Community for their priceless support. ❤️
|
791
|
-
`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).
|
792
483
|
|
793
484
|
## Contributing
|
794
485
|
|
@@ -800,6 +491,6 @@ Thanks to [dry-rb](http://dry-rb.org) Community for their priceless support. ❤
|
|
800
491
|
|
801
492
|
## Copyright
|
802
493
|
|
803
|
-
Copyright © 2014-
|
494
|
+
Copyright © 2014-2019 Luca Guidi – Released under MIT License
|
804
495
|
|
805
496
|
This project was formerly known as Lotus (`lotus-validations`).
|