hanami-validations 1.3.7 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,15 +1,19 @@
1
1
  # Hanami::Validations
2
2
 
3
- Validations mixin for Ruby objects
3
+ Data validation library for Ruby
4
4
 
5
5
  ## Status
6
6
 
7
7
  [![Gem Version](https://badge.fury.io/rb/hanami-validations.svg)](https://badge.fury.io/rb/hanami-validations)
8
- [![CI](https://github.com/hanami/validations/workflows/ci/badge.svg?branch=master)](https://github.com/hanami/validations/actions?query=workflow%3Aci+branch%3Amaster)
9
- [![Test Coverage](https://codecov.io/gh/hanami/validations/branch/master/graph/badge.svg)](https://codecov.io/gh/hanami/validations)
8
+ [![CI](https://github.com/hanami/validations/workflows/ci/badge.svg?branch=main)](https://github.com/hanami/validations/actions?query=workflow%3Aci+branch%3Amain)
9
+ [![Test Coverage](https://codecov.io/gh/hanami/validations/branch/main/graph/badge.svg)](https://codecov.io/gh/hanami/validations)
10
10
  [![Depfu](https://badges.depfu.com/badges/af6c6be539d9d587c7541ae7a013c9ff/overview.svg)](https://depfu.com/github/hanami/validations?project=Bundler)
11
11
  [![Inline Docs](http://inch-ci.org/github/hanami/validations.svg)](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) 2.3+ and JRuby 9.1.5.0+.
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 'hanami-validations'
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
- `Hanami::Validations` is a mixin that, once included by an object, adds lightweight set of validations to it.
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
- 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`.
57
+ ### Overview
76
58
 
77
- To better recognise the pattern, let’s get back to the example above. This time we will map the natural language rules with programming language rules.
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
- A name must be filled and be a string and its size must be included between 3 and 64.
81
- 👇 👇 👇 👇 👇 👇 👇 👇
82
- required(:name) { filled? & str? & size? (3 .. 64) }
83
-
84
- ```
85
-
86
- Now, I hope you’ll never format code like that, but in this case, that formatting serves well our purpose to show how Ruby’s simplicity helps to define complex rules with no effort.
87
-
88
- From a high level perspective, we can tell that input data for `name` is _valid_ only if **all** the requirements are satisfied. That’s because we used `&`.
89
-
90
- #### Logic Operators
91
-
92
- We support four logic operators:
93
-
94
- * `&` (aliased as `and`) for _conjunction_
95
- * `|` (aliased as `or`) for _disjunction_
96
- * `>` (aliased as `then`) for _implication_
97
- * `^` (aliased as `xor`) for _exclusive disjunction_
98
-
99
- #### Context Of Execution
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
- **Please notice that we used `&` over Ruby's `&&` keyword.**
102
- That's because the context of execution of these validations isn't a plain lambda, but something richer.
65
+ Only when the input is formally valid (according to the **schema**), validation **rules** are checked.
103
66
 
104
- For real world projects, we want to support common scenarios without the need of reinventing the wheel ourselves. Scenarios like _password confirmation_, _size check_ are already prepackaged with `Hanami::Validations`.
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
- 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.
70
+ require "hanami/validations"
112
71
 
113
- We already met `filled?` and `size?`, now let’s introduce the rest of them. They capture **common use cases with web forms**.
72
+ class SignupValidator < Hanami::Validator
73
+ schema do
74
+ required(:email).value(:string)
75
+ required(:age).value(:integer)
76
+ end
114
77
 
115
- ### Array
78
+ rule(:age) do
79
+ key.failure("must be greater than 18") if value < 18
80
+ end
81
+ end
116
82
 
117
- It checks if the the given value is an array, and iterates through its elements to perform checks on each of them.
83
+ validator = SignupValidator.new
118
84
 
119
- ```ruby
120
- required(:codes) { array? { each { int? } } }
121
- ```
85
+ result = validator.call(email: "user@hanamirb.test", age: 37)
86
+ result.success? # => true
122
87
 
123
- This example checks if `codes` is an array and if all the elements are integers, whereas the following example checks there are a minimum of 2 elements and all elements are strings.
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
- ```ruby
126
- required(:codes) { array? { min_size?(2) & each { str? } } }
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
- #### Emptiness
97
+ ### Schemas
130
98
 
131
- It checks if the given value is empty or not. It is designed to works with strings and collections (array and hash).
99
+ A basic schema doesn't apply data coercion, input must already have the right Ruby types.
132
100
 
133
101
  ```ruby
134
- required(:tags) { empty? }
135
- ```
102
+ # frozen_string_literal: true
136
103
 
137
- #### Equality
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
- Ruby types are respected: `23` (an integer) is only equal to `23`, and not to `"23"` (a string). See _Type Safety_ section.
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
- #### Exclusion
113
+ validator = SignupValidator.new
148
114
 
149
- 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?`.
115
+ result = validator.call(email: "user@hanamirb.test", age: 37)
116
+ puts result.success? # => true
150
117
 
151
- ```ruby
152
- required(:genre) { excluded_from?(%w(pop dance)) }
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
- #### Format
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
- #### Greater Than
125
+ When used in _params mode_, a schema applies data coercion, before to run validation checks.
167
126
 
168
- This predicate works with numbers to check if input is **greater than** a given threshold.
127
+ This is designed for Web form/HTTP params.
169
128
 
170
129
  ```ruby
171
- required(:age) { gt?(18) }
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
- ```ruby
179
- required(:age) { gteq?(19) }
180
- ```
132
+ require "bundler/setup"
133
+ require "hanami/validations"
181
134
 
182
- #### Inclusion
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
- This predicate is the opposite of `#exclude?`: it verifies if the input is **included** in the given collection.
142
+ validator = SignupValidator.new
185
143
 
186
- ```ruby
187
- required(:genre) { included_in?(%w(rock folk)) }
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
- #### Less Than
149
+ ### JSON
191
150
 
192
- This is the complement of `#gt?`: it checks for **less than** numbers.
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
- required(:age) { lt?(7) }
196
- ```
155
+ # frozen_string_literal: true
197
156
 
198
- #### Less Than Equal
157
+ require "hanami/validations"
199
158
 
200
- 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.
201
-
202
- ```ruby
203
- required(:age) { lteq?(6) }
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
- #### Filled
166
+ validator = SignupValidator.new
207
167
 
208
- It’s a predicate that ensures data input is filled, that means **not** `nil` or blank (`""`) or empty (in case we expect a collection).
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
- ```ruby
211
- required(:name) { filled? } # string
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
- #### Minimum Size
176
+ ### Whitelisting
216
177
 
217
- This verifies that the size of the given input is at least of the specified value.
178
+ Unknown keys from incoming data are filtered out:
218
179
 
219
180
  ```ruby
220
- required(:password) { min_size?(12) }
221
- ```
181
+ # frozen_string_literal: true
222
182
 
223
- #### Maximum Size
183
+ require "hanami/validations"
224
184
 
225
- This verifies that the size of the given input is at max of the specified value.
226
-
227
- ```ruby
228
- required(:name) { max_size?(128) }
229
- ```
230
-
231
- #### None
185
+ class SignupValidator < Hanami::Validator
186
+ schema do
187
+ required(:email).value(:string)
188
+ end
189
+ end
232
190
 
233
- This verifies if the given input is `nil`. Blank strings (`""`) won’t pass this test and return `false`.
191
+ validator = SignupValidator.new
234
192
 
235
- ```ruby
236
- required(:location) { none? }
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
- #### Size
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
- required(:two_factor_auth_code) { size?(6) } # exact
245
- required(:password) { size?(8..32) } # range
246
- ```
201
+ # frozen_string_literal: true
247
202
 
248
- The check works with strings and collections.
203
+ require "hanami/validations"
249
204
 
250
- ```ruby
251
- required(:answers) { size?(2) } # only 2 answers are allowed
252
- ```
205
+ module Types
206
+ include Dry::Types()
253
207
 
254
- 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.
208
+ StrippedString = Types::String.constructor(&:strip)
209
+ end
255
210
 
256
- 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.
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
- ```ruby
259
- MEGABYTE = 1024 ** 2
218
+ validator = SignupValidator.new
260
219
 
261
- required(:avatar) { size?(1..(5 * MEGABYTE)) }
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
- ### Custom Predicates
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
- 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:
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
- require 'hanami/validations'
231
+ # frozen_string_literal: true
276
232
 
277
- class Signup
278
- include Hanami::Validations
233
+ require "hanami/validations"
279
234
 
280
- predicate :url?, message: 'must be an URL' do |current|
281
- # ...
235
+ class EventValidator < Hanami::Validator
236
+ params do
237
+ required(:start_date).value(:date)
282
238
  end
283
239
 
284
- validations do
285
- required(:website) { url? }
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
- #### Global Custom Predicates
245
+ validator = EventValidator.new
291
246
 
292
- If our goal is to share common used custom predicates, we can include them in a module to use in all our validators:
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
- ```ruby
295
- require 'hanami/validations'
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
- module MyPredicates
298
- include Hanami::Validations::Predicates
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
- self.messages_path = 'config/errors.yml'
260
+ Learn more about rules: https://dry-rb.org/gems/dry-validation/master/rules/
301
261
 
302
- predicate(:email?) do |current|
303
- current.match(/.../)
304
- end
305
- end
306
- ```
262
+ ### Inheritance
307
263
 
308
- We have defined a module `MyPredicates` with the purpose to share its custom predicates with all the validators that need them.
264
+ Schema and rules validations can be inherited and used by subclasses
309
265
 
310
266
  ```ruby
311
- require 'hanami/validations'
312
- require_relative 'my_predicates'
267
+ # frozen_string_literal: true
313
268
 
314
- class Signup
315
- include Hanami::Validations
316
- predicates MyPredicates
269
+ require "hanami/validations"
317
270
 
318
- validations do
319
- required(:email) { email? }
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 Signup
332
- include Hanami::Validations
333
-
334
- validations do
335
- required(:email) { ... }
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
- ```ruby
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
- To use when we expect a value to be filled:
385
-
386
- ```ruby
387
- # expands to
388
- # required(:age) { filled? }
389
-
390
- required(:age).filled
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
- ```ruby
394
- # expands to
395
- # required(:age) { filled? & type?(Integer) }
292
+ ### Messages
396
293
 
397
- required(:age).filled(:int?)
398
- ```
399
-
400
- ```ruby
401
- # expands to
402
- # required(:age) { filled? & type?(Integer) & gt?(18) }
403
-
404
- required(:age).filled(:int?, gt?: 18)
405
- ```
406
-
407
- In the examples above `age` is **always required** as value.
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
- #### Maybe
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
- # expands to
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
- To use when we want to apply the same validation rules to all the elements of an array:
425
-
426
- ```ruby
427
- # expands to
428
- # required(:tags) { array? { each { str? } } }
302
+ require "hanami/validations"
429
303
 
430
- required(:tags).each(:str?)
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 example above `tags` **must** be an array of strings.
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
- #### Confirmation
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
- This is designed to check if pairs of web form fields have the same value. One wildly popular example is _password confirmation_.
318
+ Our **suggestion** is to start with **specific** messages and see if there is a need to generalize them.
439
319
 
440
- ```ruby
441
- required(:password).filled.confirmation
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
- It is valid if the input has `password` and `password_confirmation` keys with the same exact value.
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 CreateJob
469
- include Hanami::Validations::Form
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
- ```ruby
507
- validations do
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
- Groups can be **deeply nested**, without any limitation.
347
+ validator = SignupValidator.new
517
348
 
518
- ```ruby
519
- validations do
520
- required(:customer).schema do
521
- # other validations …
349
+ result = validator.call(username: "foo")
350
+ puts result.success? # => true
522
351
 
523
- required(:address).schema do
524
- required(:street) { }
525
- # other address validations
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
- ### Composition
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
- As the code base grows, its a good practice to DRY validation rules.
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 AddressValidator
539
- include Hanami::Validations
540
-
541
- validations do
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
- validations do
554
- required(:email) { }
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
- Again, there is no limit to the nesting levels.
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
- **Please note that whitelisting is only available for `Hanami::Validations::Form` mixin.**
377
+ validator = SignupValidator.new
596
378
 
597
- ### Result
598
-
599
- When we trigger the validation process with `#validate`, we get a result object in return. It’s able to tell if it’s successful, which rules the input data has violated and an output data bag.
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
- We can observe that:
636
-
637
- * Keys are _symbolized_
638
- * Only whitelisted keys are included
639
- * Data is coerced
640
-
641
- ### Error Messages
642
-
643
- To pick the right error message is crucial for user experience.
644
- As usual `Hanami::Validations` comes to the rescue for most common cases and it leaves space to customization of behaviors.
384
+ #### Extra information
645
385
 
646
- We have seen that builtin predicates have default messages, while [inline predicates](#inline-custom-predicates) allow to specify a custom message via the `:message` option.
386
+ The interpolation mechanism, accepts extra, arbitrary information expressed as a `Hash` (e.g. `code: "123"`)
647
387
 
648
388
  ```ruby
649
- class SignupValidator
650
- include Hanami::Validations
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
- validations do
657
- required(:email).filled(:str?, :email?)
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
- result = SignupValidator.new(email: 'foo', age: 1).validate
399
+ validator = RefundValidator.new
663
400
 
664
- result.success? # => false
665
- result.messages.fetch(:email) # => ['must be an email']
666
- result.messages.fetch(:age) # => ['must be greater than 18']
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
- #### Configurable Error Messages
406
+ Learn more about messages: https://dry-rb.org/gems/dry-validation/master/messages/
670
407
 
671
- Inline error messages are ideal for quick and dirty development, but we suggest to use an external YAML file to configure these messages:
408
+ ### External dependencies
672
409
 
673
- ```yaml
674
- # config/messages.yml
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
- class SignupValidator
684
- include Hanami::Validations
685
- messages_path 'config/messages.yml'
414
+ # frozen_string_literal: true
686
415
 
687
- predicate :email? do |current|
688
- # ...
689
- end
416
+ require "hanami/validations"
690
417
 
691
- validations do
692
- required(:email).filled(:str?, :email?)
693
- required(:age).filled(:int?, gt?: 18)
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
- 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.
427
+ schema do
428
+ required(:address).filled(:string)
429
+ end
703
430
 
704
- ```yaml
705
- # config/messages.yml
706
- en:
707
- errors:
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
- rules:
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
- Now our validator is able to look at the right error message.
443
+ Read more about external dependencies: https://dry-rb.org/gems/dry-validation/master/external-dependencies/
718
444
 
719
- ```ruby
720
- result = SignupValidator.new(email: 'foo', age: 1).validate
445
+ ### Mixin
721
446
 
722
- result.success? # => false
723
- result.messages.fetch(:age) # => ['must be an adult']
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
- ⚠ **CONVENTION:** For a given validator named `SignupValidator`, the framework will look for `signup` translation key. ⚠
450
+ ```ruby
451
+ # frozen_string_literal: true
729
452
 
730
- If for some reason that doesn't work for us, we can customize the namespace:
453
+ require "hanami/validations"
731
454
 
732
- ```ruby
733
- class SignupValidator
455
+ class UserValidator
734
456
  include Hanami::Validations
735
457
 
736
- messages_path 'config/messages.yml'
737
- namespace :my_signup
738
-
739
- # ...
458
+ validations do
459
+ required(:number).filled(:integer, eql?: 23)
460
+ end
740
461
  end
741
- ```
742
462
 
743
- The new namespace should be used in the YAML file too.
463
+ result = UserValidator.new(number: 23).validate
744
464
 
745
- ```yaml
746
- # config/messages.yml
747
- en:
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
- #### Internationalization (I18n)
469
+ result = UserValidator.new(number: 11).validate
757
470
 
758
- If your project already depends on `i18n` gem, `Hanami::Validations` is able to look at the translations defined for that gem and to use them.
759
-
760
-
761
- ```ruby
762
- class SignupValidator
763
- include Hanami::Validations
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
- ## Acknowledgements
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