composable_validations 0.0.9
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 +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: []
|