hanami-validations 1.3.7 → 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: 1a260f62f3f079229e28d43c20ce88826e06d23aff8ddcd4e67b1bda812b87a2
4
- data.tar.gz: 2f0d8555be09a4a113570f99e810a76a141b7a6728aff90c073a8740cf0c2573
3
+ metadata.gz: c94b88f4b0a39d33e6ce6d5a27e9c843afc611b3a5d76ac48799c05648c73ed1
4
+ data.tar.gz: 93a768f5a5c977ada49f6f400bade010ebe87be3072ab7b1fdec6e36afe0f00a
5
5
  SHA512:
6
- metadata.gz: 3d6ce05e6ca243b83a4a7d0ab5b3d3c41fbf86ad552f68b7439bd4f57dcbc673747a86c74b417fdeec184cdb501f208fcba5b210c5bd0b163755e9b3ef0d9fbe
7
- data.tar.gz: c90675c4bd0ecf46a0bc9d9c6ec5e35b3d8e79270fe40abdb29c344fa8e37fc2eeb9adf3e8ae871e9428ded029ea8e3e312132d9b1fe1ed44a96250dcb9b1223
6
+ metadata.gz: 01bae680528b53b9564393aaa923f6e66f91d9ccda370e6297c6c458b0fbc127c83a39a1ba61d110bb7c698e28cb95e8ce8bb0f7f174bc151a384bb7b3bdb3d5
7
+ data.tar.gz: 15c2f866bae8f77acca611e4a70aefc6da227fc33aa556e14beff08e2725880e094a6de7fa9a5cbcff22fa8fce80f282f4623627cb206119821ebdab41e5fc6a
@@ -1,17 +1,23 @@
1
1
  # Hanami::Validations
2
2
  Validations mixin for Ruby objects
3
3
 
4
- ## v1.3.7 - 2021-01-06
5
- ### Fixed
6
- - [Panagiotis Matsinopoulos] Ensure `predicate` and `predicates` to work together
7
-
8
- ## v1.3.6 - 2020-01-08
4
+ ## v2.0.0.alpha1 - 2019-07-26
9
5
  ### Added
10
- - [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
11
12
 
12
- ## v1.3.5 - 2019-07-26
13
- ### Fixed
14
- - [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`)
15
21
 
16
22
  ## v1.3.4 - 2019-07-26
17
23
  ### Fixed
data/README.md CHANGED
@@ -1,11 +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
- [![CI](https://github.com/hanami/validations/workflows/ci/badge.svg?branch=master)](https://github.com/hanami/validations/actions?query=workflow%3Aci+branch%3Amaster)
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)
9
11
  [![Test Coverage](https://codecov.io/gh/hanami/validations/branch/master/graph/badge.svg)](https://codecov.io/gh/hanami/validations)
10
12
  [![Depfu](https://badges.depfu.com/badges/af6c6be539d9d587c7541ae7a013c9ff/overview.svg)](https://depfu.com/github/hanami/validations?project=Bundler)
11
13
  [![Inline Docs](http://inch-ci.org/github/hanami/validations.svg)](http://inch-ci.org/github/hanami/validations)
@@ -23,14 +25,14 @@ Validations mixin for Ruby objects
23
25
 
24
26
  ## Rubies
25
27
 
26
- __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+
27
29
 
28
30
  ## Installation
29
31
 
30
32
  Add this line to your application's Gemfile:
31
33
 
32
34
  ```ruby
33
- gem 'hanami-validations'
35
+ gem "hanami-validations"
34
36
  ```
35
37
 
36
38
  And then execute:
@@ -47,733 +49,426 @@ $ gem install hanami-validations
47
49
 
48
50
  ## Usage
49
51
 
50
- `Hanami::Validations` is a mixin that, once included by an object, adds lightweight set of validations to it.
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.
51
54
 
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 `|`).
55
+ ### Overview
53
56
 
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`.
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.
55
59
 
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
74
-
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`.
76
-
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.
78
-
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
100
-
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.
103
-
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`.
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.
105
62
 
63
+ Only when the input is formally valid (according to the **schema**), validation **rules** are checked.
106
64
 
107
- ⚠ **For this reason, we don't allow any arbitrary Ruby code to be executed, but only well defined predicates.** ⚠
108
-
109
- ### Predicates
65
+ ```ruby
66
+ # frozen_string_literal: true
110
67
 
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.
68
+ require "hanami/validations"
112
69
 
113
- 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
114
75
 
115
- ### Array
76
+ rule(:age) do
77
+ key.failure("must be greater than 18") if value < 18
78
+ end
79
+ end
116
80
 
117
- 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
118
82
 
119
- ```ruby
120
- required(:codes) { array? { each { int? } } }
121
- ```
83
+ result = validator.call(email: "user@hanamirb.test", age: 37)
84
+ result.success? # => true
122
85
 
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.
86
+ result = validator.call(email: "user@hanamirb.test", age: "foo")
87
+ result.success? # => false
88
+ result.errors.to_h # => {:age=>["must be an integer"]}
124
89
 
125
- ```ruby
126
- 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"]}
127
93
  ```
128
94
 
129
- #### Emptiness
95
+ ### Schemas
130
96
 
131
- 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.
132
98
 
133
99
  ```ruby
134
- required(:tags) { empty? }
135
- ```
100
+ # frozen_string_literal: true
136
101
 
137
- #### Equality
102
+ require "hanami/validations"
138
103
 
139
- This predicate tests if the input is equal to a given value.
140
-
141
- ```ruby
142
- required(:magic_number) { eql?(23) }
143
- ```
144
-
145
- 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
146
110
 
147
- #### Exclusion
111
+ validator = SignupValidator.new
148
112
 
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?`.
113
+ result = validator.call(email: "user@hanamirb.test", age: 37)
114
+ puts result.success? # => true
150
115
 
151
- ```ruby
152
- 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"]}
153
119
  ```
154
120
 
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
- ```
121
+ ### Params
165
122
 
166
- #### Greater Than
123
+ When used in _params mode_, a schema applies data coercion, before to run validation checks.
167
124
 
168
- This predicate works with numbers to check if input is **greater than** a given threshold.
125
+ This is designed for Web form/HTTP params.
169
126
 
170
127
  ```ruby
171
- required(:age) { gt?(18) }
172
- ```
173
-
174
- #### Greater Than Equal
128
+ # frozen_string_literal: true
175
129
 
176
- This is an _open boundary_ variation of `gt?`. It checks if an input is **greater than or equal** of a given number.
130
+ require "bundler/setup"
131
+ require "hanami/validations"
177
132
 
178
- ```ruby
179
- required(:age) { gteq?(19) }
180
- ```
181
-
182
- #### Inclusion
133
+ class SignupValidator < Hanami::Validator
134
+ params do
135
+ required(:email).value(:string)
136
+ required(:age).value(:integer)
137
+ end
138
+ end
183
139
 
184
- This predicate is the opposite of `#exclude?`: it verifies if the input is **included** in the given collection.
140
+ validator = SignupValidator.new
185
141
 
186
- ```ruby
187
- 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}
188
145
  ```
189
146
 
190
- #### Less Than
147
+ ### JSON
191
148
 
192
- 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.
193
151
 
194
152
  ```ruby
195
- required(:age) { lt?(7) }
196
- ```
153
+ # frozen_string_literal: true
197
154
 
198
- #### Less Than Equal
155
+ require "hanami/validations"
199
156
 
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
- ```
157
+ class SignupValidator < Hanami::Validator
158
+ json do
159
+ required(:email).value(:string)
160
+ required(:age).value(:integer)
161
+ end
162
+ end
205
163
 
206
- #### Filled
164
+ validator = SignupValidator.new
207
165
 
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).
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}
209
169
 
210
- ```ruby
211
- required(:name) { filled? } # string
212
- required(:languages) { filled? } # collection
170
+ result = validator.call(email: "user@hanamirb.test", age: "37")
171
+ puts result.success? # => false
213
172
  ```
214
173
 
215
- #### Minimum Size
174
+ ### Whitelisting
216
175
 
217
- 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:
218
177
 
219
178
  ```ruby
220
- required(:password) { min_size?(12) }
221
- ```
179
+ # frozen_string_literal: true
222
180
 
223
- #### Maximum Size
181
+ require "hanami/validations"
224
182
 
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
183
+ class SignupValidator < Hanami::Validator
184
+ schema do
185
+ required(:email).value(:string)
186
+ end
187
+ end
232
188
 
233
- This verifies if the given input is `nil`. Blank strings (`""`) won’t pass this test and return `false`.
189
+ validator = SignupValidator.new
234
190
 
235
- ```ruby
236
- 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"}
237
194
  ```
238
195
 
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.
196
+ ### Custom Types
242
197
 
243
198
  ```ruby
244
- required(:two_factor_auth_code) { size?(6) } # exact
245
- required(:password) { size?(8..32) } # range
246
- ```
199
+ # frozen_string_literal: true
247
200
 
248
- The check works with strings and collections.
201
+ require "hanami/validations"
249
202
 
250
- ```ruby
251
- required(:answers) { size?(2) } # only 2 answers are allowed
252
- ```
203
+ module Types
204
+ include Dry::Types()
253
205
 
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.
206
+ StrippedString = Types::String.constructor(&:strip)
207
+ end
255
208
 
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.
209
+ class SignupValidator < Hanami::Validator
210
+ params do
211
+ required(:email).value(Types::StrippedString)
212
+ required(:age).value(:integer)
213
+ end
214
+ end
257
215
 
258
- ```ruby
259
- MEGABYTE = 1024 ** 2
216
+ validator = SignupValidator.new
260
217
 
261
- 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}
262
221
  ```
263
222
 
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
223
+ ### Rules
271
224
 
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:
225
+ Rules are performing a set of domain-specific validation checks.
226
+ Rules are executed only after the validations from the schema are satisfied.
273
227
 
274
228
  ```ruby
275
- require 'hanami/validations'
229
+ # frozen_string_literal: true
276
230
 
277
- class Signup
278
- include Hanami::Validations
231
+ require "hanami/validations"
279
232
 
280
- predicate :url?, message: 'must be an URL' do |current|
281
- # ...
233
+ class EventValidator < Hanami::Validator
234
+ params do
235
+ required(:start_date).value(:date)
282
236
  end
283
237
 
284
- validations do
285
- required(:website) { url? }
238
+ rule(:start_date) do
239
+ key.failure("must be in the future") if value <= Date.today
286
240
  end
287
241
  end
288
- ```
289
242
 
290
- #### Global Custom Predicates
243
+ validator = EventValidator.new
291
244
 
292
- 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"]}
293
248
 
294
- ```ruby
295
- 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"]}
296
252
 
297
- module MyPredicates
298
- 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
+ ```
299
257
 
300
- self.messages_path = 'config/errors.yml'
258
+ Learn more about rules: https://dry-rb.org/gems/dry-validation/rules/
301
259
 
302
- predicate(:email?) do |current|
303
- current.match(/.../)
304
- end
305
- end
306
- ```
260
+ ### Inheritance
307
261
 
308
- 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
309
263
 
310
264
  ```ruby
311
- require 'hanami/validations'
312
- require_relative 'my_predicates'
265
+ # frozen_string_literal: true
313
266
 
314
- class Signup
315
- include Hanami::Validations
316
- predicates MyPredicates
267
+ require "hanami/validations"
317
268
 
318
- validations do
319
- required(:email) { email? }
269
+ class ApplicationValidator < Hanami::Validator
270
+ params do
271
+ optional(:_csrf_token).filled(:string)
320
272
  end
321
273
  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
-
331
- class Signup
332
- include Hanami::Validations
333
274
 
334
- validations do
335
- required(:email) { ... }
336
- optional(:referral) { ... }
275
+ class SignupValidator < ApplicationValidator
276
+ params do
277
+ required(:user).hash do
278
+ required(:email).filled(:string)
279
+ end
337
280
  end
338
281
  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
282
 
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?
283
+ validator = SignupValidator.new
346
284
 
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) }
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"}
351
288
  ```
352
289
 
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:
290
+ ### Messages
358
291
 
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?`)
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.
369
294
 
370
- For each supported type, there a convenient predicate that acts as an alias. For instance, the two lines of code below are **equivalent**.
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.
371
296
 
372
297
  ```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
383
-
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
391
- ```
392
-
393
- ```ruby
394
- # expands to
395
- # required(:age) { filled? & type?(Integer) }
396
-
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.
408
-
409
- #### Maybe
410
-
411
- To use when a value can be nil:
412
-
413
- ```ruby
414
- # expands to
415
- # required(:age) { none? | int? }
416
-
417
- required(:age).maybe(:int?)
418
- ```
298
+ # frozen_string_literal: true
419
299
 
420
- In the example above `age` can be `nil`, but if we send the value, it **must** be an integer.
300
+ require "hanami/validations"
421
301
 
422
- #### Each
423
-
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? } } }
429
-
430
- 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
431
306
  ```
432
307
 
433
- 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.
434
310
 
311
+ There are two ways to organize messages:
435
312
 
436
- #### 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.
437
315
 
438
- 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.
439
317
 
440
- ```ruby
441
- 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"
442
330
  ```
443
331
 
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:
332
+ #### General purpose messages
466
333
 
467
334
  ```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
335
+ class SignupValidator < ApplicationValidator
336
+ schema do
337
+ required(:username).filled(:string)
487
338
  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
339
 
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
-
506
- ```ruby
507
- validations do
508
- required(:customer).schema do
509
- required(:email) { … }
510
- required(:name) { … }
511
- # other validations …
340
+ rule(:username) do
341
+ key.failure(:taken) if values[:username] == "jodosha"
512
342
  end
513
343
  end
514
- ```
515
344
 
516
- Groups can be **deeply nested**, without any limitation.
345
+ validator = SignupValidator.new
517
346
 
518
- ```ruby
519
- validations do
520
- required(:customer).schema do
521
- # other validations …
347
+ result = validator.call(username: "foo")
348
+ puts result.success? # => true
522
349
 
523
- required(:address).schema do
524
- required(:street) { }
525
- # other address validations
526
- end
527
- end
528
- 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"]}
529
353
  ```
530
354
 
531
- ### Composition
355
+ #### Specific messages
532
356
 
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.
534
-
535
- As the code base grows, it’s 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.
536
358
 
537
359
  ```ruby
538
- class AddressValidator
539
- include Hanami::Validations
540
-
541
- validations do
542
- required(:street) { … }
360
+ class SignupValidator < ApplicationValidator
361
+ schema do
362
+ required(:email).filled(:string)
363
+ required(:age).filled(:integer)
543
364
  end
544
- end
545
- ```
546
-
547
- This validator can be reused by other validators.
548
365
 
549
- ```ruby
550
- class CustomerValidator
551
- include Hanami::Validations
552
-
553
- validations do
554
- required(:email) { … }
555
- required(:address).schema(AddressValidator)
366
+ rule(:email) do
367
+ key.failure(:invalid) unless values[:email] =~ /@/
556
368
  end
557
- end
558
- ```
559
-
560
- Again, there is no limit to the nesting levels.
561
369
 
562
- ```ruby
563
- class OrderValidator
564
- include Hanami::Validations
565
-
566
- validations do
567
- required(:number) { … }
568
- required(:customer).schema(CustomerValidator)
370
+ rule(:age) do
371
+ key.failure(:invalid) if values[:age] < 18
569
372
  end
570
373
  end
571
- ```
572
374
 
573
- In the end, `OrderValidator` is able to validate a complex data structure like this:
375
+ validator = SignupValidator.new
574
376
 
575
- ```ruby
576
- {
577
- number: "123",
578
- customer: {
579
- email: "user@example.com",
580
- address: {
581
- city: "Rome"
582
- }
583
- }
584
- }
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"]}
585
380
  ```
586
381
 
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
-
595
- **Please note that whitelisting is only available for `Hanami::Validations::Form` mixin.**
382
+ #### Extra information
596
383
 
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.
384
+ The interpolation mechanism, accepts extra, arbitrary information expressed as a `Hash` (e.g. `code: "123"`)
600
385
 
601
386
  ```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 }
633
- ```
634
-
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.
645
-
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.
647
-
648
- ```ruby
649
- class SignupValidator
650
- include Hanami::Validations
651
-
652
- predicate :email?, message: 'must be an email' do |current|
653
- # ...
387
+ class RefundValidator < ApplicationValidator
388
+ schema do
389
+ required(:refunded_code).filled(:string)
654
390
  end
655
391
 
656
- validations do
657
- required(:email).filled(:str?, :email?)
658
- required(:age).filled(:int?, gt?: 18)
392
+ rule(:refunded_code) do
393
+ key.failure(:network, code: "123") if values[:refunded_code] == "error"
659
394
  end
660
395
  end
661
396
 
662
- result = SignupValidator.new(email: 'foo', age: 1).validate
397
+ validator = RefundValidator.new
663
398
 
664
- result.success? # => false
665
- result.messages.fetch(:email) # => ['must be an email']
666
- 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)"]}
667
402
  ```
668
403
 
669
- #### Configurable Error Messages
404
+ Learn more about messages: https://dry-rb.org/gems/dry-validation/messages/
670
405
 
671
- 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
672
407
 
673
- ```yaml
674
- # config/messages.yml
675
- en:
676
- errors:
677
- email?: "must be an email"
678
- ```
679
-
680
- 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.
681
410
 
682
411
  ```ruby
683
- class SignupValidator
684
- include Hanami::Validations
685
- messages_path 'config/messages.yml'
412
+ # frozen_string_literal: true
686
413
 
687
- predicate :email? do |current|
688
- # ...
689
- end
414
+ require "hanami/validations"
690
415
 
691
- validations do
692
- required(:email).filled(:str?, :email?)
693
- required(:age).filled(:int?, gt?: 18)
416
+ class AddressValidator
417
+ def valid?(value)
418
+ value.match(/Rome/)
694
419
  end
695
420
  end
696
421
 
697
- ```
698
-
422
+ class DeliveryValidator < Hanami::Validator
423
+ option :address_validator
699
424
 
700
- #### Custom Error Messages
701
-
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.
425
+ schema do
426
+ required(:address).filled(:string)
427
+ end
703
428
 
704
- ```yaml
705
- # config/messages.yml
706
- en:
707
- errors:
708
- 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
709
433
 
710
- rules:
711
- signup:
712
- age:
713
- gt?: "must be an adult"
434
+ validator = DeliveryValidator.new(address_validator: AddressValidator.new)
714
435
 
436
+ result = validator.call(address: "foo")
437
+ puts result.success? # => false
438
+ puts result.errors.to_h # => {:address=>["not a valid address"]}
715
439
  ```
716
440
 
717
- 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/
718
442
 
719
- ```ruby
720
- result = SignupValidator.new(email: 'foo', age: 1).validate
443
+ ### Mixin
721
444
 
722
- result.success? # => false
723
- result.messages.fetch(:age) # => ['must be an adult']
724
- ```
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.
725
447
 
726
- ##### Custom namespace
727
-
728
- ⚠ **CONVENTION:** For a given validator named `SignupValidator`, the framework will look for `signup` translation key. ⚠
448
+ ```ruby
449
+ # frozen_string_literal: true
729
450
 
730
- If for some reason that doesn't work for us, we can customize the namespace:
451
+ require "hanami/validations"
731
452
 
732
- ```ruby
733
- class SignupValidator
453
+ class UserValidator
734
454
  include Hanami::Validations
735
455
 
736
- messages_path 'config/messages.yml'
737
- namespace :my_signup
738
-
739
- # ...
456
+ validations do
457
+ required(:number).filled(:integer, eql?: 23)
458
+ end
740
459
  end
741
- ```
742
460
 
743
- The new namespace should be used in the YAML file too.
461
+ result = UserValidator.new(number: 23).validate
744
462
 
745
- ```yaml
746
- # config/messages.yml
747
- en:
748
- # ...
749
- rules:
750
- my_signup:
751
- age:
752
- gt?: "must be an adult"
753
-
754
- ```
755
-
756
- #### Internationalization (I18n)
757
-
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
463
+ puts result.success? # => true
464
+ puts result.to_h # => {:number=>23}
465
+ puts result.errors.to_h # => {}
764
466
 
765
- messages :i18n
766
-
767
- # ...
768
- end
769
- ```
467
+ result = UserValidator.new(number: 11).validate
770
468
 
771
- ```yaml
772
- # config/locales/en.yml
773
- en:
774
- errors:
775
- signup:
776
- # ...
469
+ puts result.success? # => true
470
+ puts result.to_h # => {:number=>21}
471
+ puts result.errors.to_h # => {:number=>["must be equal to 23"]}
777
472
  ```
778
473
 
779
474
  ## FAQs
@@ -784,10 +479,7 @@ Please remember that **uniqueness validation is a huge race condition between ap
784
479
 
785
480
  Please read more at: [The Perils of Uniqueness Validations](http://robots.thoughtbot.com/the-perils-of-uniqueness-validations).
786
481
 
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.
482
+ If you need to implement it, please use the External dependencies feature (see above).
791
483
 
792
484
  ## Contributing
793
485
 
@@ -799,6 +491,6 @@ Thanks to [dry-rb](http://dry-rb.org) Community for their priceless support. ❤
799
491
 
800
492
  ## Copyright
801
493
 
802
- Copyright © 2014-2021 Luca Guidi – Released under MIT License
494
+ Copyright © 2014-2019 Luca Guidi – Released under MIT License
803
495
 
804
496
  This project was formerly known as Lotus (`lotus-validations`).