composable_validations 0.0.9 → 0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +74 -0
- data/lib/composable_validations/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbb778464163c80eb1df8ec60299ce2d2d399ffd
|
4
|
+
data.tar.gz: f4e54cf70891ae58da5485e1662fe4233ef41865
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45e7ec65e32b08f8bd25834d5fcd513a4514813b5a57780b688dc80f176192df81a458fefd15e66b0f388356ef1e5dd666fb2b4b5657018e3c40e789ce82dbc6
|
7
|
+
data.tar.gz: 100912344daee564a5c1770f48cccc16124f2599a42b5edd6fdb574cde320e3211c9cda90be3ffbeeb565825b11327cbb73b53a3de166c33f491c636b2b3528b
|
data/README.md
CHANGED
@@ -66,9 +66,11 @@ end
|
|
66
66
|
```
|
67
67
|
In the example above the payload is invalid and as a result `valid` has value
|
68
68
|
`false` and `errors` contains:
|
69
|
+
|
69
70
|
```
|
70
71
|
{"person/name"=>["must be a string"], "person/age"=>["must be an integer"]}
|
71
72
|
```
|
73
|
+
|
72
74
|
Note that invalid elements of the payload are identified by exact path within
|
73
75
|
the payload.
|
74
76
|
|
@@ -123,6 +125,7 @@ end
|
|
123
125
|
The previous examples showed validation of a JSON object. We can also
|
124
126
|
validate JSON arrays. Let's add list of hobbies to our person object from
|
125
127
|
the previous examples:
|
128
|
+
|
126
129
|
```ruby
|
127
130
|
{
|
128
131
|
"person" => {
|
@@ -132,8 +135,10 @@ the previous examples:
|
|
132
135
|
}
|
133
136
|
}
|
134
137
|
```
|
138
|
+
|
135
139
|
We will also not accept people with fewer than two hobbies. Validator for this
|
136
140
|
payload:
|
141
|
+
|
137
142
|
```ruby
|
138
143
|
a_hash(
|
139
144
|
allowed_keys("person"),
|
@@ -145,13 +150,17 @@ a_hash(
|
|
145
150
|
min_size(2),
|
146
151
|
each(non_empty_string))))))
|
147
152
|
```
|
153
|
+
|
148
154
|
Try to apply this validator to the payload containing invalid list of hobbies
|
155
|
+
|
149
156
|
```
|
150
157
|
...
|
151
158
|
"hobbies" => ["knitting", {"not" => "allowed"}, "horse riding"]
|
152
159
|
...
|
153
160
|
```
|
161
|
+
|
154
162
|
and you'll get errors specifying exactly where the invalid element is:
|
163
|
+
|
155
164
|
```
|
156
165
|
{"person/hobbies/1"=>["must be a string"]}
|
157
166
|
```
|
@@ -175,9 +184,11 @@ validators](#custom-validators).
|
|
175
184
|
### Validators
|
176
185
|
|
177
186
|
Validator is a function returning boolean value and having following signature:
|
187
|
+
|
178
188
|
```ruby
|
179
189
|
lambda { |validated_object, errors_hash, path| ... }
|
180
190
|
```
|
191
|
+
|
181
192
|
* `errors_hash` is mutated while errors are collected by validators.
|
182
193
|
* `path` represents a path to the invalid element within the JSON object. It is
|
183
194
|
an array of strings (keys in hash map) and integers (indexes of an array).
|
@@ -209,45 +220,60 @@ be further composed into more powerful validation rules.
|
|
209
220
|
|
210
221
|
We want to validate object representing opening hours of a store. E.g. store
|
211
222
|
opened from 9am to 5pm would be represented by
|
223
|
+
|
212
224
|
```ruby
|
213
225
|
{"from" => 9, "to" => 17}
|
214
226
|
```
|
227
|
+
|
215
228
|
Let's start by building validator ensuring that payload is a hash where both
|
216
229
|
`from` and `to` are integers:
|
230
|
+
|
217
231
|
```ruby
|
218
232
|
a_hash(
|
219
233
|
key("from", integer),
|
220
234
|
key("to", integer))
|
221
235
|
```
|
236
|
+
|
222
237
|
We also want to make sure that extra keys like
|
238
|
+
|
223
239
|
```ruby
|
224
240
|
{"from" => 9, "to" => 17, "something" => "wrong"}
|
225
241
|
```
|
242
|
+
|
226
243
|
are not allowed. Let's fix it by using `allowed_keys` validator:
|
244
|
+
|
227
245
|
```ruby
|
228
246
|
a_hash(
|
229
247
|
allowed_keys("from", "to"),
|
230
248
|
key("from", integer),
|
231
249
|
key("to", integer))
|
232
250
|
```
|
251
|
+
|
233
252
|
Better, but we don't want to allow negative hours like this:
|
253
|
+
|
234
254
|
```ruby
|
235
255
|
{"from" => -1, "to" => 17}
|
236
256
|
```
|
257
|
+
|
237
258
|
We can fix it by using more specific integer validator:
|
259
|
+
|
238
260
|
```ruby
|
239
261
|
a_hash(
|
240
262
|
allowed_keys("from", "to"),
|
241
263
|
key("from", non_negative_integer),
|
242
264
|
key("to", non_negative_integer))
|
243
265
|
```
|
266
|
+
|
244
267
|
Let's assume here that we represent store opened all day as
|
268
|
+
|
245
269
|
```ruby
|
246
270
|
{"from" => 0, "to" => 24}
|
247
271
|
```
|
272
|
+
|
248
273
|
so hours greater than 24 should also be invalid. We can validate hour by
|
249
274
|
composing `non_negative_integer` validator with `less_or_equal` using
|
250
275
|
`fail_fast` combinator:
|
276
|
+
|
251
277
|
```ruby
|
252
278
|
hour = fail_fast(non_negative_integer, less_or_equal(24))
|
253
279
|
|
@@ -256,17 +282,23 @@ a_hash(
|
|
256
282
|
key("from", hour),
|
257
283
|
key("to", hour))
|
258
284
|
```
|
285
|
+
|
259
286
|
This validator still has a little problem. Opening hours like this are not
|
260
287
|
rejected:
|
288
|
+
|
261
289
|
```ruby
|
262
290
|
{"from" => 21, "to" => 1}
|
263
291
|
```
|
292
|
+
|
264
293
|
We have to make sure that closing is not before opening. We can do it by using
|
265
294
|
`key_greater_than_key` validator:
|
295
|
+
|
266
296
|
```ruby
|
267
297
|
key_greater_than_key("to", "from")
|
268
298
|
```
|
299
|
+
|
269
300
|
and our validator will look like this:
|
301
|
+
|
270
302
|
```ruby
|
271
303
|
a_hash(
|
272
304
|
allowed_keys("from", "to"),
|
@@ -274,15 +306,19 @@ a_hash(
|
|
274
306
|
key("to", hour),
|
275
307
|
key_greater_than_key("to", "from"))
|
276
308
|
```
|
309
|
+
|
277
310
|
That looks good, but it's not complete yet. `a_hash` validator applies all
|
278
311
|
validators to the provided payload by using `run_all` combinator. This
|
279
312
|
behaviour is problematic if our `from` or `to` keys are missing or are not
|
280
313
|
valid integers. Payload
|
314
|
+
|
281
315
|
```ruby
|
282
316
|
{"from" => "abc", "to" => 17}
|
283
317
|
```
|
318
|
+
|
284
319
|
will cause an exception as `key_greater_than_key` can not compare string to
|
285
320
|
integer. Let's fix it by using `fail_fast` and `run_all` combinators:
|
321
|
+
|
286
322
|
```ruby
|
287
323
|
a_hash(
|
288
324
|
allowed_keys("from", "to"),
|
@@ -292,6 +328,7 @@ a_hash(
|
|
292
328
|
key("to", hour)),
|
293
329
|
key_greater_than_key("to", "from")))
|
294
330
|
```
|
331
|
+
|
295
332
|
This way if `from` and `to` are not both valid hours we will not be comparing
|
296
333
|
them.
|
297
334
|
|
@@ -326,6 +363,7 @@ store = {
|
|
326
363
|
```
|
327
364
|
|
328
365
|
Definition of the store validator (using `from_to` built in the [previous section](#composability)):
|
366
|
+
|
329
367
|
```ruby
|
330
368
|
hour = fail_fast(non_negative_integer, less_or_equal(24))
|
331
369
|
|
@@ -359,21 +397,27 @@ store_validator = a_hash(
|
|
359
397
|
|
360
398
|
Let's say we try to validate store that has Wednesday opening hours invalid
|
361
399
|
(closing time before opening time) like this:
|
400
|
+
|
362
401
|
```ruby
|
363
402
|
...
|
364
403
|
"wednesday"=> {"from" => 9, "to" => 7},
|
365
404
|
...
|
366
405
|
```
|
406
|
+
|
367
407
|
Now we use store validator to fill in the collection of errors using default
|
368
408
|
error messages:
|
409
|
+
|
369
410
|
```ruby
|
370
411
|
errors = {}
|
371
412
|
result = default_errors(store_validator).call(store, errors)
|
372
413
|
```
|
414
|
+
|
373
415
|
Result is `false` and we get validation error in the `errors` hash:
|
416
|
+
|
374
417
|
```ruby
|
375
418
|
{"store/opening_hours/wednesday/to" => ["must be greater than from"]}
|
376
419
|
```
|
420
|
+
|
377
421
|
You can find this example in
|
378
422
|
[functional spec](https://github.com/shutl/composable_validations/blob/master/spec/functional/composable_validations_spec.rb)
|
379
423
|
ready for experiments.
|
@@ -387,11 +431,13 @@ There are few ways to provide your own error messages.
|
|
387
431
|
### Local override
|
388
432
|
|
389
433
|
You can override error message when building your validator:
|
434
|
+
|
390
435
|
```ruby
|
391
436
|
a_hash(
|
392
437
|
key("from", integer("custom error message")),
|
393
438
|
key("to", integer("another custom error message")))
|
394
439
|
```
|
440
|
+
|
395
441
|
This approach is good if you need just few specialized error messages for
|
396
442
|
different parts of your payload.
|
397
443
|
|
@@ -400,6 +446,7 @@ different parts of your payload.
|
|
400
446
|
If you need to change some of the error messages across all your validators you
|
401
447
|
can provide map of error messages. Keys in the map are symbols matching names
|
402
448
|
of basic validators:
|
449
|
+
|
403
450
|
```ruby
|
404
451
|
error_overrides = {
|
405
452
|
string: "not a string",
|
@@ -410,9 +457,11 @@ of basic validators:
|
|
410
457
|
errors_container = ComposableValidations::Errors.new(errors, error_overrides)
|
411
458
|
result = validator.call(valid_data, errors_container, nil)
|
412
459
|
```
|
460
|
+
|
413
461
|
Note that your error messages don't need to be strings. You could for example
|
414
462
|
use rendering function that returns combination of error code, error context
|
415
463
|
and human readable message:
|
464
|
+
|
416
465
|
```ruby
|
417
466
|
error_overrides = {
|
418
467
|
key_greater_than_key: lambda do |validated_object, path, key1, key2|
|
@@ -428,7 +477,9 @@ and human readable message:
|
|
428
477
|
errors_container = ComposableValidations::Errors.new(errors, error_overrides)
|
429
478
|
result = validator.call(valid_data, errors_container, nil)
|
430
479
|
```
|
480
|
+
|
431
481
|
And when applied to invalid payload your validator will return an error:
|
482
|
+
|
432
483
|
```ruby
|
433
484
|
{
|
434
485
|
"store/opening_hours/wednesday/to"=>
|
@@ -441,15 +492,18 @@ And when applied to invalid payload your validator will return an error:
|
|
441
492
|
]
|
442
493
|
}
|
443
494
|
```
|
495
|
+
|
444
496
|
You can experiment with this example in the [specs](https://github.com/shutl/composable_validations/blob/master/spec/functional/error_overrides_spec.rb#L19).
|
445
497
|
|
446
498
|
### Override error container
|
447
499
|
|
448
500
|
You can override error container class and provide any error collecting
|
449
501
|
behaviour you need. The only method error container must provide is:
|
502
|
+
|
450
503
|
```ruby
|
451
504
|
def add(msg, path, object)
|
452
505
|
```
|
506
|
+
|
453
507
|
where
|
454
508
|
* `msg` is a symbol of an error or an array where first element is a symbol of
|
455
509
|
error and remaining elements are context needed to render the error message.
|
@@ -458,6 +512,7 @@ error and remaining elements are context needed to render the error message.
|
|
458
512
|
* `object` is a validated object.
|
459
513
|
|
460
514
|
Example of error container that just collects error paths:
|
515
|
+
|
461
516
|
```ruby
|
462
517
|
class CollectPaths
|
463
518
|
attr_reader :paths
|
@@ -475,18 +530,23 @@ validator = ...
|
|
475
530
|
errors_container = CollectPaths.new
|
476
531
|
result = validator.call(valid_data, errors_container, nil)
|
477
532
|
```
|
533
|
+
|
478
534
|
and example of the value of `errors_container.paths` after getting an error:
|
535
|
+
|
479
536
|
```ruby
|
480
537
|
[["store", "opening_hours", "wednesday", "to"]]
|
481
538
|
```
|
539
|
+
|
482
540
|
You can experiment with this example in the [spec](https://github.com/shutl/composable_validations/blob/master/spec/functional/error_overrides_spec.rb#L80).
|
483
541
|
|
484
542
|
## Custom validators
|
485
543
|
|
486
544
|
You can create your own validators as functions returning lambdas with signature
|
545
|
+
|
487
546
|
```ruby
|
488
547
|
lambda { |validated_object, errors_hash, path| ... }
|
489
548
|
```
|
549
|
+
|
490
550
|
Use `error` helper function to add errors to the error container and
|
491
551
|
functions `validate`, `precheck` and `nil_or` to avoid boilerplate.
|
492
552
|
|
@@ -494,10 +554,13 @@ functions `validate`, `precheck` and `nil_or` to avoid boilerplate.
|
|
494
554
|
|
495
555
|
Let's say we have an ActiveRecord model Store and API allowing update of the
|
496
556
|
store name. We will be receiving payload:
|
557
|
+
|
497
558
|
```ruby
|
498
559
|
{ name: 'new store name' }
|
499
560
|
```
|
561
|
+
|
500
562
|
We can build validator ensuring uniqueness of the store name:
|
563
|
+
|
501
564
|
```ruby
|
502
565
|
a_hash(
|
503
566
|
allowed_keys('name'),
|
@@ -505,7 +568,9 @@ a_hash(
|
|
505
568
|
non_empty_string,
|
506
569
|
unique_store_name))
|
507
570
|
```
|
571
|
+
|
508
572
|
where `unique_store_name` is defined as:
|
573
|
+
|
509
574
|
```ruby
|
510
575
|
def unique_store_name
|
511
576
|
lambda do |store_name, errors, path|
|
@@ -517,7 +582,9 @@ def unique_store_name
|
|
517
582
|
end
|
518
583
|
end
|
519
584
|
```
|
585
|
+
|
520
586
|
Note that we could simplify this code by using `validate` helper method:
|
587
|
+
|
521
588
|
```ruby
|
522
589
|
def unique_store_name
|
523
590
|
validate("has already been taken") do |store_name|
|
@@ -525,8 +592,10 @@ def unique_store_name
|
|
525
592
|
end
|
526
593
|
end
|
527
594
|
```
|
595
|
+
|
528
596
|
We could also generalize this function and end up with generic ActiveModel
|
529
597
|
attribute uniqueness validator ready to be reused:
|
598
|
+
|
530
599
|
```ruby
|
531
600
|
def unique(klass, attr_name)
|
532
601
|
validate("has already been taken") do |attr_value|
|
@@ -565,12 +634,14 @@ a_hash(
|
|
565
634
|
default implementation of the error collection object. Returned function is
|
566
635
|
not a composable validator so it should only be applied to the top level
|
567
636
|
validator right before applying it to the object. Example:
|
637
|
+
|
568
638
|
```ruby
|
569
639
|
errors = {}
|
570
640
|
default_errors(validator).call(validated_object, errors)
|
571
641
|
```
|
572
642
|
|
573
643
|
* `each_in_slice(range, validator)` - applies `validator` to each slice of the array. Example:
|
644
|
+
|
574
645
|
```ruby
|
575
646
|
array(
|
576
647
|
each_in_slice(0..-2, normal_element_validator),
|
@@ -699,6 +770,7 @@ a_hash(
|
|
699
770
|
returns `true` if `&blk` returns `true` or applies all `validators` using
|
700
771
|
`run_all` combinator if `&blk` returns `false`. Example - validate that value
|
701
772
|
is a number but also allow value "infinity":
|
773
|
+
|
702
774
|
```ruby
|
703
775
|
precheck(float) { |v| v == 'infinity' }
|
704
776
|
```
|
@@ -730,6 +802,8 @@ a_hash(
|
|
730
802
|
returns `true` if `&blk` returns `true` and `false` otherwise. `msg` is an
|
731
803
|
error message added to the error container when validation returns `false`.
|
732
804
|
Example - ensure that validated object is equal "hello":
|
805
|
+
|
733
806
|
```ruby
|
734
807
|
validate('must be "hello"') { |v| v == 'hello' }
|
735
808
|
```
|
809
|
+
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: composable_validations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kajetan Bojko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|