hanami-validations 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +537 -326
- data/hanami-validations.gemspec +6 -5
- data/lib/hanami-validations.rb +1 -1
- data/lib/hanami/validations.rb +259 -208
- data/lib/hanami/validations/form.rb +47 -0
- data/lib/hanami/validations/inline_predicate.rb +46 -0
- data/lib/hanami/validations/namespace.rb +65 -0
- data/lib/hanami/validations/predicates.rb +45 -0
- data/lib/hanami/validations/version.rb +1 -1
- metadata +24 -16
- data/lib/hanami/validations/attribute.rb +0 -252
- data/lib/hanami/validations/attribute_definer.rb +0 -526
- data/lib/hanami/validations/blank_value_checker.rb +0 -55
- data/lib/hanami/validations/coercions.rb +0 -31
- data/lib/hanami/validations/error.rb +0 -95
- data/lib/hanami/validations/errors.rb +0 -155
- data/lib/hanami/validations/nested_attributes.rb +0 -22
- data/lib/hanami/validations/validation_set.rb +0 -81
- data/lib/hanami/validations/validator.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82f7ed42843fd9e538ad62d2657868bc1ae04f27
|
4
|
+
data.tar.gz: 1085aba298d69ac53d0b0044b668c975ed27589c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6a1eb54deef5dbcef139dffea132a3b78c31573835d44b3bb296341627affc3d880a7b06a54f3d948f890653f320b596e333720388b2e84adc60914c00c576b9
|
7
|
+
data.tar.gz: 9e3e32580c50d69b0bf5d7d59a8e313596bff4ec8c8efa2ae39c6991db22695c5d24bb9244ed81baf472fed260fbde1098e30cff8d7b8bf5c92b38c24997550a
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,28 @@
|
|
1
1
|
# Hanami::Validations
|
2
2
|
Validations mixin for Ruby objects
|
3
3
|
|
4
|
+
## v0.6.0 - 2016-07-22
|
5
|
+
### Added
|
6
|
+
- [Luca Guidi] Predicates syntax
|
7
|
+
- [Luca Guidi] Custom predicates
|
8
|
+
- [Luca Guidi] Inline predicates
|
9
|
+
- [Luca Guidi] Shared predicates
|
10
|
+
- [Luca Guidi] High level rules
|
11
|
+
- [Luca Guidi] Error messages with I18n support (`i18n` gem)
|
12
|
+
- [Luca Guidi] Introduced `Hanami::Validations#validate`, which returns a result object.
|
13
|
+
- [Luca Guidi] Introduced `Hanami::Validations::Form` mixin, which must be used when input comes from HTTP params or web forms.
|
14
|
+
|
15
|
+
### Fixed
|
16
|
+
– [Luca Guidi] Ensure to threat blank values as `nil`
|
17
|
+
|
18
|
+
### Changed
|
19
|
+
– [Luca Guidi] Drop support for Ruby 2.0, 2.1 and Rubinius. Official support for JRuby 9.0.5.0+.
|
20
|
+
- [Luca Guidi] Validations must be wrapped in `.validations` block.
|
21
|
+
- [Luca Guidi] Removed `.attribute` DSL. A validator doesn't create accessors (getters/setters) for validated keys.
|
22
|
+
- [Luca Guidi] Removed `Hanami::Validations#valid?` in favor of `#validate`.
|
23
|
+
- [Luca Guidi] Error messages are accessible via result object. Eg. `result.errors` or `result.errors(full: true)`
|
24
|
+
- [Luca Guidi] Coerced and sanitized data is accessible via result object. Eg. `result.output`
|
25
|
+
|
4
26
|
## v0.5.0 - 2016-01-22
|
5
27
|
### Changed
|
6
28
|
- [Luca Guidi] Renamed the project
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Hanami::Validations
|
2
2
|
|
3
|
-
Validations
|
3
|
+
Validations mixin for Ruby objects
|
4
4
|
|
5
5
|
## Status
|
6
6
|
|
@@ -22,7 +22,7 @@ Validations mixins for objects
|
|
22
22
|
|
23
23
|
## Rubies
|
24
24
|
|
25
|
-
__Hanami::Validations__ supports Ruby (MRI) 2
|
25
|
+
__Hanami::Validations__ supports Ruby (MRI) 2+ and JRuby 9.0.5.0+.
|
26
26
|
|
27
27
|
## Installation
|
28
28
|
|
@@ -34,204 +34,237 @@ gem 'hanami-validations'
|
|
34
34
|
|
35
35
|
And then execute:
|
36
36
|
|
37
|
-
|
37
|
+
```shell
|
38
|
+
$ bundle
|
39
|
+
```
|
38
40
|
|
39
41
|
Or install it yourself as:
|
40
42
|
|
41
|
-
|
43
|
+
```shell
|
44
|
+
$ gem install hanami-validations
|
45
|
+
```
|
42
46
|
|
43
47
|
## Usage
|
44
48
|
|
45
|
-
`Hanami::Validations` is a set of
|
49
|
+
`Hanami::Validations` is a mixin that, once included by an object, adds lightweight set of validations to it.
|
46
50
|
|
47
|
-
|
51
|
+
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 `|`).
|
48
52
|
|
49
|
-
|
50
|
-
|
51
|
-
It defines an initializer, whose attributes can be passed as a hash.
|
52
|
-
All unknown values are ignored, which is useful for whitelisting attributes.
|
53
|
+
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`.
|
53
54
|
|
54
55
|
```ruby
|
55
|
-
|
56
|
-
|
57
|
-
class Person
|
56
|
+
class Signup
|
58
57
|
include Hanami::Validations
|
59
58
|
|
60
|
-
|
61
|
-
|
59
|
+
validations do
|
60
|
+
required(:name) { filled? & str? & size?(3..64) }
|
61
|
+
end
|
62
62
|
end
|
63
63
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
64
|
+
result = Signup.new(name: "Luca").validate
|
65
|
+
result.success? # => true
|
66
|
+
```
|
67
|
+
|
68
|
+
There is more that `Hanami::Validations` can do: **type safety**, **composition**, **complex data structures**, **built-in and custom predicates**.
|
69
|
+
|
70
|
+
But before to dive into advanced topics, we need to understand the basics of _boolean logic_.
|
71
|
+
|
72
|
+
### Boolean Logic
|
73
|
+
|
74
|
+
When we check data, we expect only two outcomes: an input can be valid or not. No grey areas, nor fuzzy results. It’s white or black, 1 or 0, `true` or `false` and _boolean logic_ is the perfect tool to express these two states. Indeed, a Ruby _boolean expression_ can only return `true` or `false`.
|
75
|
+
|
76
|
+
To better recognise the pattern, let’s get back to the example above. This time we will map the natural language rules with programming language rules.
|
77
|
+
|
78
|
+
```
|
79
|
+
A name must be filled and be a string and its size must be included between 3 and 64.
|
80
|
+
👇 👇 👇 👇 👇 👇 👇 👇
|
81
|
+
required(:name) { filled? & str? & size? (3 .. 64) }
|
82
|
+
|
68
83
|
```
|
69
84
|
|
70
|
-
|
85
|
+
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.
|
71
86
|
|
72
|
-
|
87
|
+
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 `&`.
|
88
|
+
|
89
|
+
#### Logic Operators
|
90
|
+
|
91
|
+
We support four logic operators:
|
92
|
+
|
93
|
+
* `&` (aliased as `and`) for _conjunction_
|
94
|
+
* `|` (aliased as `or`) for _disjunction_
|
95
|
+
* `>` (aliased as `then`) for _implication_
|
96
|
+
* `^` (aliased as `xor`) for _exclusive disjunction_
|
97
|
+
|
98
|
+
#### Context Of Execution
|
99
|
+
|
100
|
+
**Please notice that we used `&` over Ruby's `&&` keyword.**
|
101
|
+
That's because the context of execution of these validations isn't a plain lambda, but something richer.
|
102
|
+
|
103
|
+
For real world projects, we want to support common scenarios without the need of reinventing the wheel ourselves. Scenarios like _password confirmation_, _size check_ are already prepackaged with `Hanami::Validations`.
|
104
|
+
|
105
|
+
|
106
|
+
⚠ **For this reason, we don't allow any arbitrary Ruby code to be executed, but only well defined predicates.** ⚠
|
107
|
+
|
108
|
+
### Predicates
|
109
|
+
|
110
|
+
To meet our needs, `Hanami::Validations` has an extensive collection of **built-in** predicates. **A predicate is the expression of a business requirement** (e.g. _size greater than_). The chain of several predicates determines if input data is valid or not.
|
111
|
+
|
112
|
+
We already met `filled?` and `size?`, now let’s introduce the rest of them. They capture **common use cases with web forms**.
|
113
|
+
|
114
|
+
### Array
|
115
|
+
|
116
|
+
It checks if the the given value is an array, and iterates through its elements to perform checks on each of them.
|
73
117
|
|
74
118
|
```ruby
|
75
|
-
|
119
|
+
required(:codes) { array? { each { int? } } }
|
120
|
+
```
|
76
121
|
|
77
|
-
|
78
|
-
include Hanami::Validations
|
122
|
+
This example checks if `codes` is an array and if all the elements are integers.
|
79
123
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
124
|
+
#### Emptiness
|
125
|
+
|
126
|
+
It checks if the given value is empty or not. It is designed to works with strings and collections (array and hash).
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
required(:tags) { empty? }
|
130
|
+
```
|
131
|
+
|
132
|
+
#### Equality
|
85
133
|
|
86
|
-
|
87
|
-
Person.new(name: '').valid? # < true
|
88
|
-
Person.new(skills: '').valid? # < true
|
89
|
-
Person.new(skills: ['ruby', 'hanami']).valid? # < true
|
134
|
+
This predicate tests if the input is equal to a given value.
|
90
135
|
|
91
|
-
|
92
|
-
|
93
|
-
Person.new(keys: {a: :b}, skills: []).valid? # < false
|
136
|
+
```ruby
|
137
|
+
required(:magic_number) { eql?(23) }
|
94
138
|
```
|
95
139
|
|
96
|
-
|
140
|
+
Ruby types are respected: `23` (an integer) is only equal to `23`, and not to `"23"` (a string). See _Type Safety_ section.
|
97
141
|
|
98
|
-
|
142
|
+
#### Exclusion
|
99
143
|
|
100
|
-
|
101
|
-
you can use the following alternative syntax.
|
144
|
+
It checks if the input is **not** included by a given collection. This collection can be an array, a set, a range or any object that responds to `#include?`.
|
102
145
|
|
103
146
|
```ruby
|
104
|
-
|
147
|
+
required(:genre) { excluded_from?(%w(pop dance)) }
|
148
|
+
```
|
105
149
|
|
106
|
-
|
107
|
-
include Hanami::Validations
|
108
|
-
attr_accessor :name, :email
|
150
|
+
#### Format
|
109
151
|
|
110
|
-
|
111
|
-
def initialize(attributes = {})
|
112
|
-
@name, @email = attributes.values_at(:name, :email)
|
113
|
-
end
|
152
|
+
This is a predicate that works with a regular expression to match it against data input.
|
114
153
|
|
115
|
-
|
116
|
-
|
117
|
-
|
154
|
+
```ruby
|
155
|
+
require 'uri'
|
156
|
+
HTTP_FORMAT = URI.regexp(%w(http https))
|
118
157
|
|
119
|
-
|
120
|
-
person.name # => "Luca"
|
121
|
-
person.email # => "me@example.org"
|
158
|
+
required(:url) { format?(HTTP_FORMAT) }
|
122
159
|
```
|
123
160
|
|
124
|
-
|
125
|
-
Ruby objects. It also allows to use Hanami::Validations in combination with
|
126
|
-
**other frameworks**.
|
161
|
+
#### Greater Than
|
127
162
|
|
128
|
-
|
163
|
+
This predicate works with numbers to check if input is **greater than** a given threshold.
|
129
164
|
|
130
|
-
|
165
|
+
```ruby
|
166
|
+
required(:age) { gt?(18) }
|
167
|
+
```
|
168
|
+
|
169
|
+
#### Greater Than Equal
|
131
170
|
|
132
|
-
|
171
|
+
This is an _open boundary_ variation of `gt?`. It checks if an input is **greater than or equal** of a given number.
|
133
172
|
|
134
173
|
```ruby
|
135
|
-
|
174
|
+
required(:age) { gteq?(19) }
|
175
|
+
```
|
136
176
|
|
137
|
-
|
138
|
-
include Hanami::Validations
|
177
|
+
#### Inclusion
|
139
178
|
|
140
|
-
|
141
|
-
end
|
179
|
+
This predicate is the opposite of `#exclude?`: it verifies if the input is **included** in the given collection.
|
142
180
|
|
143
|
-
|
144
|
-
|
181
|
+
```ruby
|
182
|
+
required(:genre) { included_in?(%w(rock folk)) }
|
183
|
+
```
|
184
|
+
|
185
|
+
#### Less Than
|
145
186
|
|
146
|
-
|
187
|
+
This is the complement of `#gt?`: it checks for **less than** numbers.
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
required(:age) { lt?(7) }
|
147
191
|
```
|
148
192
|
|
149
|
-
|
193
|
+
#### Less Than Equal
|
150
194
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
* `Float`
|
157
|
-
* `Hash`
|
158
|
-
* `Integer`
|
159
|
-
* `Pathname`
|
160
|
-
* `Set`
|
161
|
-
* `String`
|
162
|
-
* `Symbol`
|
163
|
-
* `Time`
|
195
|
+
Similarly to `#gteq?`, this is the _open bounded_ version of `#lt?`: an input is valid if it’s **less than or equal** to a number.
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
required(:age) { lteq?(6) }
|
199
|
+
```
|
164
200
|
|
165
|
-
####
|
201
|
+
#### Filled
|
166
202
|
|
167
|
-
|
168
|
-
The only limitation is that the constructor should have **arity of 1**.
|
203
|
+
It’s a predicate that ensures data input is filled, that means **not** `nil` or blank (`""`) or empty (in case we expect a collection).
|
169
204
|
|
170
205
|
```ruby
|
171
|
-
|
206
|
+
required(:name) { filled? } # string
|
207
|
+
required(:languages) { filled? } # collection
|
208
|
+
```
|
172
209
|
|
173
|
-
|
174
|
-
def initialize(number)
|
175
|
-
@number = number
|
176
|
-
end
|
177
|
-
end
|
210
|
+
#### Minimum Size
|
178
211
|
|
179
|
-
|
180
|
-
end
|
212
|
+
This verifies that the size of the given input is at least of the specified value.
|
181
213
|
|
182
|
-
|
183
|
-
|
214
|
+
```ruby
|
215
|
+
required(:password) { min_size?(12) }
|
216
|
+
```
|
184
217
|
|
185
|
-
|
186
|
-
attribute :date, type: BirthDate
|
187
|
-
end
|
218
|
+
#### Maximum Size
|
188
219
|
|
189
|
-
|
190
|
-
person.valid?
|
220
|
+
This verifies that the size of the given input is at max of the specified value.
|
191
221
|
|
192
|
-
|
193
|
-
|
222
|
+
```ruby
|
223
|
+
required(:name) { max_size?(128) }
|
194
224
|
```
|
195
225
|
|
196
|
-
|
226
|
+
#### None
|
197
227
|
|
198
|
-
|
199
|
-
validations.
|
228
|
+
This verifies if the given input is `nil`. Blank strings (`""`) won’t pass this test and return `false`.
|
200
229
|
|
201
|
-
|
230
|
+
```ruby
|
231
|
+
required(:location) { none? }
|
232
|
+
```
|
202
233
|
|
203
|
-
####
|
234
|
+
#### Size
|
204
235
|
|
205
|
-
|
236
|
+
It checks if the size of input data is: a) exactly the same of a given quantity or b) it falls into a range.
|
206
237
|
|
207
238
|
```ruby
|
208
|
-
|
239
|
+
required(:two_factor_auth_code) { size?(6) } # exact
|
240
|
+
required(:password) { size?(8..32) } # range
|
241
|
+
```
|
209
242
|
|
210
|
-
|
211
|
-
include Hanami::Validations
|
243
|
+
The check works with strings and collections.
|
212
244
|
|
213
|
-
|
214
|
-
|
245
|
+
```ruby
|
246
|
+
required(:answers) { size?(2) } # only 2 answers are allowed
|
247
|
+
```
|
215
248
|
|
216
|
-
|
217
|
-
signup.valid? # => true
|
249
|
+
This predicate works with objects that respond to `#size`. Until now we have seen strings and arrays being analysed by this validation, but there is another interesting usage: files.
|
218
250
|
|
219
|
-
|
220
|
-
signup.valid? # => true
|
251
|
+
When a user uploads a file, the web server sets an instance of `Tempfile`, which responds to `#size`. That means we can validate the weight in bytes of file uploads.
|
221
252
|
|
222
|
-
|
223
|
-
|
253
|
+
```ruby
|
254
|
+
MEGABYTE = 1024 ** 2
|
224
255
|
|
225
|
-
|
226
|
-
signup.valid? # => false
|
256
|
+
required(:avatar) { size?(1..(5 * MEGABYTE)) }
|
227
257
|
```
|
228
258
|
|
229
|
-
|
259
|
+
### Custom Predicates
|
260
|
+
|
261
|
+
We have seen that built-in predicates as an expressive tool to get our job done with common use cases.
|
262
|
+
|
263
|
+
But what if our case is not common? We can define our own custom predicates.
|
230
264
|
|
231
|
-
|
232
|
-
is valid.
|
265
|
+
#### Inline Custom Predicates
|
233
266
|
|
234
|
-
|
267
|
+
If we are facing a really unique validation that don't need to be reused across our code, we can opt for an inline custom predicate:
|
235
268
|
|
236
269
|
```ruby
|
237
270
|
require 'hanami/validations'
|
@@ -239,46 +272,53 @@ require 'hanami/validations'
|
|
239
272
|
class Signup
|
240
273
|
include Hanami::Validations
|
241
274
|
|
242
|
-
|
275
|
+
predicate :url?, message: 'must be an URL' do |current|
|
276
|
+
# ...
|
277
|
+
end
|
278
|
+
|
279
|
+
validations do
|
280
|
+
required(:website) { url? }
|
281
|
+
end
|
243
282
|
end
|
283
|
+
```
|
244
284
|
|
245
|
-
|
246
|
-
signup.valid? # => true
|
285
|
+
#### Global Custom Predicates
|
247
286
|
|
248
|
-
|
249
|
-
signup.valid? # => false
|
250
|
-
```
|
287
|
+
If our goal is to share common used custom predicates, we can include them in a module to use in all our validators:
|
251
288
|
|
252
|
-
|
289
|
+
```ruby
|
290
|
+
require 'hanami/validations'
|
253
291
|
|
254
|
-
|
255
|
-
|
292
|
+
module MyPredicates
|
293
|
+
include Hanami::Validations::Predicates
|
256
294
|
|
257
|
-
|
258
|
-
In Ruby, this includes most of the core objects: `String`, `Enumerable` (`Array`, `Hash`,
|
259
|
-
`Range`, `Set`).
|
295
|
+
self.messages_path = 'config/errors.yml'
|
260
296
|
|
261
|
-
|
297
|
+
predicate(:email?) do |current|
|
298
|
+
current.match(/.../)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
We have defined a module `MyPredicates` with the purpose to share its custom predicates with all the validators that need them.
|
262
304
|
|
263
305
|
```ruby
|
264
306
|
require 'hanami/validations'
|
307
|
+
require_relative 'my_predicates'
|
265
308
|
|
266
309
|
class Signup
|
267
310
|
include Hanami::Validations
|
311
|
+
predicates MyPredicates
|
268
312
|
|
269
|
-
|
313
|
+
validations do
|
314
|
+
required(:email) { email? }
|
315
|
+
end
|
270
316
|
end
|
271
|
-
|
272
|
-
signup = Signup.new(music: 'rock')
|
273
|
-
signup.valid? # => true
|
274
|
-
|
275
|
-
signup = Signup.new(music: 'pop')
|
276
|
-
signup.valid? # => false
|
277
317
|
```
|
278
318
|
|
279
|
-
|
319
|
+
### Required and Optional keys
|
280
320
|
|
281
|
-
|
321
|
+
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`.
|
282
322
|
|
283
323
|
```ruby
|
284
324
|
require 'hanami/validations'
|
@@ -286,296 +326,467 @@ require 'hanami/validations'
|
|
286
326
|
class Signup
|
287
327
|
include Hanami::Validations
|
288
328
|
|
289
|
-
|
329
|
+
validations do
|
330
|
+
required(:email) { ... }
|
331
|
+
optional(:referral) { ... }
|
332
|
+
end
|
290
333
|
end
|
291
|
-
|
292
|
-
signup = Signup.new(name: 'Luca')
|
293
|
-
signup.valid? # => true
|
294
|
-
|
295
|
-
signup = Signup.new(name: '23')
|
296
|
-
signup.valid? # => false
|
297
334
|
```
|
298
335
|
|
299
|
-
|
336
|
+
### Type Safety
|
300
337
|
|
301
|
-
|
302
|
-
value.
|
338
|
+
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.
|
303
339
|
|
304
|
-
|
305
|
-
In Ruby, this includes most of the core objects: like `String`, `Enumerable` (`Array`, `Hash`,
|
306
|
-
`Range`, `Set`).
|
340
|
+
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?
|
307
341
|
|
308
|
-
|
342
|
+
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?`.
|
309
343
|
|
310
344
|
```ruby
|
311
|
-
|
312
|
-
|
345
|
+
required(:age) { type?(Integer) & gteq?(18) }
|
346
|
+
```
|
313
347
|
|
314
|
-
|
315
|
-
def initialize(limit)
|
316
|
-
@numbers = Prime.each(limit).to_a
|
317
|
-
end
|
348
|
+
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.
|
318
349
|
|
319
|
-
|
320
|
-
@numbers.include?(number)
|
321
|
-
end
|
322
|
-
end
|
350
|
+
**We suggest to use `#type?` at the beginning of the validations block. This _type safety_ policy is crucial to prevent runtime errors.**
|
323
351
|
|
324
|
-
|
325
|
-
include Hanami::Validations
|
352
|
+
`Hanami::Validations` supports the most common Ruby types:
|
326
353
|
|
327
|
-
|
328
|
-
|
329
|
-
|
354
|
+
* `Array` (aliased as `array?`)
|
355
|
+
* `BigDecimal` (aliased as `decimal?`)
|
356
|
+
* `Boolean` (aliased as `bool?`)
|
357
|
+
* `Date` (aliased as `date?`)
|
358
|
+
* `DateTime` (aliased as `datetime?`)
|
359
|
+
* `Float` (aliased as `float?`)
|
360
|
+
* `Hash` (aliased as `hash?`)
|
361
|
+
* `Integer` (aliased as `int?`)
|
362
|
+
* `String` (aliased as `str?`)
|
363
|
+
* `Time` (aliased as `time?`)
|
364
|
+
|
365
|
+
For each supported type, there a convenient predicate that acts as an alias. For instance, the two lines of code below are **equivalent**.
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
required(:age) { type?(Integer) }
|
369
|
+
required(:age) { int? }
|
370
|
+
```
|
330
371
|
|
331
|
-
|
332
|
-
signup.valid? # => true
|
372
|
+
### Macros
|
333
373
|
|
334
|
-
|
335
|
-
|
374
|
+
Rule composition with blocks is powerful, but it can become verbose.
|
375
|
+
To reduce verbosity, `Hanami::Validations` offers convenient _macros_ that are internally _expanded_ (aka interpreted) to an equivalent _block expression_
|
336
376
|
|
337
|
-
|
338
|
-
signup.valid? # => true
|
377
|
+
#### Filled
|
339
378
|
|
340
|
-
|
341
|
-
|
379
|
+
To use when we expect a value to be filled:
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
# expands to
|
383
|
+
# required(:age) { filled? }
|
384
|
+
|
385
|
+
required(:age).filled
|
342
386
|
```
|
343
387
|
|
344
|
-
|
388
|
+
```ruby
|
389
|
+
# expands to
|
390
|
+
# required(:age) { filled? & type?(Integer) }
|
345
391
|
|
346
|
-
|
392
|
+
required(:age).filled(:int?)
|
393
|
+
```
|
347
394
|
|
348
395
|
```ruby
|
349
|
-
|
396
|
+
# expands to
|
397
|
+
# required(:age) { filled? & type?(Integer) & gt?(18) }
|
350
398
|
|
351
|
-
|
352
|
-
|
399
|
+
required(:age).filled(:int?, gt?: 18)
|
400
|
+
```
|
353
401
|
|
354
|
-
|
355
|
-
|
402
|
+
In the examples above `age` is **always required** as value.
|
403
|
+
|
404
|
+
#### Maybe
|
356
405
|
|
357
|
-
|
358
|
-
signup.valid? # => true
|
406
|
+
To use when a value can be nil:
|
359
407
|
|
360
|
-
|
361
|
-
|
408
|
+
```ruby
|
409
|
+
# expands to
|
410
|
+
# required(:age) { none? | int? }
|
362
411
|
|
363
|
-
|
364
|
-
signup.valid? # => false
|
412
|
+
required(:age).maybe(:int?)
|
365
413
|
```
|
366
414
|
|
367
|
-
|
415
|
+
In the example above `age` can be `nil`, but if we send the value, it **must** be an integer.
|
368
416
|
|
369
|
-
|
417
|
+
#### Each
|
418
|
+
|
419
|
+
To use when we want to apply the same validation rules to all the elements of an array:
|
370
420
|
|
371
421
|
```ruby
|
372
|
-
|
422
|
+
# expands to
|
423
|
+
# required(:tags) { array? { each { str? } } }
|
373
424
|
|
374
|
-
|
375
|
-
|
376
|
-
include Hanami::Validations
|
425
|
+
required(:tags).each(:str?)
|
426
|
+
```
|
377
427
|
|
378
|
-
|
379
|
-
attribute :password, size: 8..64 # range
|
380
|
-
attribute :avatar, size: 1..(5 * MEGABYTE)
|
381
|
-
end
|
428
|
+
In the example above `tags` **must** be an array of strings.
|
382
429
|
|
383
|
-
signup = Signup.new(password: 'a-very-long-password')
|
384
|
-
signup.valid? # => true
|
385
430
|
|
386
|
-
|
387
|
-
|
431
|
+
#### Confirmation
|
432
|
+
|
433
|
+
This is designed to check if pairs of web form fields have the same value. One wildly popular example is _password confirmation_.
|
434
|
+
|
435
|
+
```ruby
|
436
|
+
required(:password).filled.confirmation
|
388
437
|
```
|
389
438
|
|
390
|
-
|
391
|
-
because Ruby's `File` and `Tempfile` both respond to `#size`.**
|
439
|
+
It is valid if the input has `password` and `password_confirmation` keys with the same exact value.
|
392
440
|
|
393
|
-
|
441
|
+
⚠ **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. ⚠
|
394
442
|
|
395
|
-
|
396
|
-
The other reason is that this isn't an effective way to ensure uniqueness of a value in a database.
|
443
|
+
### Forms
|
397
444
|
|
398
|
-
|
445
|
+
An important precondition to check before to implement a validator is about the expected input.
|
446
|
+
When we use validators for already preprocessed data it's safe to use basic validations from `Hanami::Validations` mixin.
|
447
|
+
|
448
|
+
If the data is coming directly from user input via a HTTP form, it's advisable to use `Hanami::Validations::Form` instead.
|
449
|
+
**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.
|
450
|
+
|
451
|
+
### Rules
|
452
|
+
|
453
|
+
Predicates and macros are tools to code validations that concern a single key like `first_name` or `email`.
|
454
|
+
If the outcome of a validation depends on two or more attributes we can use _rules_.
|
399
455
|
|
400
|
-
|
456
|
+
Here's a practical example: a job board.
|
457
|
+
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).
|
458
|
+
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`.
|
401
459
|
|
402
|
-
|
460
|
+
Here's the code:
|
403
461
|
|
404
462
|
```ruby
|
405
|
-
class
|
406
|
-
include Hanami::Validations
|
463
|
+
class CreateJob
|
464
|
+
include Hanami::Validations::Form
|
465
|
+
|
466
|
+
validations do
|
467
|
+
required(:type).filled(:int?, included_in?: [1, 2, 3])
|
468
|
+
|
469
|
+
optional(:location).maybe(:str?)
|
470
|
+
optional(:remote).maybe(:bool?)
|
471
|
+
|
472
|
+
required(:title).filled(:str?)
|
473
|
+
required(:description).filled(:str?)
|
474
|
+
required(:company).filled(:str?)
|
407
475
|
|
408
|
-
|
476
|
+
optional(:website).filled(:str?, format?: URI.regexp(%w(http https)))
|
409
477
|
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
attribute :postal_code, presence: true, format: /.../
|
478
|
+
rule(location_presence: [:location, :remote]) do |location, remote|
|
479
|
+
(remote.none? | remote.false?).then(location.filled?) &
|
480
|
+
remote.true?.then(location.none?)
|
481
|
+
end
|
415
482
|
end
|
416
483
|
end
|
417
|
-
|
418
|
-
validator = ShippingDetails.new
|
419
|
-
validator.valid? # => false
|
420
484
|
```
|
421
485
|
|
422
|
-
|
423
|
-
|
486
|
+
We specify a rule with `rule` method, which takes an arbitrary name and an array of preconditions.
|
487
|
+
Only if `:location` and `:remote` are valid according to their validations described above, the `rule` block is evaluated.
|
488
|
+
|
489
|
+
The block yields the same exact keys that we put in the precondintions.
|
490
|
+
So for `[:location, :remote]` it will yield the corresponding values, bound to the `location` and `remote` variables.
|
491
|
+
|
492
|
+
We can use these variables to define the rule. We covered a few cases:
|
493
|
+
|
494
|
+
* If `remote` is missing or false, then `location` must be filled
|
495
|
+
* If `remote` is true, then `location` must be omitted
|
496
|
+
|
497
|
+
### Nested Input Data
|
498
|
+
|
499
|
+
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.
|
424
500
|
|
425
501
|
```ruby
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
502
|
+
validations do
|
503
|
+
required(:customer).schema do
|
504
|
+
required(:email) { … }
|
505
|
+
required(:name) { … }
|
506
|
+
# other validations …
|
507
|
+
end
|
430
508
|
end
|
431
509
|
```
|
432
510
|
|
433
|
-
|
511
|
+
Groups can be **deeply nested**, without any limitation.
|
434
512
|
|
435
513
|
```ruby
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
514
|
+
validations do
|
515
|
+
required(:customer).schema do
|
516
|
+
# other validations …
|
517
|
+
|
518
|
+
required(:address).schema do
|
519
|
+
required(:street) { … }
|
520
|
+
# other address validations …
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
443
524
|
```
|
444
525
|
|
445
|
-
###
|
526
|
+
### Composition
|
446
527
|
|
447
|
-
|
528
|
+
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.
|
448
529
|
|
449
|
-
|
450
|
-
require 'hanami/validations'
|
530
|
+
As the code base grows, it’s a good practice to DRY validation rules.
|
451
531
|
|
452
|
-
|
532
|
+
```ruby
|
533
|
+
class AddressValidator
|
453
534
|
include Hanami::Validations
|
454
535
|
|
455
|
-
|
536
|
+
validations do
|
537
|
+
required(:street) { … }
|
538
|
+
end
|
456
539
|
end
|
540
|
+
```
|
541
|
+
|
542
|
+
This validator can be reused by other validators.
|
457
543
|
|
458
|
-
|
544
|
+
```ruby
|
545
|
+
class CustomerValidator
|
459
546
|
include Hanami::Validations
|
460
547
|
|
461
|
-
|
548
|
+
validations do
|
549
|
+
required(:email) { … }
|
550
|
+
required(:address).schema(AddressValidator)
|
551
|
+
end
|
462
552
|
end
|
553
|
+
```
|
554
|
+
|
555
|
+
Again, there is no limit to the nesting levels.
|
463
556
|
|
464
|
-
|
557
|
+
```ruby
|
558
|
+
class OrderValidator
|
465
559
|
include Hanami::Validations
|
466
560
|
|
467
|
-
|
468
|
-
|
561
|
+
validations do
|
562
|
+
required(:number) { … }
|
563
|
+
required(:customer).schema(CustomerValidator)
|
564
|
+
end
|
469
565
|
end
|
566
|
+
```
|
470
567
|
|
471
|
-
|
472
|
-
include EmailValidations
|
473
|
-
include PasswordValidations
|
474
|
-
end
|
568
|
+
In the end, `OrderValidator` is able to validate a complex data structure like this:
|
475
569
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
570
|
+
```ruby
|
571
|
+
{
|
572
|
+
number: "123",
|
573
|
+
customer: {
|
574
|
+
email: "user@example.com",
|
575
|
+
address: {
|
576
|
+
city: "Rome"
|
577
|
+
}
|
578
|
+
}
|
579
|
+
}
|
580
|
+
```
|
483
581
|
|
484
|
-
|
485
|
-
# This additional validation is active only in this case.
|
486
|
-
attribute :password, confirmation: true
|
487
|
-
end
|
582
|
+
### Whitelisting
|
488
583
|
|
489
|
-
|
490
|
-
|
491
|
-
# * password (presence)
|
492
|
-
class Signin
|
493
|
-
include CommonValidations
|
494
|
-
end
|
584
|
+
Another fundamental role that validators plays in the architecture of our projects is input whitelisting.
|
585
|
+
For security reasons, we want to allow known keys to come in and reject everything else.
|
495
586
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
587
|
+
This process happens when we invoke `#validate`.
|
588
|
+
Allowed keys are the ones defined with `.required`.
|
589
|
+
|
590
|
+
**Please note that whitelisting is only available for `Hanami::Validations::Form` mixin.**
|
591
|
+
|
592
|
+
### Result
|
593
|
+
|
594
|
+
When we trigger the validation process with `#validate`, we get a result object in return. It’s able to tell if it’s successful, which rules the input data has violated and an output data bag.
|
595
|
+
|
596
|
+
```ruby
|
597
|
+
result = OrderValidator.new({}).validate
|
598
|
+
result.success? # => false
|
501
599
|
```
|
502
600
|
|
503
|
-
|
601
|
+
#### Messages
|
602
|
+
|
603
|
+
`result.messages` returns a nested set of validation error messages.
|
604
|
+
|
605
|
+
Each error carries on informations about a single rule violation.
|
504
606
|
|
505
607
|
```ruby
|
506
|
-
|
608
|
+
result.messages.fetch(:number) # => ["is missing"]
|
609
|
+
result.messages.fetch(:customer) # => ["is missing"]
|
610
|
+
```
|
507
611
|
|
508
|
-
|
612
|
+
#### Output
|
613
|
+
|
614
|
+
`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.
|
615
|
+
|
616
|
+
```ruby
|
617
|
+
{
|
618
|
+
"number" => "123",
|
619
|
+
"unknown" => "foo"
|
620
|
+
}
|
621
|
+
```
|
622
|
+
|
623
|
+
If we receive the input above, `output` will look like this.
|
624
|
+
|
625
|
+
```ruby
|
626
|
+
result.output
|
627
|
+
# => { :number => 123 }
|
628
|
+
```
|
629
|
+
|
630
|
+
We can observe that:
|
631
|
+
|
632
|
+
* Keys are _symbolized_
|
633
|
+
* Only whitelisted keys are included
|
634
|
+
* Data is coerced
|
635
|
+
|
636
|
+
### Error Messages
|
637
|
+
|
638
|
+
To pick the right error message is crucial for user experience.
|
639
|
+
As usual `Hanami::Validations` comes to the rescue for most common cases and it leaves space to customization of behaviors.
|
640
|
+
|
641
|
+
We have seen that builtin predicates have default messages, while [inline predicates](#inline-custom-predicates) allow to specify a custom message via the `:message` option.
|
642
|
+
|
643
|
+
```ruby
|
644
|
+
class SignupValidator
|
509
645
|
include Hanami::Validations
|
510
646
|
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
647
|
+
predicate :email?, message: 'must be an email' do |current|
|
648
|
+
# ...
|
649
|
+
end
|
650
|
+
|
651
|
+
validations do
|
652
|
+
required(:email).filled(:str?, :email?)
|
653
|
+
required(:age).filled(:int?, gt?: 18)
|
654
|
+
end
|
515
655
|
end
|
656
|
+
|
657
|
+
result = SignupValidator.new(email: 'foo', age: 1).validate
|
658
|
+
|
659
|
+
result.success? # => false
|
660
|
+
result.messages.fetch(:email) # => ['must be an email']
|
661
|
+
result.messages.fetch(:age) # => ['must be greater than 18']
|
516
662
|
```
|
517
663
|
|
518
|
-
|
664
|
+
#### Configurable Error Messages
|
665
|
+
|
666
|
+
Inline error messages are ideal for quick and dirty development, but we suggest to use an external YAML file to configure these messages:
|
667
|
+
|
668
|
+
```yaml
|
669
|
+
# config/messages.yml
|
670
|
+
en:
|
671
|
+
errors:
|
672
|
+
email?: "must be an email"
|
673
|
+
```
|
519
674
|
|
520
|
-
|
521
|
-
It's a set of errors grouped by attribute. Each error contains the name of the
|
522
|
-
invalid attribute, the failed validation, the expected value, and the current one.
|
675
|
+
To be used like this:
|
523
676
|
|
524
677
|
```ruby
|
525
|
-
|
678
|
+
class SignupValidator
|
679
|
+
include Hanami::Validations
|
680
|
+
messages_path 'config/messages.yml'
|
681
|
+
|
682
|
+
predicate :email? do |current|
|
683
|
+
# ...
|
684
|
+
end
|
685
|
+
|
686
|
+
validations do
|
687
|
+
required(:email).filled(:str?, :email?)
|
688
|
+
required(:age).filled(:int?, gt?: 18)
|
689
|
+
end
|
690
|
+
end
|
526
691
|
|
527
|
-
|
692
|
+
```
|
693
|
+
|
694
|
+
|
695
|
+
#### Custom Error Messages
|
696
|
+
|
697
|
+
In the example above, the failure message for age is fine: `"must be greater than 18"`, but how to tweak it? What if we need to change into something diffent? Again, we can use the YAML configuration file for our purpose.
|
698
|
+
|
699
|
+
```yaml
|
700
|
+
# config/messages.yml
|
701
|
+
en:
|
702
|
+
errors:
|
703
|
+
email?: "must be an email"
|
704
|
+
|
705
|
+
rules:
|
706
|
+
signup:
|
707
|
+
age:
|
708
|
+
gt?: "must be an adult"
|
709
|
+
|
710
|
+
```
|
711
|
+
|
712
|
+
Now our validator is able to look at the right error message.
|
713
|
+
|
714
|
+
```ruby
|
715
|
+
result = SignupValidator.new(email: 'foo', age: 1).validate
|
716
|
+
|
717
|
+
result.success? # => false
|
718
|
+
result.messages.fetch(:age) # => ['must be an adult']
|
719
|
+
```
|
720
|
+
|
721
|
+
##### Custom namespace
|
722
|
+
|
723
|
+
⚠ **CONVENTION:** For a given validator named `SignupValidator`, the framework will look for `signup` translation key. ⚠
|
724
|
+
|
725
|
+
If for some reason that doesn't work for us, we can customize the namespace:
|
726
|
+
|
727
|
+
```ruby
|
728
|
+
class SignupValidator
|
528
729
|
include Hanami::Validations
|
529
730
|
|
530
|
-
|
531
|
-
|
731
|
+
messages_path 'config/messages.yml'
|
732
|
+
namespace :my_signup
|
733
|
+
|
734
|
+
# ...
|
532
735
|
end
|
736
|
+
```
|
533
737
|
|
534
|
-
|
535
|
-
signup.valid? # => true
|
738
|
+
The new namespace should be used in the YAML file too.
|
536
739
|
|
537
|
-
|
538
|
-
|
740
|
+
```yaml
|
741
|
+
# config/messages.yml
|
742
|
+
en:
|
743
|
+
# ...
|
744
|
+
rules:
|
745
|
+
my_signup:
|
746
|
+
age:
|
747
|
+
gt?: "must be an adult"
|
539
748
|
|
540
|
-
signup.errors
|
541
|
-
# => #<Hanami::Validations::Errors:0x007fe00ced9b78
|
542
|
-
# @errors={
|
543
|
-
# :email=>[
|
544
|
-
# #<Hanami::Validations::Error:0x007fe00cee3290 @attribute=:email, @validation=:presence, @expected=true, @actual="">,
|
545
|
-
# #<Hanami::Validations::Error:0x007fe00cee31f0 @attribute=:email, @validation=:format, @expected=/\A(.*)@(.*)\.(.*)\z/, @actual="">
|
546
|
-
# ],
|
547
|
-
# :age=>[
|
548
|
-
# #<Hanami::Validations::Error:0x007fe00cee30d8 @attribute=:age, @validation=:size, @expected=18..99, @actual=17>
|
549
|
-
# ]
|
550
|
-
# }>
|
551
749
|
```
|
552
750
|
|
553
|
-
|
751
|
+
#### Internationalization (I18n)
|
554
752
|
|
555
|
-
|
753
|
+
If your project already depends on `i18n` gem, `Hanami::Validations` is able to look at the translations defined for that gem and to use them.
|
556
754
|
|
557
|
-
```ruby
|
558
|
-
require 'hanami/model'
|
559
|
-
require 'hanami/validations'
|
560
755
|
|
561
|
-
|
562
|
-
|
756
|
+
```ruby
|
757
|
+
class SignupValidator
|
563
758
|
include Hanami::Validations
|
564
759
|
|
565
|
-
|
566
|
-
attribute :price, type: Integer, presence: true
|
567
|
-
end
|
760
|
+
messages :i18n
|
568
761
|
|
569
|
-
|
570
|
-
|
762
|
+
# ...
|
763
|
+
end
|
764
|
+
```
|
571
765
|
|
572
|
-
|
573
|
-
|
766
|
+
```yaml
|
767
|
+
# config/locales/en.yml
|
768
|
+
en:
|
769
|
+
errors:
|
770
|
+
signup:
|
771
|
+
# ...
|
574
772
|
```
|
575
773
|
|
774
|
+
## FAQs
|
775
|
+
### Uniqueness Validation
|
776
|
+
|
777
|
+
Uniqueness validation isn't implemented by `Hanami::Validations` because the context of execution is completely decoupled from persistence.
|
778
|
+
Please remember that **uniqueness validation is a huge race condition between application and the database, and it doesn't guarantee records uniqueness for real.** To effectively enforce this policy you can use SQL database constraints.
|
779
|
+
|
780
|
+
Please read more at: [The Perils of Uniqueness Validations](http://robots.thoughtbot.com/the-perils-of-uniqueness-validations).
|
781
|
+
|
782
|
+
## Acknowledgements
|
783
|
+
|
784
|
+
Thanks to [dry-rb](http://dry-rb.org) Community for their priceless support. ❤️
|
785
|
+
`hanami-validations` uses [dry-validation](http://dry-rb.org/gems/dry-validation) as powerful low-level engine.
|
786
|
+
|
576
787
|
## Contributing
|
577
788
|
|
578
|
-
1. Fork it ( https://github.com/hanami/
|
789
|
+
1. Fork it ( https://github.com/hanami/validations/fork )
|
579
790
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
580
791
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
581
792
|
4. Push to the branch (`git push origin my-new-feature`)
|