hanami-validations 1.3.7 → 2.0.0.beta1
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 +99 -0
- data/README.md +262 -568
- data/hanami-validations.gemspec +4 -6
- data/lib/hanami/validations/form.rb +26 -20
- data/lib/hanami/validations/version.rb +1 -1
- data/lib/hanami/validations.rb +35 -311
- data/lib/hanami/validator.rb +9 -0
- metadata +18 -60
- 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
- data/lib/hanami-validations.rb +0 -3
data/README.md
CHANGED
@@ -1,15 +1,19 @@
|
|
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://github.com/hanami/validations/actions?query=workflow%3Aci+branch%3Amain)
|
9
|
+
[](https://codecov.io/gh/hanami/validations)
|
10
10
|
[](https://depfu.com/github/hanami/validations?project=Bundler)
|
11
11
|
[](http://inch-ci.org/github/hanami/validations)
|
12
12
|
|
13
|
+
## Version
|
14
|
+
|
15
|
+
**This branch contains the code for `hanami-validations` 2.x.**
|
16
|
+
|
13
17
|
## Contact
|
14
18
|
|
15
19
|
* Home page: http://hanamirb.org
|
@@ -23,14 +27,14 @@ Validations mixin for Ruby objects
|
|
23
27
|
|
24
28
|
## Rubies
|
25
29
|
|
26
|
-
__Hanami::Validations__ supports Ruby (MRI)
|
30
|
+
__Hanami::Validations__ supports Ruby (MRI) 3.0+
|
27
31
|
|
28
32
|
## Installation
|
29
33
|
|
30
34
|
Add this line to your application's Gemfile:
|
31
35
|
|
32
36
|
```ruby
|
33
|
-
gem
|
37
|
+
gem "hanami-validations"
|
34
38
|
```
|
35
39
|
|
36
40
|
And then execute:
|
@@ -47,733 +51,426 @@ $ gem install hanami-validations
|
|
47
51
|
|
48
52
|
## Usage
|
49
53
|
|
50
|
-
|
51
|
-
|
52
|
-
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 `|`).
|
53
|
-
|
54
|
-
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`.
|
55
|
-
|
56
|
-
```ruby
|
57
|
-
class Signup
|
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
|
54
|
+
[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.
|
55
|
+
`hanami-validations` is based on [`dry-validation`](https://dry-rb.org/gems/dry-validation/master/), for this reason the documentation explains the basics of this gem, but for advanced topics, it links to `dry-validation` docs.
|
74
56
|
|
75
|
-
|
57
|
+
### Overview
|
76
58
|
|
77
|
-
|
59
|
+
The main object provided by this gem is `Hanami::Validator`.
|
60
|
+
It providers a powerful DSL to define a validation contract, which is made of a schema and optional rules.
|
78
61
|
|
79
|
-
|
80
|
-
|
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
|
62
|
+
A validation **schema** is a set of steps that filters, coerces, and checks the validity of incoming data.
|
63
|
+
Validation **rules** are a set of directives, to check if business rules are respected.
|
100
64
|
|
101
|
-
|
102
|
-
That's because the context of execution of these validations isn't a plain lambda, but something richer.
|
65
|
+
Only when the input is formally valid (according to the **schema**), validation **rules** are checked.
|
103
66
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
⚠ **For this reason, we don't allow any arbitrary Ruby code to be executed, but only well defined predicates.** ⚠
|
108
|
-
|
109
|
-
### Predicates
|
67
|
+
```ruby
|
68
|
+
# frozen_string_literal: true
|
110
69
|
|
111
|
-
|
70
|
+
require "hanami/validations"
|
112
71
|
|
113
|
-
|
72
|
+
class SignupValidator < Hanami::Validator
|
73
|
+
schema do
|
74
|
+
required(:email).value(:string)
|
75
|
+
required(:age).value(:integer)
|
76
|
+
end
|
114
77
|
|
115
|
-
|
78
|
+
rule(:age) do
|
79
|
+
key.failure("must be greater than 18") if value < 18
|
80
|
+
end
|
81
|
+
end
|
116
82
|
|
117
|
-
|
83
|
+
validator = SignupValidator.new
|
118
84
|
|
119
|
-
|
120
|
-
|
121
|
-
```
|
85
|
+
result = validator.call(email: "user@hanamirb.test", age: 37)
|
86
|
+
result.success? # => true
|
122
87
|
|
123
|
-
|
88
|
+
result = validator.call(email: "user@hanamirb.test", age: "foo")
|
89
|
+
result.success? # => false
|
90
|
+
result.errors.to_h # => {:age=>["must be an integer"]}
|
124
91
|
|
125
|
-
|
126
|
-
|
92
|
+
result = validator.call(email: "user@hanamirb.test", age: 17)
|
93
|
+
puts result.success? # => false
|
94
|
+
puts result.errors.to_h # => {:age=>["must be greater than 18"]}
|
127
95
|
```
|
128
96
|
|
129
|
-
|
97
|
+
### Schemas
|
130
98
|
|
131
|
-
|
99
|
+
A basic schema doesn't apply data coercion, input must already have the right Ruby types.
|
132
100
|
|
133
101
|
```ruby
|
134
|
-
|
135
|
-
```
|
102
|
+
# frozen_string_literal: true
|
136
103
|
|
137
|
-
|
138
|
-
|
139
|
-
This predicate tests if the input is equal to a given value.
|
140
|
-
|
141
|
-
```ruby
|
142
|
-
required(:magic_number) { eql?(23) }
|
143
|
-
```
|
104
|
+
require "hanami/validations"
|
144
105
|
|
145
|
-
|
106
|
+
class SignupValidator < Hanami::Validator
|
107
|
+
schema do
|
108
|
+
required(:email).value(:string)
|
109
|
+
required(:age).value(:integer)
|
110
|
+
end
|
111
|
+
end
|
146
112
|
|
147
|
-
|
113
|
+
validator = SignupValidator.new
|
148
114
|
|
149
|
-
|
115
|
+
result = validator.call(email: "user@hanamirb.test", age: 37)
|
116
|
+
puts result.success? # => true
|
150
117
|
|
151
|
-
|
152
|
-
|
118
|
+
result = validator.call(email: "user@hanamirb.test", age: "37")
|
119
|
+
puts result.success? # => false
|
120
|
+
puts result.errors.to_h # => {:age=>["must be an integer"]}
|
153
121
|
```
|
154
122
|
|
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
|
-
```
|
123
|
+
### Params
|
165
124
|
|
166
|
-
|
125
|
+
When used in _params mode_, a schema applies data coercion, before to run validation checks.
|
167
126
|
|
168
|
-
This
|
127
|
+
This is designed for Web form/HTTP params.
|
169
128
|
|
170
129
|
```ruby
|
171
|
-
|
172
|
-
```
|
173
|
-
|
174
|
-
#### Greater Than Equal
|
175
|
-
|
176
|
-
This is an _open boundary_ variation of `gt?`. It checks if an input is **greater than or equal** of a given number.
|
130
|
+
# frozen_string_literal: true
|
177
131
|
|
178
|
-
|
179
|
-
|
180
|
-
```
|
132
|
+
require "bundler/setup"
|
133
|
+
require "hanami/validations"
|
181
134
|
|
182
|
-
|
135
|
+
class SignupValidator < Hanami::Validator
|
136
|
+
params do
|
137
|
+
required(:email).value(:string)
|
138
|
+
required(:age).value(:integer)
|
139
|
+
end
|
140
|
+
end
|
183
141
|
|
184
|
-
|
142
|
+
validator = SignupValidator.new
|
185
143
|
|
186
|
-
|
187
|
-
|
144
|
+
result = validator.call(email: "user@hanamirb.test", age: "37")
|
145
|
+
puts result.success? # => true
|
146
|
+
puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37}
|
188
147
|
```
|
189
148
|
|
190
|
-
|
149
|
+
### JSON
|
191
150
|
|
192
|
-
|
151
|
+
When used in _JSON mode_, data coercions are still applied, but they follow different policies.
|
152
|
+
For instance, because JSON supports integers, strings won't be coerced into integers.
|
193
153
|
|
194
154
|
```ruby
|
195
|
-
|
196
|
-
```
|
155
|
+
# frozen_string_literal: true
|
197
156
|
|
198
|
-
|
157
|
+
require "hanami/validations"
|
199
158
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
required(:age)
|
204
|
-
|
159
|
+
class SignupValidator < Hanami::Validator
|
160
|
+
json do
|
161
|
+
required(:email).value(:string)
|
162
|
+
required(:age).value(:integer)
|
163
|
+
end
|
164
|
+
end
|
205
165
|
|
206
|
-
|
166
|
+
validator = SignupValidator.new
|
207
167
|
|
208
|
-
|
168
|
+
result = validator.call(email: "user@hanamirb.test", age: 37)
|
169
|
+
puts result.success? # => true
|
170
|
+
puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37}
|
209
171
|
|
210
|
-
|
211
|
-
|
212
|
-
required(:languages) { filled? } # collection
|
172
|
+
result = validator.call(email: "user@hanamirb.test", age: "37")
|
173
|
+
puts result.success? # => false
|
213
174
|
```
|
214
175
|
|
215
|
-
|
176
|
+
### Whitelisting
|
216
177
|
|
217
|
-
|
178
|
+
Unknown keys from incoming data are filtered out:
|
218
179
|
|
219
180
|
```ruby
|
220
|
-
|
221
|
-
```
|
181
|
+
# frozen_string_literal: true
|
222
182
|
|
223
|
-
|
183
|
+
require "hanami/validations"
|
224
184
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
#### None
|
185
|
+
class SignupValidator < Hanami::Validator
|
186
|
+
schema do
|
187
|
+
required(:email).value(:string)
|
188
|
+
end
|
189
|
+
end
|
232
190
|
|
233
|
-
|
191
|
+
validator = SignupValidator.new
|
234
192
|
|
235
|
-
|
236
|
-
|
193
|
+
result = validator.call(email: "user@hanamirb.test", foo: "bar")
|
194
|
+
puts result.success? # => true
|
195
|
+
puts result.to_h # => {:email=>"user@hanamirb.test"}
|
237
196
|
```
|
238
197
|
|
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.
|
198
|
+
### Custom Types
|
242
199
|
|
243
200
|
```ruby
|
244
|
-
|
245
|
-
required(:password) { size?(8..32) } # range
|
246
|
-
```
|
201
|
+
# frozen_string_literal: true
|
247
202
|
|
248
|
-
|
203
|
+
require "hanami/validations"
|
249
204
|
|
250
|
-
|
251
|
-
|
252
|
-
```
|
205
|
+
module Types
|
206
|
+
include Dry::Types()
|
253
207
|
|
254
|
-
|
208
|
+
StrippedString = Types::String.constructor(&:strip)
|
209
|
+
end
|
255
210
|
|
256
|
-
|
211
|
+
class SignupValidator < Hanami::Validator
|
212
|
+
params do
|
213
|
+
required(:email).value(Types::StrippedString)
|
214
|
+
required(:age).value(:integer)
|
215
|
+
end
|
216
|
+
end
|
257
217
|
|
258
|
-
|
259
|
-
MEGABYTE = 1024 ** 2
|
218
|
+
validator = SignupValidator.new
|
260
219
|
|
261
|
-
|
220
|
+
result = validator.call(email: " user@hanamirb.test ", age: "37")
|
221
|
+
puts result.success? # => true
|
222
|
+
puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37}
|
262
223
|
```
|
263
224
|
|
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
|
225
|
+
### Rules
|
271
226
|
|
272
|
-
|
227
|
+
Rules are performing a set of domain-specific validation checks.
|
228
|
+
Rules are executed only after the validations from the schema are satisfied.
|
273
229
|
|
274
230
|
```ruby
|
275
|
-
|
231
|
+
# frozen_string_literal: true
|
276
232
|
|
277
|
-
|
278
|
-
include Hanami::Validations
|
233
|
+
require "hanami/validations"
|
279
234
|
|
280
|
-
|
281
|
-
|
235
|
+
class EventValidator < Hanami::Validator
|
236
|
+
params do
|
237
|
+
required(:start_date).value(:date)
|
282
238
|
end
|
283
239
|
|
284
|
-
|
285
|
-
|
240
|
+
rule(:start_date) do
|
241
|
+
key.failure("must be in the future") if value <= Date.today
|
286
242
|
end
|
287
243
|
end
|
288
|
-
```
|
289
244
|
|
290
|
-
|
245
|
+
validator = EventValidator.new
|
291
246
|
|
292
|
-
|
247
|
+
result = validator.call(start_date: "foo")
|
248
|
+
puts result.success? # => false
|
249
|
+
puts result.errors.to_h # => {:start_date=>["must be a date"]}
|
293
250
|
|
294
|
-
|
295
|
-
|
251
|
+
result = validator.call(start_date: Date.today)
|
252
|
+
puts result.success? # => false
|
253
|
+
puts result.errors.to_h # => {:start_date=>["must be in the future"]}
|
296
254
|
|
297
|
-
|
298
|
-
|
255
|
+
result = validator.call(start_date: Date.today + 1)
|
256
|
+
puts result.success? # => true
|
257
|
+
puts result.to_h # => {:start_date=>#<Date: 2019-07-03 ((2458668j,0s,0n),+0s,2299161j)>}
|
258
|
+
```
|
299
259
|
|
300
|
-
|
260
|
+
Learn more about rules: https://dry-rb.org/gems/dry-validation/master/rules/
|
301
261
|
|
302
|
-
|
303
|
-
current.match(/.../)
|
304
|
-
end
|
305
|
-
end
|
306
|
-
```
|
262
|
+
### Inheritance
|
307
263
|
|
308
|
-
|
264
|
+
Schema and rules validations can be inherited and used by subclasses
|
309
265
|
|
310
266
|
```ruby
|
311
|
-
|
312
|
-
require_relative 'my_predicates'
|
267
|
+
# frozen_string_literal: true
|
313
268
|
|
314
|
-
|
315
|
-
include Hanami::Validations
|
316
|
-
predicates MyPredicates
|
269
|
+
require "hanami/validations"
|
317
270
|
|
318
|
-
|
319
|
-
|
271
|
+
class ApplicationValidator < Hanami::Validator
|
272
|
+
params do
|
273
|
+
optional(:_csrf_token).filled(:string)
|
320
274
|
end
|
321
275
|
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
276
|
|
331
|
-
class
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
optional(:referral) { ... }
|
277
|
+
class SignupValidator < ApplicationValidator
|
278
|
+
params do
|
279
|
+
required(:user).hash do
|
280
|
+
required(:email).filled(:string)
|
281
|
+
end
|
337
282
|
end
|
338
283
|
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
|
-
|
345
|
-
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?
|
346
|
-
|
347
|
-
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?`.
|
348
|
-
|
349
|
-
```ruby
|
350
|
-
required(:age) { type?(Integer) & gteq?(18) }
|
351
|
-
```
|
352
|
-
|
353
|
-
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.
|
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:
|
358
|
-
|
359
|
-
* `Array` (aliased as `array?`)
|
360
|
-
* `BigDecimal` (aliased as `decimal?`)
|
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?`)
|
369
|
-
|
370
|
-
For each supported type, there a convenient predicate that acts as an alias. For instance, the two lines of code below are **equivalent**.
|
371
284
|
|
372
|
-
|
373
|
-
required(:age) { type?(Integer) }
|
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
|
285
|
+
validator = SignupValidator.new
|
383
286
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
# expands to
|
388
|
-
# required(:age) { filled? }
|
389
|
-
|
390
|
-
required(:age).filled
|
287
|
+
result = validator.call(user: { email: "user@hanamirb.test" }, _csrf_token: "abc123")
|
288
|
+
puts result.success? # => true
|
289
|
+
puts result.to_h # => {:user=>{:email=>"user@hanamirb.test"}, :_csrf_token=>"abc123"}
|
391
290
|
```
|
392
291
|
|
393
|
-
|
394
|
-
# expands to
|
395
|
-
# required(:age) { filled? & type?(Integer) }
|
292
|
+
### Messages
|
396
293
|
|
397
|
-
|
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.
|
294
|
+
Failure messages can be hardcoded or refer to a message template system.
|
295
|
+
`hanami-validations` supports natively a default YAML based message template system, or alternatively, `i18n` gem.
|
408
296
|
|
409
|
-
|
410
|
-
|
411
|
-
To use when a value can be nil:
|
297
|
+
We have already seen rule failures set with hardcoded messages, here's an example of how to use keys to refer to interpolated messages.
|
412
298
|
|
413
299
|
```ruby
|
414
|
-
#
|
415
|
-
# required(:age) { none? | int? }
|
416
|
-
|
417
|
-
required(:age).maybe(:int?)
|
418
|
-
```
|
419
|
-
|
420
|
-
In the example above `age` can be `nil`, but if we send the value, it **must** be an integer.
|
421
|
-
|
422
|
-
#### Each
|
300
|
+
# frozen_string_literal: true
|
423
301
|
|
424
|
-
|
425
|
-
|
426
|
-
```ruby
|
427
|
-
# expands to
|
428
|
-
# required(:tags) { array? { each { str? } } }
|
302
|
+
require "hanami/validations"
|
429
303
|
|
430
|
-
|
304
|
+
class ApplicationValidator < Hanami::Validator
|
305
|
+
config.messages.top_namespace = "bookshelf"
|
306
|
+
config.messages.load_paths << "config/errors.yml"
|
307
|
+
end
|
431
308
|
```
|
432
309
|
|
433
|
-
In the
|
310
|
+
In the `ApplicationValidator` there is defined the application namespace (`"bookshelf"`), which is the root of the messages file.
|
311
|
+
Below that top name, there is the key `errors`. Everything that is nested here is accessible by the validations.
|
434
312
|
|
313
|
+
There are two ways to organize messages:
|
435
314
|
|
436
|
-
|
315
|
+
1. Right below `errors`. This is for **general purposes** error messages (e.g. `bookshelf` => `errors` => `taken`)
|
316
|
+
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
317
|
|
438
|
-
|
318
|
+
Our **suggestion** is to start with **specific** messages and see if there is a need to generalize them.
|
439
319
|
|
440
|
-
```
|
441
|
-
|
320
|
+
```yaml
|
321
|
+
# config/errors.yml
|
322
|
+
en:
|
323
|
+
bookshelf:
|
324
|
+
errors:
|
325
|
+
taken: "oh noes, it's already taken"
|
326
|
+
network: "there is a network error (%{code})"
|
327
|
+
rules:
|
328
|
+
age:
|
329
|
+
invalid: "must be greater than 18"
|
330
|
+
email:
|
331
|
+
invalid: "not a valid email"
|
442
332
|
```
|
443
333
|
|
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:
|
334
|
+
#### General purpose messages
|
466
335
|
|
467
336
|
```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
|
337
|
+
class SignupValidator < ApplicationValidator
|
338
|
+
schema do
|
339
|
+
required(:username).filled(:string)
|
487
340
|
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
|
-
|
499
|
-
* If `remote` is missing or false, then `location` must be filled
|
500
|
-
* If `remote` is true, then `location` must be omitted
|
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
341
|
|
506
|
-
|
507
|
-
|
508
|
-
required(:customer).schema do
|
509
|
-
required(:email) { … }
|
510
|
-
required(:name) { … }
|
511
|
-
# other validations …
|
342
|
+
rule(:username) do
|
343
|
+
key.failure(:taken) if values[:username] == "jodosha"
|
512
344
|
end
|
513
345
|
end
|
514
|
-
```
|
515
346
|
|
516
|
-
|
347
|
+
validator = SignupValidator.new
|
517
348
|
|
518
|
-
|
519
|
-
|
520
|
-
required(:customer).schema do
|
521
|
-
# other validations …
|
349
|
+
result = validator.call(username: "foo")
|
350
|
+
puts result.success? # => true
|
522
351
|
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
end
|
527
|
-
end
|
528
|
-
end
|
352
|
+
result = validator.call(username: "jodosha")
|
353
|
+
puts result.success? # => false
|
354
|
+
puts result.errors.to_h # => {:username=>["oh noes, it's already taken"]}
|
529
355
|
```
|
530
356
|
|
531
|
-
|
532
|
-
|
533
|
-
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.
|
357
|
+
#### Specific messages
|
534
358
|
|
535
|
-
|
359
|
+
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
360
|
|
537
361
|
```ruby
|
538
|
-
class
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
required(:street) { … }
|
362
|
+
class SignupValidator < ApplicationValidator
|
363
|
+
schema do
|
364
|
+
required(:email).filled(:string)
|
365
|
+
required(:age).filled(:integer)
|
543
366
|
end
|
544
|
-
end
|
545
|
-
```
|
546
|
-
|
547
|
-
This validator can be reused by other validators.
|
548
|
-
|
549
|
-
```ruby
|
550
|
-
class CustomerValidator
|
551
|
-
include Hanami::Validations
|
552
367
|
|
553
|
-
|
554
|
-
|
555
|
-
required(:address).schema(AddressValidator)
|
368
|
+
rule(:email) do
|
369
|
+
key.failure(:invalid) unless values[:email] =~ /@/
|
556
370
|
end
|
557
|
-
end
|
558
|
-
```
|
559
371
|
|
560
|
-
|
561
|
-
|
562
|
-
```ruby
|
563
|
-
class OrderValidator
|
564
|
-
include Hanami::Validations
|
565
|
-
|
566
|
-
validations do
|
567
|
-
required(:number) { … }
|
568
|
-
required(:customer).schema(CustomerValidator)
|
372
|
+
rule(:age) do
|
373
|
+
key.failure(:invalid) if values[:age] < 18
|
569
374
|
end
|
570
375
|
end
|
571
|
-
```
|
572
|
-
|
573
|
-
In the end, `OrderValidator` is able to validate a complex data structure like this:
|
574
|
-
|
575
|
-
```ruby
|
576
|
-
{
|
577
|
-
number: "123",
|
578
|
-
customer: {
|
579
|
-
email: "user@example.com",
|
580
|
-
address: {
|
581
|
-
city: "Rome"
|
582
|
-
}
|
583
|
-
}
|
584
|
-
}
|
585
|
-
```
|
586
|
-
|
587
|
-
### Whitelisting
|
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
376
|
|
595
|
-
|
377
|
+
validator = SignupValidator.new
|
596
378
|
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
```ruby
|
602
|
-
result = OrderValidator.new({}).validate
|
603
|
-
result.success? # => false
|
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 }
|
379
|
+
result = validator.call(email: "foo", age: 17)
|
380
|
+
puts result.success? # => false
|
381
|
+
puts result.errors.to_h # => {:email=>["not a valid email"], :age=>["must be greater than 18"]}
|
633
382
|
```
|
634
383
|
|
635
|
-
|
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.
|
384
|
+
#### Extra information
|
645
385
|
|
646
|
-
|
386
|
+
The interpolation mechanism, accepts extra, arbitrary information expressed as a `Hash` (e.g. `code: "123"`)
|
647
387
|
|
648
388
|
```ruby
|
649
|
-
class
|
650
|
-
|
651
|
-
|
652
|
-
predicate :email?, message: 'must be an email' do |current|
|
653
|
-
# ...
|
389
|
+
class RefundValidator < ApplicationValidator
|
390
|
+
schema do
|
391
|
+
required(:refunded_code).filled(:string)
|
654
392
|
end
|
655
393
|
|
656
|
-
|
657
|
-
|
658
|
-
required(:age).filled(:int?, gt?: 18)
|
394
|
+
rule(:refunded_code) do
|
395
|
+
key.failure(:network, code: "123") if values[:refunded_code] == "error"
|
659
396
|
end
|
660
397
|
end
|
661
398
|
|
662
|
-
|
399
|
+
validator = RefundValidator.new
|
663
400
|
|
664
|
-
result.
|
665
|
-
result.
|
666
|
-
result.
|
401
|
+
result = validator.call(refunded_code: "error")
|
402
|
+
puts result.success? # => false
|
403
|
+
puts result.errors.to_h # => {:refunded_code=>["there is a network error (123)"]}
|
667
404
|
```
|
668
405
|
|
669
|
-
|
406
|
+
Learn more about messages: https://dry-rb.org/gems/dry-validation/master/messages/
|
670
407
|
|
671
|
-
|
408
|
+
### External dependencies
|
672
409
|
|
673
|
-
|
674
|
-
|
675
|
-
en:
|
676
|
-
errors:
|
677
|
-
email?: "must be an email"
|
678
|
-
```
|
679
|
-
|
680
|
-
To be used like this:
|
410
|
+
If the validator needs to plug one or more objects to run the validations, there is a DSL to do so: `:option`.
|
411
|
+
When the validator is instantiated, the declared dependencies must be passed.
|
681
412
|
|
682
413
|
```ruby
|
683
|
-
|
684
|
-
include Hanami::Validations
|
685
|
-
messages_path 'config/messages.yml'
|
414
|
+
# frozen_string_literal: true
|
686
415
|
|
687
|
-
|
688
|
-
# ...
|
689
|
-
end
|
416
|
+
require "hanami/validations"
|
690
417
|
|
691
|
-
|
692
|
-
|
693
|
-
|
418
|
+
class AddressValidator
|
419
|
+
def valid?(value)
|
420
|
+
value.match(/Rome/)
|
694
421
|
end
|
695
422
|
end
|
696
423
|
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
#### Custom Error Messages
|
424
|
+
class DeliveryValidator < Hanami::Validator
|
425
|
+
option :address_validator
|
701
426
|
|
702
|
-
|
427
|
+
schema do
|
428
|
+
required(:address).filled(:string)
|
429
|
+
end
|
703
430
|
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
email?: "must be an email"
|
431
|
+
rule(:address) do
|
432
|
+
key.failure("not a valid address") unless address_validator.valid?(values[:address])
|
433
|
+
end
|
434
|
+
end
|
709
435
|
|
710
|
-
|
711
|
-
signup:
|
712
|
-
age:
|
713
|
-
gt?: "must be an adult"
|
436
|
+
validator = DeliveryValidator.new(address_validator: AddressValidator.new)
|
714
437
|
|
438
|
+
result = validator.call(address: "foo")
|
439
|
+
puts result.success? # => false
|
440
|
+
puts result.errors.to_h # => {:address=>["not a valid address"]}
|
715
441
|
```
|
716
442
|
|
717
|
-
|
443
|
+
Read more about external dependencies: https://dry-rb.org/gems/dry-validation/master/external-dependencies/
|
718
444
|
|
719
|
-
|
720
|
-
result = SignupValidator.new(email: 'foo', age: 1).validate
|
445
|
+
### Mixin
|
721
446
|
|
722
|
-
|
723
|
-
|
724
|
-
```
|
725
|
-
|
726
|
-
##### Custom namespace
|
447
|
+
`hanami-validations` 1.x used to ship a mixin `Hanami::Validations` to be included in classes to provide validation rules.
|
448
|
+
The 2.x series, still ships this mixin, but it will be probably removed in 3.x.
|
727
449
|
|
728
|
-
|
450
|
+
```ruby
|
451
|
+
# frozen_string_literal: true
|
729
452
|
|
730
|
-
|
453
|
+
require "hanami/validations"
|
731
454
|
|
732
|
-
|
733
|
-
class SignupValidator
|
455
|
+
class UserValidator
|
734
456
|
include Hanami::Validations
|
735
457
|
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
# ...
|
458
|
+
validations do
|
459
|
+
required(:number).filled(:integer, eql?: 23)
|
460
|
+
end
|
740
461
|
end
|
741
|
-
```
|
742
462
|
|
743
|
-
|
463
|
+
result = UserValidator.new(number: 23).validate
|
744
464
|
|
745
|
-
|
746
|
-
#
|
747
|
-
|
748
|
-
# ...
|
749
|
-
rules:
|
750
|
-
my_signup:
|
751
|
-
age:
|
752
|
-
gt?: "must be an adult"
|
753
|
-
|
754
|
-
```
|
465
|
+
puts result.success? # => true
|
466
|
+
puts result.to_h # => {:number=>23}
|
467
|
+
puts result.errors.to_h # => {}
|
755
468
|
|
756
|
-
|
469
|
+
result = UserValidator.new(number: 11).validate
|
757
470
|
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
```ruby
|
762
|
-
class SignupValidator
|
763
|
-
include Hanami::Validations
|
764
|
-
|
765
|
-
messages :i18n
|
766
|
-
|
767
|
-
# ...
|
768
|
-
end
|
769
|
-
```
|
770
|
-
|
771
|
-
```yaml
|
772
|
-
# config/locales/en.yml
|
773
|
-
en:
|
774
|
-
errors:
|
775
|
-
signup:
|
776
|
-
# ...
|
471
|
+
puts result.success? # => true
|
472
|
+
puts result.to_h # => {:number=>21}
|
473
|
+
puts result.errors.to_h # => {:number=>["must be equal to 23"]}
|
777
474
|
```
|
778
475
|
|
779
476
|
## FAQs
|
@@ -784,10 +481,7 @@ Please remember that **uniqueness validation is a huge race condition between ap
|
|
784
481
|
|
785
482
|
Please read more at: [The Perils of Uniqueness Validations](http://robots.thoughtbot.com/the-perils-of-uniqueness-validations).
|
786
483
|
|
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.
|
484
|
+
If you need to implement it, please use the External dependencies feature (see above).
|
791
485
|
|
792
486
|
## Contributing
|
793
487
|
|