composable_validations 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|