hanami-validations 0.5.0 → 0.6.0

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