composable_validations 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +735 -0
- data/lib/composable_validations/combinators.rb +32 -0
- data/lib/composable_validations/comparison.rb +65 -0
- data/lib/composable_validations/default_error_messages.rb +40 -0
- data/lib/composable_validations/errors.rb +48 -0
- data/lib/composable_validations/utils.rb +29 -0
- data/lib/composable_validations/version.rb +3 -0
- data/lib/composable_validations.rb +218 -0
- metadata +67 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 859b71a70691f26e28d171883bd5297af8897692
|
4
|
+
data.tar.gz: a2e612c3c41de11b37a828a4cf8466c01a6682b3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 35890438972b2e2898fe20aea2a8e25184b19ff863df5a69208ae4da10d66996cdc50c2b6dff42efb5cd6bb517c02a3b62f2b3cf928baf00564add5b1463ad67
|
7
|
+
data.tar.gz: 1613d3040d41f1fdbbefbfe4098bf4c901711e1a4eeee76a5d4a651da0f5846e57e8266228c5f04480d6a3c07e9f49dd7967409e2eb78a7dfaa64e334c665b15
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2016 Shutl Ltd
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,735 @@
|
|
1
|
+
# Composable Validations
|
2
|
+
|
3
|
+
Gem for validating complex JSON payloads.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
* allows composition of generic validators into readable and reusable
|
7
|
+
validation rules ([Composability](#composability))
|
8
|
+
* returned errors always contain exact path of the invalid payload element
|
9
|
+
([Path to invalid element](#path-to-an-invalid-element))
|
10
|
+
* easy to extend with new validators ([Custom validators](#custom-validators))
|
11
|
+
* overridable error messages ([Overriding error messages](#overriding-error-messages))
|
12
|
+
|
13
|
+
## Requirements
|
14
|
+
|
15
|
+
* Ruby 2+
|
16
|
+
* A tolerance to parantheses... the validator code has rather "lispy" functional look and feel
|
17
|
+
|
18
|
+
## Install
|
19
|
+
|
20
|
+
```
|
21
|
+
gem install composable_validations
|
22
|
+
```
|
23
|
+
|
24
|
+
## Quick guide
|
25
|
+
|
26
|
+
This gem allows you to build a validator - how/when you call this validation is up to you.
|
27
|
+
|
28
|
+
### Basic example
|
29
|
+
|
30
|
+
Say we want to validate a payload that specifies a person with name and age. E.g. `{"person" => {"name"
|
31
|
+
=> "Bob", "age" => 28}}`
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require 'composable_validations'
|
35
|
+
|
36
|
+
include ComposableValidations
|
37
|
+
|
38
|
+
# building validator function
|
39
|
+
validator = a_hash(
|
40
|
+
allowed_keys("person"),
|
41
|
+
key("person", a_hash(
|
42
|
+
allowed_keys("name", "age"),
|
43
|
+
key("name", non_empty_string),
|
44
|
+
key("age", non_negative_integer))))
|
45
|
+
|
46
|
+
# invalid payload with non-integer age
|
47
|
+
payload = {
|
48
|
+
"person" => {
|
49
|
+
"name" => 123,
|
50
|
+
"age" => "mistake!"
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
# container for error messages
|
55
|
+
errors = {}
|
56
|
+
|
57
|
+
# application of the validator to the payload with default error messages
|
58
|
+
valid = default_errors(validator).call(payload, errors)
|
59
|
+
|
60
|
+
if valid
|
61
|
+
puts "payload is valid"
|
62
|
+
else
|
63
|
+
# examine error messages collected by validator
|
64
|
+
puts errors.inspect
|
65
|
+
end
|
66
|
+
```
|
67
|
+
In the example above the payload is invalid and as a result `valid` has value
|
68
|
+
`false` and `errors` contains:
|
69
|
+
```
|
70
|
+
{"person/name"=>["must be a string"], "person/age"=>["must be an integer"]}
|
71
|
+
```
|
72
|
+
Note that invalid elements of the payload are identified by exact path within
|
73
|
+
the payload.
|
74
|
+
|
75
|
+
### Sinatra app
|
76
|
+
|
77
|
+
When using this gem in your application code you would only include
|
78
|
+
`ComposableValidations` module in classes responsible for validation.
|
79
|
+
|
80
|
+
Extending previous example into Sinatra app:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
require 'sinatra'
|
84
|
+
require 'json'
|
85
|
+
require 'composable_validations'
|
86
|
+
|
87
|
+
post '/' do
|
88
|
+
payload = JSON.parse(request.body.read)
|
89
|
+
validator = PersonValidator.new(payload)
|
90
|
+
|
91
|
+
if validator.valid?
|
92
|
+
status(204)
|
93
|
+
else
|
94
|
+
status(422)
|
95
|
+
validator.errors.to_json
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class PersonValidator
|
100
|
+
include ComposableValidations
|
101
|
+
attr_reader :errors
|
102
|
+
|
103
|
+
def initialize(payload)
|
104
|
+
@payload = payload
|
105
|
+
@errors = {}
|
106
|
+
|
107
|
+
@validator = a_hash(
|
108
|
+
allowed_keys("person"),
|
109
|
+
key("person", a_hash(
|
110
|
+
allowed_keys("name", "age"),
|
111
|
+
key("name", non_empty_string),
|
112
|
+
key("age", non_negative_integer))))
|
113
|
+
end
|
114
|
+
|
115
|
+
def valid?
|
116
|
+
default_errors(@validator).call(@payload, @errors)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
### Arrays
|
122
|
+
|
123
|
+
The previous examples showed validation of a JSON object. We can also
|
124
|
+
validate JSON arrays. Let's add list of hobbies to our person object from
|
125
|
+
the previous examples:
|
126
|
+
```ruby
|
127
|
+
{
|
128
|
+
"person" => {
|
129
|
+
"name" => "Bob",
|
130
|
+
"age" => 28,
|
131
|
+
"hobbies" => ["knitting", "horse riding"]
|
132
|
+
}
|
133
|
+
}
|
134
|
+
```
|
135
|
+
We will also not accept people with fewer than two hobbies. Validator for this
|
136
|
+
payload:
|
137
|
+
```ruby
|
138
|
+
a_hash(
|
139
|
+
allowed_keys("person"),
|
140
|
+
key("person", a_hash(
|
141
|
+
allowed_keys("name", "age", "hobbies"),
|
142
|
+
key("name", non_empty_string),
|
143
|
+
key("age", non_negative_integer),
|
144
|
+
key("hobbies", array(
|
145
|
+
min_size(2),
|
146
|
+
each(non_empty_string))))))
|
147
|
+
```
|
148
|
+
Try to apply this validator to the payload containing invalid list of hobbies
|
149
|
+
```
|
150
|
+
...
|
151
|
+
"hobbies" => ["knitting", {"not" => "allowed"}, "horse riding"]
|
152
|
+
...
|
153
|
+
```
|
154
|
+
and you'll get errors specifying exactly where the invalid element is:
|
155
|
+
```
|
156
|
+
{"person/hobbies/1"=>["must be a string"]}
|
157
|
+
```
|
158
|
+
|
159
|
+
### Dependent validations
|
160
|
+
|
161
|
+
Sometimes we need to ensure that elements of the payload are in certain
|
162
|
+
relation.
|
163
|
+
|
164
|
+
We can ensure simple relations between keys using validators
|
165
|
+
`key_greater_than_key`, `key_less_than_key` etc. Check out
|
166
|
+
[Composability](#composability) for example of simple relation between keys.
|
167
|
+
|
168
|
+
### Uniqueness
|
169
|
+
|
170
|
+
For uniqueness validation follow the example in [Custom
|
171
|
+
validators](#custom-validators).
|
172
|
+
|
173
|
+
## Key concepts
|
174
|
+
|
175
|
+
### Validators
|
176
|
+
|
177
|
+
Validator is a function returning boolean value and having following signature:
|
178
|
+
```ruby
|
179
|
+
lambda { |validated_object, errors_hash, path| ... }
|
180
|
+
```
|
181
|
+
* `errors_hash` is mutated while errors are collected by validators.
|
182
|
+
* `path` represents a path to the invalid element within the JSON object. It is
|
183
|
+
an array of strings (keys in hash map) and integers (indexes of an array).
|
184
|
+
E.g. if validated payload is `{"numbers" => [1, 2, "abc", 4]}`, path to
|
185
|
+
invalid element "abc" is `["numbers", 2]`.
|
186
|
+
|
187
|
+
This gem comes with basic validators like `a_hash`, `array`, `string`,
|
188
|
+
`integer`, `float`, `date_string`, etc. You can find complete list of
|
189
|
+
validators [below](#api). Adding new validators is explained in ([Custom
|
190
|
+
validators](#custom-validators)).
|
191
|
+
|
192
|
+
### Combinators
|
193
|
+
|
194
|
+
Validators can be composed using two combinators:
|
195
|
+
|
196
|
+
* `run_all(*validators)` - applies all validators collecting errors from all of
|
197
|
+
them and returning false if any of the validators returns false. Useful when
|
198
|
+
collecting errors of independent validators e.g. fields of the hash.
|
199
|
+
|
200
|
+
* `fail_fast(*validators)` - applies validators returning false on first failing
|
201
|
+
validator. Useful when using validators depending on some preconditions. For
|
202
|
+
example when checking that a value is non negative, you want to ensure first
|
203
|
+
that it is a number: `fail_fast(float, non_negative)`.
|
204
|
+
|
205
|
+
Return values of above combinators are themselves validators. This way they can
|
206
|
+
be further composed into more powerful validation rules.
|
207
|
+
|
208
|
+
### Composability
|
209
|
+
|
210
|
+
We want to validate object representing opening hours of a store. E.g. store
|
211
|
+
opened from 9am to 5pm would be represented by
|
212
|
+
```ruby
|
213
|
+
{"from" => 9, "to" => 17}
|
214
|
+
```
|
215
|
+
Let's start by building validator ensuring that payload is a hash where both
|
216
|
+
`from` and `to` are integers:
|
217
|
+
```ruby
|
218
|
+
a_hash(
|
219
|
+
key("from", integer),
|
220
|
+
key("to", integer))
|
221
|
+
```
|
222
|
+
We also want to make sure that extra keys like
|
223
|
+
```ruby
|
224
|
+
{"from" => 9, "to" => 17, "something" => "wrong"}
|
225
|
+
```
|
226
|
+
are not allowed. Let's fix it by using `allowed_keys` validator:
|
227
|
+
```ruby
|
228
|
+
a_hash(
|
229
|
+
allowed_keys("from", "to"),
|
230
|
+
key("from", integer),
|
231
|
+
key("to", integer))
|
232
|
+
```
|
233
|
+
Better, but we don't want to allow negative hours like this:
|
234
|
+
```ruby
|
235
|
+
{"from" => -1, "to" => 17}
|
236
|
+
```
|
237
|
+
We can fix it by using more specific integer validator:
|
238
|
+
```ruby
|
239
|
+
a_hash(
|
240
|
+
allowed_keys("from", "to"),
|
241
|
+
key("from", non_negative_integer),
|
242
|
+
key("to", non_negative_integer))
|
243
|
+
```
|
244
|
+
Let's assume here that we represent store opened all day as
|
245
|
+
```ruby
|
246
|
+
{"from" => 0, "to" => 24}
|
247
|
+
```
|
248
|
+
so hours greater than 24 should also be invalid. We can validate hour by
|
249
|
+
composing `non_negative_integer` validator with `less_or_equal` using
|
250
|
+
`fail_fast` combinator:
|
251
|
+
```ruby
|
252
|
+
hour = fail_fast(non_negative_integer, less_or_equal(24))
|
253
|
+
|
254
|
+
a_hash(
|
255
|
+
allowed_keys("from", "to"),
|
256
|
+
key("from", hour),
|
257
|
+
key("to", hour))
|
258
|
+
```
|
259
|
+
This validator still has a little problem. Opening hours like this are not
|
260
|
+
rejected:
|
261
|
+
```ruby
|
262
|
+
{"from" => 21, "to" => 1}
|
263
|
+
```
|
264
|
+
We have to make sure that closing is not before opening. We can do it by using
|
265
|
+
`key_greater_than_key` validator:
|
266
|
+
```ruby
|
267
|
+
key_greater_than_key("to", "from")
|
268
|
+
```
|
269
|
+
and our validator will look like this:
|
270
|
+
```ruby
|
271
|
+
a_hash(
|
272
|
+
allowed_keys("from", "to"),
|
273
|
+
key("from", hour),
|
274
|
+
key("to", hour),
|
275
|
+
key_greater_than_key("to", "from"))
|
276
|
+
```
|
277
|
+
That looks good, but it's not complete yet. `a_hash` validator applies all
|
278
|
+
validators to the provided payload by using `run_all` combinator. This
|
279
|
+
behaviour is problematic if our `from` or `to` keys are missing or are not
|
280
|
+
valid integers. Payload
|
281
|
+
```ruby
|
282
|
+
{"from" => "abc", "to" => 17}
|
283
|
+
```
|
284
|
+
will cause an exception as `key_greater_than_key` can not compare string to
|
285
|
+
integer. Let's fix it by using `fail_fast` and `run_all` combinators:
|
286
|
+
```ruby
|
287
|
+
a_hash(
|
288
|
+
allowed_keys("from", "to"),
|
289
|
+
fail_fast(
|
290
|
+
run_all(
|
291
|
+
key("from", hour),
|
292
|
+
key("to", hour)),
|
293
|
+
key_greater_than_key("to", "from")))
|
294
|
+
```
|
295
|
+
This way if `from` and `to` are not both valid hours we will not be comparing
|
296
|
+
them.
|
297
|
+
|
298
|
+
You can see this validator reused in a bigger example
|
299
|
+
[below](#path-to-an-invalid-element).
|
300
|
+
|
301
|
+
## Path to an invalid element
|
302
|
+
|
303
|
+
Validation errors on deeply nested JSON structure will always contain exact
|
304
|
+
path to the invalid element.
|
305
|
+
|
306
|
+
### Example
|
307
|
+
|
308
|
+
Let's say we validate stores. Example of store object:
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
store = {
|
312
|
+
"store" => {
|
313
|
+
"name" => "Scrutton Street",
|
314
|
+
"description" => "large store",
|
315
|
+
"opening_hours" => {
|
316
|
+
"monday" => {"from" => 9, "to" => 17},
|
317
|
+
"tuesday" => {"from" => 9, "to" => 17},
|
318
|
+
"wednesday"=> {"from" => 9, "to" => 17},
|
319
|
+
"thursday" => {"from" => 9, "to" => 17},
|
320
|
+
"friday" => {"from" => 9, "to" => 17},
|
321
|
+
"saturday" => {"from" => 10, "to" => 16}
|
322
|
+
},
|
323
|
+
"employees"=> ["bob", "alice"]
|
324
|
+
}
|
325
|
+
}
|
326
|
+
```
|
327
|
+
|
328
|
+
Definition of the store validator (using `from_to` built in the [previous section](#composability)):
|
329
|
+
```ruby
|
330
|
+
hour = fail_fast(non_negative_integer, less_or_equal(24))
|
331
|
+
|
332
|
+
from_to = a_hash(
|
333
|
+
allowed_keys("from", "to"),
|
334
|
+
fail_fast(
|
335
|
+
run_all(
|
336
|
+
key("from", hour),
|
337
|
+
key("to", hour)),
|
338
|
+
key_greater_than_key("to", "from")))
|
339
|
+
|
340
|
+
store_validator = a_hash(
|
341
|
+
allowed_keys("store"),
|
342
|
+
key("store",
|
343
|
+
a_hash(
|
344
|
+
allowed_keys("name", "description", "opening_hours", "employees"),
|
345
|
+
key("name", non_empty_string),
|
346
|
+
optional_key("description"),
|
347
|
+
key("opening_hours",
|
348
|
+
a_hash(
|
349
|
+
allowed_keys("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"),
|
350
|
+
optional_key("monday", from_to),
|
351
|
+
optional_key("tuesday", from_to),
|
352
|
+
optional_key("wednesday", from_to),
|
353
|
+
optional_key("thursday", from_to),
|
354
|
+
optional_key("friday", from_to),
|
355
|
+
optional_key("saturday", from_to),
|
356
|
+
optional_key("sunday", from_to))),
|
357
|
+
key("employees", array(each(non_empty_string))))))
|
358
|
+
```
|
359
|
+
|
360
|
+
Let's say we try to validate store that has Wednesday opening hours invalid
|
361
|
+
(closing time before opening time) like this:
|
362
|
+
```ruby
|
363
|
+
...
|
364
|
+
"wednesday"=> {"from" => 9, "to" => 7},
|
365
|
+
...
|
366
|
+
```
|
367
|
+
Now we use store validator to fill in the collection of errors using default
|
368
|
+
error messages:
|
369
|
+
```ruby
|
370
|
+
errors = {}
|
371
|
+
result = default_errors(store_validator).call(store, errors)
|
372
|
+
```
|
373
|
+
Result is `false` and we get validation error in the `errors` hash:
|
374
|
+
```ruby
|
375
|
+
{"store/opening_hours/wednesday/to" => ["must be greater than from"]}
|
376
|
+
```
|
377
|
+
You can find this example in
|
378
|
+
[functional spec](https://github.com/shutl/composable_validations/blob/master/spec/functional/composable_validations_spec.rb)
|
379
|
+
ready for experiments.
|
380
|
+
|
381
|
+
## Overriding error messages
|
382
|
+
|
383
|
+
This gem comes with set of [default error
|
384
|
+
messages](https://github.com/shutl/composable_validations/blob/master/lib/composable_validations/default_error_messages.rb).
|
385
|
+
There are few ways to provide your own error messages.
|
386
|
+
|
387
|
+
### Local override
|
388
|
+
|
389
|
+
You can override error message when building your validator:
|
390
|
+
```ruby
|
391
|
+
a_hash(
|
392
|
+
key("from", integer("custom error message")),
|
393
|
+
key("to", integer("another custom error message")))
|
394
|
+
```
|
395
|
+
This approach is good if you need just few specialized error messages for
|
396
|
+
different parts of your payload.
|
397
|
+
|
398
|
+
### Global override
|
399
|
+
|
400
|
+
If you need to change some of the error messages across all your validators you
|
401
|
+
can provide map of error messages. Keys in the map are symbols matching names
|
402
|
+
of basic validators:
|
403
|
+
```ruby
|
404
|
+
error_overrides = {
|
405
|
+
string: "not a string",
|
406
|
+
integer: "not an integer"
|
407
|
+
}
|
408
|
+
|
409
|
+
errors = {}
|
410
|
+
errors_container = ComposableValidations::Errors.new(errors, error_overrides)
|
411
|
+
result = validator.call(valid_data, errors_container, nil)
|
412
|
+
```
|
413
|
+
Note that your error messages don't need to be strings. You could for example
|
414
|
+
use rendering function that returns combination of error code, error context
|
415
|
+
and human readable message:
|
416
|
+
```ruby
|
417
|
+
error_overrides = {
|
418
|
+
key_greater_than_key: lambda do |validated_object, path, key1, key2|
|
419
|
+
{
|
420
|
+
code: 123,
|
421
|
+
context: [key1, key2],
|
422
|
+
message: "#{key1}=#{object[key1]} is not less than or equal to #{key2}=#{object[key2]}"
|
423
|
+
}
|
424
|
+
end
|
425
|
+
}
|
426
|
+
|
427
|
+
errors = {}
|
428
|
+
errors_container = ComposableValidations::Errors.new(errors, error_overrides)
|
429
|
+
result = validator.call(valid_data, errors_container, nil)
|
430
|
+
```
|
431
|
+
And when applied to invalid payload your validator will return an error:
|
432
|
+
```ruby
|
433
|
+
{
|
434
|
+
"store/opening_hours/wednesday/to"=>
|
435
|
+
[
|
436
|
+
{
|
437
|
+
:code=>123,
|
438
|
+
:context=>["to", "from"],
|
439
|
+
:message=>"to=17 is not less than or equal to from=24"
|
440
|
+
}
|
441
|
+
]
|
442
|
+
}
|
443
|
+
```
|
444
|
+
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
|
+
|
446
|
+
### Override error container
|
447
|
+
|
448
|
+
You can override error container class and provide any error collecting
|
449
|
+
behaviour you need. The only method error container must provide is:
|
450
|
+
```ruby
|
451
|
+
def add(msg, path, object)
|
452
|
+
```
|
453
|
+
where
|
454
|
+
* `msg` is a symbol of an error or an array where first element is a symbol of
|
455
|
+
error and remaining elements are context needed to render the error message.
|
456
|
+
* `path` represents a path to the invalid element within the JSON object. It is
|
457
|
+
an array of strings (keys in hash map) and integers (indexes in array).
|
458
|
+
* `object` is a validated object.
|
459
|
+
|
460
|
+
Example of error container that just collects error paths:
|
461
|
+
```ruby
|
462
|
+
class CollectPaths
|
463
|
+
attr_reader :paths
|
464
|
+
|
465
|
+
def initialize
|
466
|
+
@paths = []
|
467
|
+
end
|
468
|
+
|
469
|
+
def add(msg, path, object)
|
470
|
+
@paths << path
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
validator = ...
|
475
|
+
errors_container = CollectPaths.new
|
476
|
+
result = validator.call(valid_data, errors_container, nil)
|
477
|
+
```
|
478
|
+
and example of the value of `errors_container.paths` after getting an error:
|
479
|
+
```ruby
|
480
|
+
[["store", "opening_hours", "wednesday", "to"]]
|
481
|
+
```
|
482
|
+
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
|
+
|
484
|
+
## Custom validators
|
485
|
+
|
486
|
+
You can create your own validators as functions returning lambdas with signature
|
487
|
+
```ruby
|
488
|
+
lambda { |validated_object, errors_hash, path| ... }
|
489
|
+
```
|
490
|
+
Use `error` helper function to add errors to the error container and
|
491
|
+
functions `validate`, `precheck` and `nil_or` to avoid boilerplate.
|
492
|
+
|
493
|
+
### Example
|
494
|
+
|
495
|
+
Let's say we have an ActiveRecord model Store and API allowing update of the
|
496
|
+
store name. We will be receiving payload:
|
497
|
+
```ruby
|
498
|
+
{ name: 'new store name' }
|
499
|
+
```
|
500
|
+
We can build validator ensuring uniqueness of the store name:
|
501
|
+
```ruby
|
502
|
+
a_hash(
|
503
|
+
allowed_keys('name'),
|
504
|
+
key('name',
|
505
|
+
non_empty_string,
|
506
|
+
unique_store_name))
|
507
|
+
```
|
508
|
+
where `unique_store_name` is defined as:
|
509
|
+
```ruby
|
510
|
+
def unique_store_name
|
511
|
+
lambda do |store_name, errors, path|
|
512
|
+
if !Store.exists?(name: store_name)
|
513
|
+
true
|
514
|
+
else
|
515
|
+
error(errors, "has already been taken", store_name, path)
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
```
|
520
|
+
Note that we could simplify this code by using `validate` helper method:
|
521
|
+
```ruby
|
522
|
+
def unique_store_name
|
523
|
+
validate("has already been taken") do |store_name|
|
524
|
+
!Store.exists?(name: store_name)
|
525
|
+
end
|
526
|
+
end
|
527
|
+
```
|
528
|
+
We could also generalize this function and end up with generic ActiveModel
|
529
|
+
attribute uniqueness validator ready to be reused:
|
530
|
+
```ruby
|
531
|
+
def unique(klass, attr_name)
|
532
|
+
validate("has already been taken") do |attr_value|
|
533
|
+
!klass.exists?(attr_name => attr_value)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
a_hash(
|
538
|
+
allowed_keys('name'),
|
539
|
+
key('name',
|
540
|
+
non_empty_string,
|
541
|
+
unique(Store, :name)))
|
542
|
+
```
|
543
|
+
|
544
|
+
## API
|
545
|
+
|
546
|
+
* `a_hash(*validators)` - ensures that the validated object is a hash and then
|
547
|
+
applies all `validators` in sequence using `run_all` combinator.
|
548
|
+
|
549
|
+
* `allowed_keys(*allowed_keys)` - ensures that validated hash has only keys
|
550
|
+
provided as arguments.
|
551
|
+
|
552
|
+
* `array(*validators)` - ensures that the validated object is an array and then
|
553
|
+
applies all `validators` in sequence using `run_all` combinator.
|
554
|
+
|
555
|
+
* `at_least_one_of(*keys)` - ensures that the validated hash has at least one
|
556
|
+
of the keys provided as arguments.
|
557
|
+
|
558
|
+
* `boolean` - ensures that validated object is `true` or `false`.
|
559
|
+
|
560
|
+
* `date_string(format = /\A\d\d\d\d-\d\d-\d\d\Z/, msg = [:date_string,
|
561
|
+
'YYYY-MM-DD'])` - ensures that validated object is a string in a given
|
562
|
+
format and is parsable by `Date#parse`.
|
563
|
+
|
564
|
+
* `default_errors(validator)` - helper function binding validator to the
|
565
|
+
default implementation of the error collection object. Returned function is
|
566
|
+
not a composable validator so it should only be applied to the top level
|
567
|
+
validator right before applying it to the object. Example:
|
568
|
+
```ruby
|
569
|
+
errors = {}
|
570
|
+
default_errors(validator).call(validated_object, errors)
|
571
|
+
```
|
572
|
+
|
573
|
+
* `each_in_slice(range, validator)` - applies `validator` to each slice of the array. Example:
|
574
|
+
```ruby
|
575
|
+
array(
|
576
|
+
each_in_slice(0..-2, normal_element_validator),
|
577
|
+
each_in_slice(-1..-1, special_last_element_validator))
|
578
|
+
```
|
579
|
+
|
580
|
+
* `each(validator)` - applies `validator` to each element of the array.
|
581
|
+
|
582
|
+
* `equal(val, msg = [:equal, val])` - ensures that validated object is equal
|
583
|
+
`val`.
|
584
|
+
|
585
|
+
* `error(errors, msg, object, *segments)` - adds error message `msg` to the
|
586
|
+
error collection `errors` under path `segments`. Use it in your custom
|
587
|
+
validators.
|
588
|
+
|
589
|
+
* `exact_size(n, msg = [:exact_size, n])` - ensures that validated object has
|
590
|
+
size of exactly `n`. Can be applied only to objects responding to the method
|
591
|
+
`#size`.
|
592
|
+
|
593
|
+
* `fail_fast(*validators)` - executes `validators` in sequence until one
|
594
|
+
of the validators returns `false` or all of them were executed.
|
595
|
+
|
596
|
+
* `float(msg = :float)` - ensures that validated object is a number (parsable
|
597
|
+
as `Float` or `Fixnum`).
|
598
|
+
|
599
|
+
* `format(regex, msg = :format)` - ensures that validated string conforms to
|
600
|
+
the regular expression provided.
|
601
|
+
|
602
|
+
* `greater_or_equal(val, msg = [:greater_or_equal, val])` - ensures that
|
603
|
+
validated object is greater or equal than `val`.
|
604
|
+
|
605
|
+
* `greater(val, msg = [:greater, val])` - ensures that validated object is
|
606
|
+
greater than `val`.
|
607
|
+
|
608
|
+
* `guarded_parsing(format, msg, &blk)` - ensures that validated object is a
|
609
|
+
string of a given format and that it can be parsed by provided block (block
|
610
|
+
does not raise `ArgumentError` or `TypeError`).
|
611
|
+
|
612
|
+
* `inclusion(options, msg = [:inclusion, options])` - ensures that validated
|
613
|
+
object is one of the provided `options`.
|
614
|
+
|
615
|
+
* `in_range(range, msg = [:in_range, range])` - ensures that validated object is
|
616
|
+
in given `range`.
|
617
|
+
|
618
|
+
* `integer(msg = :integer)` - ensures that validated object is an integer
|
619
|
+
(parsable as `Fixnum`).
|
620
|
+
|
621
|
+
* `just_array(msg = :just_array)` - ensures that validated object is of type
|
622
|
+
`Array`.
|
623
|
+
|
624
|
+
* `just_hash(msg = :just_hash)` - ensures that validated object is of type
|
625
|
+
`Hash`.
|
626
|
+
|
627
|
+
* `key_equal_to_key(key1, key2, msg = [:key_equal_to_key, key1, key2])` -
|
628
|
+
ensures that validated hash has equal values under keys `key1` and `key2`. If
|
629
|
+
any of the values are nil validator returns true.
|
630
|
+
|
631
|
+
* `key_greater_or_equal_to_key(key1, key2, msg = [:key_greater_or_equal_to_key,
|
632
|
+
key1, key2])` - ensures that validated hash has values under keys `key1` and
|
633
|
+
`key2` in relation `h[key1] >= h[key2]`. If any of the values are nil
|
634
|
+
validator returns true.
|
635
|
+
|
636
|
+
* `key_greater_than_key(key1, key2, msg = [:key_greater_than_key, key1, key2])`-
|
637
|
+
ensures that validated hash has values under keys `key1` and `key2` in
|
638
|
+
relation `h[key1] > h[key2]`. If any of the values are nil validator
|
639
|
+
returns true.
|
640
|
+
|
641
|
+
* `key(key, *validators)` - ensures presence of the key in the validated hash
|
642
|
+
and applies validators to the value under the `key` using `run_all`
|
643
|
+
combinator.
|
644
|
+
|
645
|
+
* `key_less_or_equal_to_key(key1, key2, msg = [:key_less_or_equal_to_key, key1, key2])`-
|
646
|
+
ensures that validated hash has values under keys `key1` and `key2` in
|
647
|
+
relation `h[key1] <= h[key2]`. If any of the values are nil validator
|
648
|
+
returns true.
|
649
|
+
|
650
|
+
* `key_less_than_key(key1, key2, msg = [:key_less_than_key, key1, key2])`-
|
651
|
+
ensures that validated hash has values under keys `key1` and `key2` in
|
652
|
+
relation `h[key1] < h[key2]`. If any of the values are nil validator
|
653
|
+
returns true.
|
654
|
+
|
655
|
+
* `less_or_equal(val, msg = [:less_or_equal, val])` - ensures that
|
656
|
+
validated object is less or equal than `val`.
|
657
|
+
|
658
|
+
* `less(val, msg = [:less, val])` - ensures that validated object is less than
|
659
|
+
`val`.
|
660
|
+
|
661
|
+
* `max_size(n, msg = [:max_size, n])` - ensures that validated object has size
|
662
|
+
not greater than `n`. Can be applied only to objects responding to the method
|
663
|
+
`#size`.
|
664
|
+
|
665
|
+
* `min_size(n, msg = [:min_size, n])` - ensures that validated object has size
|
666
|
+
not less than `n`. Can be applied only to objects responding to the method
|
667
|
+
`#size`.
|
668
|
+
|
669
|
+
* `nil_or(*validators)` - helper function returning validator that returns true
|
670
|
+
if validated object is `nil` or applies all `validators` using `run_all`
|
671
|
+
combinator if validated object is not `nil`.
|
672
|
+
|
673
|
+
* `non_empty(msg = :non_empty)` - ensures that validated object is not empty.
|
674
|
+
|
675
|
+
* `non_empty_string(msg = :non_empty_string)` - ensures that validated object
|
676
|
+
is a non-empty string.
|
677
|
+
|
678
|
+
* `non_negative_float` - ensures that validated object is a non-negative number.
|
679
|
+
|
680
|
+
* `non_negative_integer` - ensures that validated object is a non-negative
|
681
|
+
integer.
|
682
|
+
|
683
|
+
* `non_negative(msg = :non_negative)` - ensures that validated object is not
|
684
|
+
negative.
|
685
|
+
|
686
|
+
* `non_negative_stringy_float` - ensures that validated object is a
|
687
|
+
non-negative number or string that can be parsed into non-negative number.
|
688
|
+
Example: both 0.1 and "0.1" are valid.
|
689
|
+
|
690
|
+
* `non_negative_stringy_integer` - ensures that validated object is a
|
691
|
+
non-negative integer or string that can be parsed into non-negative integer.
|
692
|
+
Example: both 1 and "1" are valid.
|
693
|
+
|
694
|
+
* `optional_key(key, *validators)` - applies `validators` to the value under
|
695
|
+
the `key` using `run_all` combinator. Returns `true` if `key` does not exist
|
696
|
+
in the validated hash.
|
697
|
+
|
698
|
+
* `precheck(*validators, &blk)` - helper function returning validator that
|
699
|
+
returns `true` if `&blk` returns `true` or applies all `validators` using
|
700
|
+
`run_all` combinator if `&blk` returns `false`. Example - validate that value
|
701
|
+
is a number but also allow value "infinity":
|
702
|
+
```ruby
|
703
|
+
precheck(float) { |v| v == 'infinity' }
|
704
|
+
```
|
705
|
+
|
706
|
+
* `presence_of_key(key, msg = :presence_of_key)` - ensures that validated hash
|
707
|
+
has `key`.
|
708
|
+
|
709
|
+
* `run_all(*validators)` - executes all `validators` in sequence collecting all
|
710
|
+
error messages.
|
711
|
+
|
712
|
+
* `size_range(range, msg = [:size_range, range])` - ensures that validated
|
713
|
+
object has size `n` in range `range`. Can be applied only to objects
|
714
|
+
responding to the method `#size`.
|
715
|
+
|
716
|
+
* `string(msg = :string)` - ensures that validated object is of class `String`.
|
717
|
+
|
718
|
+
* `stringy_float(msg = :stringy_float)` - ensures that validated object is a
|
719
|
+
number or string that can be parsed into number. Example: both 0.1 and "0.1"
|
720
|
+
are valid.
|
721
|
+
|
722
|
+
* `stringy_integer(msg = :stringy_integer)` - ensures that validated object is
|
723
|
+
an integer or string that can be parsed into integer. Example: both 1 and
|
724
|
+
"1" are valid.
|
725
|
+
|
726
|
+
* `time_string(format = //, msg = :time_string)` - ensures that validated
|
727
|
+
object is a string in a given format and is parsable by `Time#parse`.
|
728
|
+
|
729
|
+
* `validate(msg, key = nil, &blk)` - helper method returning validator that
|
730
|
+
returns `true` if `&blk` returns `true` and `false` otherwise. `msg` is an
|
731
|
+
error message added to the error container when validation returns `false`.
|
732
|
+
Example - ensure that validated object is equal "hello":
|
733
|
+
```ruby
|
734
|
+
validate('must be "hello"') { |v| v == 'hello' }
|
735
|
+
```
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ComposableValidations; end
|
2
|
+
|
3
|
+
module ComposableValidations::Combinators
|
4
|
+
def run_all(*validators)
|
5
|
+
lambda do |object, errors, prefix|
|
6
|
+
validators.inject(true) do |acc, validator|
|
7
|
+
r = validator.call(object, errors, prefix)
|
8
|
+
acc && r
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def fail_fast(*validators)
|
14
|
+
lambda do |object, errors, prefix|
|
15
|
+
validators.each do |validator|
|
16
|
+
return false if !validator.call(object, errors, prefix)
|
17
|
+
end
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def nil_or(*validators)
|
23
|
+
precheck(*validators, &:nil?)
|
24
|
+
end
|
25
|
+
|
26
|
+
def precheck(*validators, &blk)
|
27
|
+
lambda do |object, errors, prefix|
|
28
|
+
return true if yield(object)
|
29
|
+
run_all(*validators).call(object, errors, prefix)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module ComposableValidations::Comparison
|
2
|
+
def key_to_key_comparison(key1, key2, msg, &compfun)
|
3
|
+
precheck(
|
4
|
+
validate(msg, key1) do |h|
|
5
|
+
v1 = h[key1]
|
6
|
+
v2 = h[key2]
|
7
|
+
compfun.call(v1, v2)
|
8
|
+
end
|
9
|
+
) do |h|
|
10
|
+
h[key1].nil? || h[key2].nil?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def comparison(val, msg, &compfun)
|
15
|
+
validate(msg) do |v|
|
16
|
+
compfun.call(v, val)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def key_greater_or_equal_to_key(key1, key2, msg = [:key_greater_or_equal_to_key, key1, key2])
|
21
|
+
key_to_key_comparison(key1, key2, msg, &:>=)
|
22
|
+
end
|
23
|
+
|
24
|
+
def key_greater_than_key(key1, key2, msg = [:key_greater_than_key, key1, key2])
|
25
|
+
key_to_key_comparison(key1, key2, msg, &:>)
|
26
|
+
end
|
27
|
+
|
28
|
+
def key_less_or_equal_to_key(key1, key2, msg = [:key_less_or_equal_to_key, key1, key2])
|
29
|
+
key_to_key_comparison(key1, key2, msg, &:<=)
|
30
|
+
end
|
31
|
+
|
32
|
+
def key_less_than_key(key1, key2, msg = [:key_less_than_key, key1, key2])
|
33
|
+
key_to_key_comparison(key1, key2, msg, &:<)
|
34
|
+
end
|
35
|
+
|
36
|
+
def key_equal_to_key(key1, key2, msg = [:key_equal_to_key, key1, key2])
|
37
|
+
key_to_key_comparison(key1, key2, msg, &:==)
|
38
|
+
end
|
39
|
+
|
40
|
+
def greater_or_equal(val, msg = [:greater_or_equal, val])
|
41
|
+
comparison(val, msg, &:>=)
|
42
|
+
end
|
43
|
+
|
44
|
+
def greater(val, msg = [:greater, val])
|
45
|
+
comparison(val, msg, &:>)
|
46
|
+
end
|
47
|
+
|
48
|
+
def less_or_equal(val, msg = [:less_or_equal, val])
|
49
|
+
comparison(val, msg, &:<=)
|
50
|
+
end
|
51
|
+
|
52
|
+
def less(val, msg = [:less, val])
|
53
|
+
comparison(val, msg, &:<)
|
54
|
+
end
|
55
|
+
|
56
|
+
def equal(val, msg = [:equal, val])
|
57
|
+
comparison(val, msg, &:==)
|
58
|
+
end
|
59
|
+
|
60
|
+
def in_range(range, msg = [:in_range, range])
|
61
|
+
validate(msg) do |val|
|
62
|
+
range.include?(val)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
ComposableValidations::Errors::DEFAULT_MESSAGE_MAP = {
|
2
|
+
non_empty_string: "can't be blank",
|
3
|
+
non_empty: "can't be empty",
|
4
|
+
allowed_keys: "is not allowed",
|
5
|
+
presence_of_key: "can't be blank",
|
6
|
+
just_hash: "must be a hash",
|
7
|
+
just_array: "must be an array",
|
8
|
+
string: "must be a string",
|
9
|
+
integer: "must be an integer",
|
10
|
+
stringy_integer: "must be an integer",
|
11
|
+
float: "must be a number",
|
12
|
+
stringy_float: "must be a number",
|
13
|
+
non_negative: "must be greater than or equal to 0",
|
14
|
+
format: "is invalid",
|
15
|
+
time_string: "must be a time",
|
16
|
+
date_string: lambda { |object, path, format| "must be a date in format #{format}" },
|
17
|
+
at_least_one_of: lambda { |object, path, keys| "at least one of #{keys.join(', ')} is required" },
|
18
|
+
key_greater_or_equal_to_key: lambda { |object, path, key1, key2| "must be greater than or equal to #{key2}" },
|
19
|
+
key_greater_than_key: lambda { |object, path, key1, key2| "must be greater than #{key2}" },
|
20
|
+
key_less_or_equal_to_key: lambda { |object, path, key1, key2| "must be less than or equal to #{key2}" },
|
21
|
+
key_less_than_key: lambda { |object, path, key1, key2| "must be less than #{key2}" },
|
22
|
+
key_equal_to_key: lambda { |object, path, key1, key2| "must be equal to #{key2}" },
|
23
|
+
greater_or_equal: lambda { |object, path, val| "must be greater than or equal to #{val}" },
|
24
|
+
greater: lambda { |object, path, val| "must be greater than #{val}" },
|
25
|
+
less_or_equal: lambda { |object, path, val| "must be less than or equal to #{val}" },
|
26
|
+
less: lambda { |object, path, val| "must be less than #{val}" },
|
27
|
+
equal: lambda { |object, path, val| "must be equal to #{val}" },
|
28
|
+
min_size: lambda { |object, path, n| "is too short (minium size is #{n})" },
|
29
|
+
max_size: lambda { |object, path, n| "is too long (maximum size is #{n})" },
|
30
|
+
exact_size: lambda { |object, path, n| "is the wrong size (should be #{n})" },
|
31
|
+
size_range: lambda { |object, path, range| "is the wrong size (minimum is #{range.min} and maximum is #{range.max})" },
|
32
|
+
in_range: lambda { |object, path, range| "must be in range #{range.min}..#{range.max}" },
|
33
|
+
inclusion: lambda do |object, path, options|
|
34
|
+
if options.length > 10
|
35
|
+
"is not allowed"
|
36
|
+
else
|
37
|
+
"must be one of: #{options.join(', ')}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class ComposableValidations::Errors
|
2
|
+
def initialize(errors, message_map = nil)
|
3
|
+
@errors = errors
|
4
|
+
@message_map = DEFAULT_MESSAGE_MAP.merge(message_map || {})
|
5
|
+
end
|
6
|
+
|
7
|
+
def add(msg, path, object)
|
8
|
+
merge(@errors, {join(path) => [render_message(msg, path, object)]})
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_hash
|
12
|
+
@errors
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def render_message(msg, path, object)
|
18
|
+
message = Array(msg)
|
19
|
+
message_builder = @message_map.fetch(message.first, msg)
|
20
|
+
if message_builder.is_a?(Proc)
|
21
|
+
message_builder.call(object, path, *msg[1..-1])
|
22
|
+
else
|
23
|
+
message_builder
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def merge(e1, e2)
|
28
|
+
e2.keys.each do |k, v|
|
29
|
+
e1[k] = ((e1[k] || []) + (e2[k] || [])).uniq
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def join(segments)
|
34
|
+
if segments.empty?
|
35
|
+
segments[0] = base_key
|
36
|
+
end
|
37
|
+
|
38
|
+
segments.compact.join(separator)
|
39
|
+
end
|
40
|
+
|
41
|
+
def base_key
|
42
|
+
'base'
|
43
|
+
end
|
44
|
+
|
45
|
+
def separator
|
46
|
+
'/'
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ComposableValidations::Utils
|
2
|
+
def default_errors(validator)
|
3
|
+
lambda do |object, errors_hash|
|
4
|
+
errors = ComposableValidations::Errors.new(errors_hash)
|
5
|
+
validator.call(object, errors, nil)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def join(*segments)
|
10
|
+
segments.inject([]) do |acc, seg|
|
11
|
+
acc + Array(seg)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate(msg, key = nil, &blk)
|
16
|
+
lambda do |o, errors, prefix|
|
17
|
+
if yield(o)
|
18
|
+
true
|
19
|
+
else
|
20
|
+
error(errors, msg, o, prefix, key)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def error(errors, msg, object, *segments)
|
26
|
+
errors.add(msg, join(*segments), object)
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
require_relative 'composable_validations/version'
|
5
|
+
require_relative 'composable_validations/combinators'
|
6
|
+
require_relative 'composable_validations/utils'
|
7
|
+
require_relative 'composable_validations/comparison'
|
8
|
+
require_relative 'composable_validations/errors'
|
9
|
+
require_relative 'composable_validations/default_error_messages'
|
10
|
+
|
11
|
+
module ComposableValidations
|
12
|
+
include ComposableValidations::Combinators
|
13
|
+
include ComposableValidations::Utils
|
14
|
+
include ComposableValidations::Comparison
|
15
|
+
|
16
|
+
# it is named with prefix 'a_' to avoid conflict with built in hash method
|
17
|
+
def a_hash(*validators)
|
18
|
+
fail_fast(just_hash, run_all(*validators))
|
19
|
+
end
|
20
|
+
|
21
|
+
def array(*validators)
|
22
|
+
fail_fast(
|
23
|
+
just_array,
|
24
|
+
run_all(*validators))
|
25
|
+
end
|
26
|
+
|
27
|
+
def each(validator)
|
28
|
+
each_in_slice(0..-1, validator)
|
29
|
+
end
|
30
|
+
|
31
|
+
def each_in_slice(range, validator)
|
32
|
+
lambda do |a, errors, prefix|
|
33
|
+
slice = a.slice(range) || []
|
34
|
+
slice.inject([true, 0]) do |(acc, index), elem|
|
35
|
+
suffix = to_index(a, range.begin) + index
|
36
|
+
b = validator.call(elem, errors, join(prefix, suffix))
|
37
|
+
[acc && b, index + 1]
|
38
|
+
end.first
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_index(array, range_index)
|
43
|
+
indexes = array.map.with_index { |_, i| i }
|
44
|
+
indexes[range_index]
|
45
|
+
end
|
46
|
+
|
47
|
+
def allowed_keys(*allowed_keys)
|
48
|
+
lambda do |h, errors, prefix|
|
49
|
+
h.keys.inject(true) do |acc, key|
|
50
|
+
if !allowed_keys.include?(key)
|
51
|
+
error(errors, :allowed_keys, h, prefix, key)
|
52
|
+
else
|
53
|
+
acc && true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def key(key, *validators)
|
60
|
+
fail_fast(
|
61
|
+
presence_of_key(key),
|
62
|
+
lambda do |h, errors, prefix|
|
63
|
+
run_all(*validators).call(h[key], errors, join(prefix, key))
|
64
|
+
end
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
def optional_key(key, *validators)
|
69
|
+
lambda do |h, errors, prefix|
|
70
|
+
if h.has_key?(key)
|
71
|
+
key(key, *validators).call(h, errors, prefix)
|
72
|
+
else
|
73
|
+
true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def just_hash(msg = :just_hash)
|
79
|
+
just_type(Hash, msg)
|
80
|
+
end
|
81
|
+
|
82
|
+
def just_array(msg = :just_array)
|
83
|
+
just_type(Array, msg)
|
84
|
+
end
|
85
|
+
|
86
|
+
def string(msg = :string)
|
87
|
+
just_type(String, msg)
|
88
|
+
end
|
89
|
+
|
90
|
+
def non_empty_string(msg = :non_empty_string)
|
91
|
+
fail_fast(
|
92
|
+
string,
|
93
|
+
validate(msg) { |s| s.strip != '' })
|
94
|
+
end
|
95
|
+
|
96
|
+
def integer(msg = :integer)
|
97
|
+
just_type(Fixnum, msg)
|
98
|
+
end
|
99
|
+
|
100
|
+
def stringy_integer(msg = :stringy_integer)
|
101
|
+
parsing(msg) { |v| Integer(v.to_s) }
|
102
|
+
end
|
103
|
+
|
104
|
+
def float(msg = :float)
|
105
|
+
just_types(msg, Float, Fixnum)
|
106
|
+
end
|
107
|
+
|
108
|
+
def stringy_float(msg = :stringy_float)
|
109
|
+
parsing(msg) { |v| Float(v.to_s) }
|
110
|
+
end
|
111
|
+
|
112
|
+
def non_negative_float
|
113
|
+
fail_fast(float, non_negative)
|
114
|
+
end
|
115
|
+
|
116
|
+
def non_negative_integer
|
117
|
+
fail_fast(integer, non_negative)
|
118
|
+
end
|
119
|
+
|
120
|
+
def non_negative_stringy_float
|
121
|
+
fail_fast(stringy_float, non_negative)
|
122
|
+
end
|
123
|
+
|
124
|
+
def non_negative_stringy_integer
|
125
|
+
fail_fast(stringy_integer, non_negative)
|
126
|
+
end
|
127
|
+
|
128
|
+
def non_negative(msg = :non_negative)
|
129
|
+
validate(msg) { |v| Float(v.to_s) >= 0 }
|
130
|
+
end
|
131
|
+
|
132
|
+
def date_string(format = /\A\d\d\d\d-\d\d-\d\d\Z/, msg = [:date_string, 'YYYY-MM-DD'])
|
133
|
+
guarded_parsing(format, msg) { |v| Date.parse(v) }
|
134
|
+
end
|
135
|
+
|
136
|
+
def time_string(format = //, msg = :time_string)
|
137
|
+
guarded_parsing(format, msg) { |v| Time.parse(v) }
|
138
|
+
end
|
139
|
+
|
140
|
+
def format(regex, msg = :format)
|
141
|
+
validate(msg) { |val| regex.match(val) }
|
142
|
+
end
|
143
|
+
|
144
|
+
def guarded_parsing(format, msg, &blk)
|
145
|
+
fail_fast(
|
146
|
+
string(msg),
|
147
|
+
format(format, msg),
|
148
|
+
parsing(msg, &blk))
|
149
|
+
end
|
150
|
+
|
151
|
+
def parsing(msg, &blk)
|
152
|
+
validate(msg) do |val|
|
153
|
+
begin
|
154
|
+
yield(val)
|
155
|
+
true
|
156
|
+
rescue ArgumentError, TypeError
|
157
|
+
false
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def just_type(type, msg)
|
163
|
+
just_types(msg, type)
|
164
|
+
end
|
165
|
+
|
166
|
+
def just_types(msg, *types)
|
167
|
+
validate(msg) do |v|
|
168
|
+
types.inject(false) do |acc, type|
|
169
|
+
acc || v.is_a?(type)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def boolean
|
175
|
+
inclusion([true, false])
|
176
|
+
end
|
177
|
+
|
178
|
+
def inclusion(options, msg = [:inclusion, options])
|
179
|
+
validate(msg) { |v| options.include?(v) }
|
180
|
+
end
|
181
|
+
|
182
|
+
def presence_of_key(key, msg = :presence_of_key)
|
183
|
+
validate(:presence_of_key, key) { |h| h.has_key?(key) }
|
184
|
+
end
|
185
|
+
|
186
|
+
def non_empty(msg = :non_empty)
|
187
|
+
validate(msg) { |v| !v.empty? }
|
188
|
+
end
|
189
|
+
|
190
|
+
def at_least_one_of(*keys)
|
191
|
+
validate([:at_least_one_of, keys]) do |h|
|
192
|
+
count = h.keys.count { |k| keys.include?(k) }
|
193
|
+
count > 0
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def size(validator)
|
198
|
+
lambda do |object, errors, path|
|
199
|
+
validator.call(object.size, errors, path)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def min_size(n, msg = [:min_size, n])
|
204
|
+
size(greater_or_equal(n, msg))
|
205
|
+
end
|
206
|
+
|
207
|
+
def max_size(n, msg = [:max_size, n])
|
208
|
+
size(less_or_equal(n, msg))
|
209
|
+
end
|
210
|
+
|
211
|
+
def exact_size(n, msg = [:exact_size, n])
|
212
|
+
size(equal(n, msg))
|
213
|
+
end
|
214
|
+
|
215
|
+
def size_range(range, msg = [:size_range, range])
|
216
|
+
size(in_range(range, msg))
|
217
|
+
end
|
218
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: composable_validations
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.9
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kajetan Bojko
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3'
|
27
|
+
description: Gem for validating complex JSON payloads in a functional way
|
28
|
+
email: kai@shutl.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- LICENSE
|
34
|
+
- README.md
|
35
|
+
- lib/composable_validations.rb
|
36
|
+
- lib/composable_validations/combinators.rb
|
37
|
+
- lib/composable_validations/comparison.rb
|
38
|
+
- lib/composable_validations/default_error_messages.rb
|
39
|
+
- lib/composable_validations/errors.rb
|
40
|
+
- lib/composable_validations/utils.rb
|
41
|
+
- lib/composable_validations/version.rb
|
42
|
+
homepage: https://github.com/shutl/composable_validations
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- .
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '2'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 2.4.6
|
64
|
+
signing_key:
|
65
|
+
specification_version: 4
|
66
|
+
summary: Gem for validating complex JSON payloads
|
67
|
+
test_files: []
|