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