hanami-validations 1.3.6 → 2.0.0.alpha1

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