hanami-validations 1.3.9 → 2.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,16 +1,14 @@
1
1
  # Hanami::Validations
2
2
 
3
- Validations mixin for Ruby objects
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
  [![Gem Version](https://badge.fury.io/rb/hanami-validations.svg)](https://badge.fury.io/rb/hanami-validations)
12
- [![CI](https://github.com/hanami/validations/workflows/ci/badge.svg?branch=1.3.x)](https://github.com/hanami/validations/actions?query=workflow%3Aci+branch%3A1.3.x)
13
- [![Test Coverage](https://codecov.io/gh/hanami/validations/branch/1.3.x/graph/badge.svg)](https://codecov.io/gh/hanami/validations)
8
+ [![TravisCI](https://travis-ci.org/hanami/validations.svg?branch=master)](https://travis-ci.org/hanami/validations)
9
+ [![CircleCI](https://circleci.com/gh/hanami/validations/tree/master.svg?style=svg)](https://circleci.com/gh/hanami/validations/tree/master)
10
+ [![Build Status](https://ci.hanamirb.org/api/badges/hanami/validations/status.svg)](https://ci.hanamirb.org/hanami/validations)
11
+ [![Test Coverage](https://codecov.io/gh/hanami/validations/branch/master/graph/badge.svg)](https://codecov.io/gh/hanami/validations)
14
12
  [![Depfu](https://badges.depfu.com/badges/af6c6be539d9d587c7541ae7a013c9ff/overview.svg)](https://depfu.com/github/hanami/validations?project=Bundler)
15
13
  [![Inline Docs](http://inch-ci.org/github/hanami/validations.svg)](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.3+ and JRuby 9.1.5.0+.
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 'hanami-validations'
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
- `Hanami::Validations` is a mixin that, once included by an object, adds lightweight set of validations to it.
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
- 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`.
55
+ ### Overview
80
56
 
81
- 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.
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
- A name must be filled and be a string and its size must be included between 3 and 64.
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
- **Please notice that we used `&` over Ruby's `&&` keyword.**
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
- 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`.
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
- 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.
68
+ require "hanami/validations"
116
69
 
117
- We already met `filled?` and `size?`, now let’s introduce the rest of them. They capture **common use cases with web forms**.
70
+ class SignupValidator < Hanami::Validator
71
+ schema do
72
+ required(:email).value(:string)
73
+ required(:age).value(:integer)
74
+ end
118
75
 
119
- ### Array
76
+ rule(:age) do
77
+ key.failure("must be greater than 18") if value < 18
78
+ end
79
+ end
120
80
 
121
- It checks if the the given value is an array, and iterates through its elements to perform checks on each of them.
81
+ validator = SignupValidator.new
122
82
 
123
- ```ruby
124
- required(:codes) { array? { each { int? } } }
125
- ```
83
+ result = validator.call(email: "user@hanamirb.test", age: 37)
84
+ result.success? # => true
126
85
 
127
- 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.
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
- ```ruby
130
- required(:codes) { array? { min_size?(2) & each { str? } } }
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
- #### Emptiness
95
+ ### Schemas
134
96
 
135
- It checks if the given value is empty or not. It is designed to works with strings and collections (array and hash).
97
+ A basic schema doesn't apply data coercion, input must already have the right Ruby types.
136
98
 
137
99
  ```ruby
138
- required(:tags) { empty? }
139
- ```
100
+ # frozen_string_literal: true
140
101
 
141
- #### Equality
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
- Ruby types are respected: `23` (an integer) is only equal to `23`, and not to `"23"` (a string). See _Type Safety_ section.
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
- #### Exclusion
111
+ validator = SignupValidator.new
152
112
 
153
- 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?`.
113
+ result = validator.call(email: "user@hanamirb.test", age: 37)
114
+ puts result.success? # => true
154
115
 
155
- ```ruby
156
- required(:genre) { excluded_from?(%w(pop dance)) }
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
- #### Format
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
- #### Greater Than
123
+ When used in _params mode_, a schema applies data coercion, before to run validation checks.
171
124
 
172
- This predicate works with numbers to check if input is **greater than** a given threshold.
125
+ This is designed for Web form/HTTP params.
173
126
 
174
127
  ```ruby
175
- required(:age) { gt?(18) }
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
- ```ruby
183
- required(:age) { gteq?(19) }
184
- ```
130
+ require "bundler/setup"
131
+ require "hanami/validations"
185
132
 
186
- #### Inclusion
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
- This predicate is the opposite of `#exclude?`: it verifies if the input is **included** in the given collection.
140
+ validator = SignupValidator.new
189
141
 
190
- ```ruby
191
- required(:genre) { included_in?(%w(rock folk)) }
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
- #### Less Than
147
+ ### JSON
195
148
 
196
- This is the complement of `#gt?`: it checks for **less than** numbers.
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
- required(:age) { lt?(7) }
200
- ```
153
+ # frozen_string_literal: true
201
154
 
202
- #### Less Than Equal
155
+ require "hanami/validations"
203
156
 
204
- 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.
205
-
206
- ```ruby
207
- required(:age) { lteq?(6) }
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
- #### Filled
164
+ validator = SignupValidator.new
211
165
 
212
- It’s a predicate that ensures data input is filled, that means **not** `nil` or blank (`""`) or empty (in case we expect a collection).
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
- ```ruby
215
- required(:name) { filled? } # string
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
- #### Minimum Size
174
+ ### Whitelisting
220
175
 
221
- This verifies that the size of the given input is at least of the specified value.
176
+ Unknown keys from incoming data are filtered out:
222
177
 
223
178
  ```ruby
224
- required(:password) { min_size?(12) }
225
- ```
179
+ # frozen_string_literal: true
226
180
 
227
- #### Maximum Size
181
+ require "hanami/validations"
228
182
 
229
- This verifies that the size of the given input is at max of the specified value.
230
-
231
- ```ruby
232
- required(:name) { max_size?(128) }
233
- ```
234
-
235
- #### None
183
+ class SignupValidator < Hanami::Validator
184
+ schema do
185
+ required(:email).value(:string)
186
+ end
187
+ end
236
188
 
237
- This verifies if the given input is `nil`. Blank strings (`""`) won’t pass this test and return `false`.
189
+ validator = SignupValidator.new
238
190
 
239
- ```ruby
240
- required(:location) { none? }
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
- #### Size
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
- required(:two_factor_auth_code) { size?(6) } # exact
249
- required(:password) { size?(8..32) } # range
250
- ```
199
+ # frozen_string_literal: true
251
200
 
252
- The check works with strings and collections.
201
+ require "hanami/validations"
253
202
 
254
- ```ruby
255
- required(:answers) { size?(2) } # only 2 answers are allowed
256
- ```
203
+ module Types
204
+ include Dry::Types()
257
205
 
258
- 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.
206
+ StrippedString = Types::String.constructor(&:strip)
207
+ end
259
208
 
260
- 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.
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
- ```ruby
263
- MEGABYTE = 1024 ** 2
216
+ validator = SignupValidator.new
264
217
 
265
- required(:avatar) { size?(1..(5 * MEGABYTE)) }
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
- ### Custom Predicates
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
- 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:
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
- require 'hanami/validations'
229
+ # frozen_string_literal: true
280
230
 
281
- class Signup
282
- include Hanami::Validations
231
+ require "hanami/validations"
283
232
 
284
- predicate :url?, message: 'must be an URL' do |current|
285
- # ...
233
+ class EventValidator < Hanami::Validator
234
+ params do
235
+ required(:start_date).value(:date)
286
236
  end
287
237
 
288
- validations do
289
- required(:website) { url? }
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
- #### Global Custom Predicates
243
+ validator = EventValidator.new
295
244
 
296
- If our goal is to share common used custom predicates, we can include them in a module to use in all our validators:
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
- ```ruby
299
- require 'hanami/validations'
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
- module MyPredicates
302
- include Hanami::Validations::Predicates
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
- self.messages_path = 'config/errors.yml'
258
+ Learn more about rules: https://dry-rb.org/gems/dry-validation/rules/
305
259
 
306
- predicate(:email?) do |current|
307
- current.match(/.../)
308
- end
309
- end
310
- ```
260
+ ### Inheritance
311
261
 
312
- We have defined a module `MyPredicates` with the purpose to share its custom predicates with all the validators that need them.
262
+ Schema and rules validations can be inherited and used by subclasses
313
263
 
314
264
  ```ruby
315
- require 'hanami/validations'
316
- require_relative 'my_predicates'
265
+ # frozen_string_literal: true
317
266
 
318
- class Signup
319
- include Hanami::Validations
320
- predicates MyPredicates
267
+ require "hanami/validations"
321
268
 
322
- validations do
323
- required(:email) { email? }
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 Signup
336
- include Hanami::Validations
337
-
338
- validations do
339
- required(:email) { ... }
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
- ```ruby
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
- To use when we expect a value to be filled:
389
-
390
- ```ruby
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
- ```ruby
398
- # expands to
399
- # required(:age) { filled? & type?(Integer) }
290
+ ### Messages
400
291
 
401
- required(:age).filled(:int?)
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
- #### Maybe
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
- # expands to
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
- To use when we want to apply the same validation rules to all the elements of an array:
429
-
430
- ```ruby
431
- # expands to
432
- # required(:tags) { array? { each { str? } } }
300
+ require "hanami/validations"
433
301
 
434
- required(:tags).each(:str?)
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 example above `tags` **must** be an array of strings.
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
- #### Confirmation
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
- This is designed to check if pairs of web form fields have the same value. One wildly popular example is _password confirmation_.
316
+ Our **suggestion** is to start with **specific** messages and see if there is a need to generalize them.
443
317
 
444
- ```ruby
445
- required(:password).filled.confirmation
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
- It is valid if the input has `password` and `password_confirmation` keys with the same exact value.
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 CreateJob
473
- include Hanami::Validations::Form
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
- ```ruby
511
- validations do
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
- Groups can be **deeply nested**, without any limitation.
345
+ validator = SignupValidator.new
521
346
 
522
- ```ruby
523
- validations do
524
- required(:customer).schema do
525
- # other validations …
347
+ result = validator.call(username: "foo")
348
+ puts result.success? # => true
526
349
 
527
- required(:address).schema do
528
- required(:street) { }
529
- # other address validations
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
- ### Composition
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
- As the code base grows, its a good practice to DRY validation rules.
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 AddressValidator
543
- include Hanami::Validations
544
-
545
- validations do
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
- validations do
558
- required(:email) { }
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
- Again, there is no limit to the nesting levels.
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
- **Please note that whitelisting is only available for `Hanami::Validations::Form` mixin.**
375
+ validator = SignupValidator.new
600
376
 
601
- ### Result
602
-
603
- 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.
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
- We can observe that:
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
- 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.
384
+ The interpolation mechanism, accepts extra, arbitrary information expressed as a `Hash` (e.g. `code: "123"`)
651
385
 
652
386
  ```ruby
653
- class SignupValidator
654
- include Hanami::Validations
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
- validations do
661
- required(:email).filled(:str?, :email?)
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
- result = SignupValidator.new(email: 'foo', age: 1).validate
397
+ validator = RefundValidator.new
667
398
 
668
- result.success? # => false
669
- result.messages.fetch(:email) # => ['must be an email']
670
- result.messages.fetch(:age) # => ['must be greater than 18']
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
- #### Configurable Error Messages
404
+ Learn more about messages: https://dry-rb.org/gems/dry-validation/messages/
674
405
 
675
- Inline error messages are ideal for quick and dirty development, but we suggest to use an external YAML file to configure these messages:
406
+ ### External dependencies
676
407
 
677
- ```yaml
678
- # config/messages.yml
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
- class SignupValidator
688
- include Hanami::Validations
689
- messages_path 'config/messages.yml'
412
+ # frozen_string_literal: true
690
413
 
691
- predicate :email? do |current|
692
- # ...
693
- end
414
+ require "hanami/validations"
694
415
 
695
- validations do
696
- required(:email).filled(:str?, :email?)
697
- required(:age).filled(:int?, gt?: 18)
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
- 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.
425
+ schema do
426
+ required(:address).filled(:string)
427
+ end
707
428
 
708
- ```yaml
709
- # config/messages.yml
710
- en:
711
- errors:
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
- rules:
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
- Now our validator is able to look at the right error message.
441
+ Read more about external dependencies: https://dry-rb.org/gems/dry-validation/external-dependencies/
722
442
 
723
- ```ruby
724
- result = SignupValidator.new(email: 'foo', age: 1).validate
443
+ ### Mixin
725
444
 
726
- result.success? # => false
727
- result.messages.fetch(:age) # => ['must be an adult']
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
- ⚠ **CONVENTION:** For a given validator named `SignupValidator`, the framework will look for `signup` translation key. ⚠
448
+ ```ruby
449
+ # frozen_string_literal: true
733
450
 
734
- If for some reason that doesn't work for us, we can customize the namespace:
451
+ require "hanami/validations"
735
452
 
736
- ```ruby
737
- class SignupValidator
453
+ class UserValidator
738
454
  include Hanami::Validations
739
455
 
740
- messages_path 'config/messages.yml'
741
- namespace :my_signup
742
-
743
- # ...
456
+ validations do
457
+ required(:number).filled(:integer, eql?: 23)
458
+ end
744
459
  end
745
- ```
746
460
 
747
- The new namespace should be used in the YAML file too.
461
+ result = UserValidator.new(number: 23).validate
748
462
 
749
- ```yaml
750
- # config/messages.yml
751
- en:
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
- #### Internationalization (I18n)
467
+ result = UserValidator.new(number: 11).validate
761
468
 
762
- 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.
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
- ## Acknowledgements
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-2021 Luca Guidi – Released under MIT License
494
+ Copyright © 2014-2019 Luca Guidi – Released under MIT License
807
495
 
808
496
  This project was formerly known as Lotus (`lotus-validations`).