fend 0.1.0
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.txt +21 -0
- data/README.md +516 -0
- data/fend.gemspec +21 -0
- data/lib/fend.rb +296 -0
- data/lib/fend/plugins/coercions.rb +442 -0
- data/lib/fend/plugins/collective_params.rb +60 -0
- data/lib/fend/plugins/data_processing.rb +212 -0
- data/lib/fend/plugins/dependencies.rb +130 -0
- data/lib/fend/plugins/external_validation.rb +98 -0
- data/lib/fend/plugins/full_messages.rb +67 -0
- data/lib/fend/plugins/validation_helpers.rb +246 -0
- data/lib/fend/plugins/validation_options.rb +116 -0
- data/lib/fend/plugins/value_helpers.rb +148 -0
- data/lib/fend/version.rb +13 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f9f96fc948bc1fc0e30a18c67ce1597c6ab6388d
|
4
|
+
data.tar.gz: 85cdf09079a2a087c1be08703a7f203d6920b325
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 595c8b6770e4ed1e28c63edb07b5745a130d0025810a926153e420729b107b2d9ac72ed927cba975416bcbd9b7e9168a0a374305381742428db1bb069799bf45
|
7
|
+
data.tar.gz: 010a595c487f5408a351d4679bf9b17d1d6722269a6f3b79a579ec53998313af8f7f9c2fdcd17cdaf3dbca912ae0e08b9a3f49bc57533d1272f8fcd4771af201
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Aleksandar Radunovic
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,516 @@
|
|
1
|
+
# Fend [](https://travis-ci.org/aradunovic/fend)
|
2
|
+
|
3
|
+
Fend is a small and extensible data validation toolkit.
|
4
|
+
|
5
|
+
## Contents
|
6
|
+
|
7
|
+
* [**Features**](#features)
|
8
|
+
* [**Documentation**](#documentation)
|
9
|
+
* [**Why?**](#why)
|
10
|
+
* [**Installation**](#installation)
|
11
|
+
* [**Introduction**](#introduction)
|
12
|
+
* [Core functionalities](#core-functionalities)
|
13
|
+
* [Nested params](#nested-params)
|
14
|
+
* [Arrays](#arrays)
|
15
|
+
* [**Plugins overview**](#plugins-overview)
|
16
|
+
* [Value helpers](#value-helpers)
|
17
|
+
* [Validation helpers](#validation-helpers)
|
18
|
+
* [Validation options](#validation-options)
|
19
|
+
* [Collective params](#collective-params)
|
20
|
+
* [Data processing](#data-processing)
|
21
|
+
* [Dependencies](#dependencies)
|
22
|
+
* [Coercions](#coercions)
|
23
|
+
* [External validation](#external-validation)
|
24
|
+
* [Full messages](#full-messages)
|
25
|
+
* [**Code of Conduct**](#code-of-conduct)
|
26
|
+
* [**License**](#license)
|
27
|
+
|
28
|
+
## Features
|
29
|
+
|
30
|
+
Some of the features include:
|
31
|
+
|
32
|
+
* Helpers for common validation cases
|
33
|
+
* Type coercion
|
34
|
+
* Dependency management
|
35
|
+
* Custom/external validation support
|
36
|
+
* Data processing
|
37
|
+
|
38
|
+
## Documentation
|
39
|
+
|
40
|
+
For detailed documentation visit [fend.radunovic.io](http://fend.radunovic.io)
|
41
|
+
|
42
|
+
## Why?
|
43
|
+
|
44
|
+
Let's be honest, data validation often tends to get messy and complex.
|
45
|
+
Most of the time you'll find yourself adding validation logic to domain models
|
46
|
+
and coming up with workarounds in order handle more complex cases.
|
47
|
+
|
48
|
+
What I wanted to make was a library that doesn't do too much. Even better, a
|
49
|
+
library that **does nothing**, but provide the tools for building custom
|
50
|
+
validation logic.
|
51
|
+
|
52
|
+
## Installation
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# Gemfile
|
56
|
+
gem "fend"
|
57
|
+
```
|
58
|
+
|
59
|
+
Or install system wide:
|
60
|
+
|
61
|
+
gem install fend
|
62
|
+
|
63
|
+
## Introduction
|
64
|
+
|
65
|
+
We'll start with a simple example that show Fend's core functionalities. The
|
66
|
+
implementation will later be improved through a series of refactoring steps,
|
67
|
+
which will get you familiar with Fend's plugins.
|
68
|
+
|
69
|
+
### Core functionalities
|
70
|
+
|
71
|
+
By default, Fend doesn't do much. As the example below shows, it provides
|
72
|
+
methods for specifying params, fetching their values and appending errors.
|
73
|
+
All checks need to be implemented manually.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
require "fend"
|
77
|
+
|
78
|
+
# Create a validation class which inherits from `Fend`
|
79
|
+
class UserValidation < Fend
|
80
|
+
# define validation block
|
81
|
+
validation do |i|
|
82
|
+
# specify :username param that needs to be validated
|
83
|
+
i.param(:username) do |username|
|
84
|
+
# append error if username value is not string
|
85
|
+
username.add_error("must be string") unless username.value.is_a?(String)
|
86
|
+
|
87
|
+
username.valid? #=> false
|
88
|
+
username.invalid? #=> true
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
* `i` - represents validation input. It's actually an instance of
|
95
|
+
`Param` class, same as `username`.
|
96
|
+
|
97
|
+
Let's run the validation:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# run the validation and store the result
|
101
|
+
result = UserValidation.call(username: 1234)
|
102
|
+
|
103
|
+
# check if result is a success
|
104
|
+
result.success? #=> false
|
105
|
+
|
106
|
+
# check if result is failure
|
107
|
+
result.failure? #=> true
|
108
|
+
|
109
|
+
# get validation input
|
110
|
+
result.input #=> { username: 1234 }
|
111
|
+
|
112
|
+
# get result output
|
113
|
+
result.input #=> { username: 1234 }
|
114
|
+
|
115
|
+
# get error messages
|
116
|
+
result.messages #=> { username: ["must be string"] }
|
117
|
+
```
|
118
|
+
|
119
|
+
`result` is an instance of `Result` class.
|
120
|
+
|
121
|
+
### Nested params
|
122
|
+
|
123
|
+
Nested params are defined in the same way as regular params:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
i.param(:address) do |address|
|
127
|
+
address.add_error("must be hash") unless address.value.is_a?(Hash)
|
128
|
+
|
129
|
+
address.param(:city) do |city|
|
130
|
+
city.add_error("must be string") unless city.value.is_a?(String)
|
131
|
+
end
|
132
|
+
|
133
|
+
address.param(:street) do |street|
|
134
|
+
street.add_error("must be string") unless street.value.is_a?(String)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
Let's execute the validation:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
result = UserValidation.call(address: :invalid)
|
143
|
+
result.failure? #=> true
|
144
|
+
result.messages #=> { address: ["must be hash"] }
|
145
|
+
```
|
146
|
+
|
147
|
+
As you can see, nested param validations are **not** executed when
|
148
|
+
parent param is invalid.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
result = UserValidation.call(username: "test", address: {})
|
152
|
+
result.messages #=> { address: { city: ["must be string"], street: ["must be string"] } }
|
153
|
+
```
|
154
|
+
|
155
|
+
### Arrays
|
156
|
+
|
157
|
+
Validating array members is done by passing a block to `Param#each` method:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
i.param(:tags) do |tags|
|
161
|
+
tags.each do |tag|
|
162
|
+
tag.add_error("must be string") unless tag.value.is_a?(String)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
Now, if we run the validation:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
result = UserValidation.call(tags: [1, 2])
|
171
|
+
result.messages #=> { tags: { 0 => ["must be string"], 1 => ["must be string"] } }
|
172
|
+
```
|
173
|
+
|
174
|
+
Needless to say, member validation won't be run if `tags` is not an array.
|
175
|
+
|
176
|
+
Fend makes it possible to validate specific array members, since `#each` method
|
177
|
+
also provides an `index`:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
tags.each do |tag, index|
|
181
|
+
if index == 0
|
182
|
+
tag.add_error("must be integer") unless tag.value.is_a?(Integer)
|
183
|
+
else
|
184
|
+
tag.add_error("must be string") unless tag.value.is_a?(String)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
```
|
188
|
+
|
189
|
+
## Plugins overview
|
190
|
+
|
191
|
+
For complete plugins documentation, go to [fend.radunovic.io](http://fend.radunovic.io).
|
192
|
+
|
193
|
+
### Value helpers
|
194
|
+
|
195
|
+
The `value_helpers` plugin provides additional `Param` methods that can be used to
|
196
|
+
check or fetch param values.
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
plugin :collective_params
|
200
|
+
|
201
|
+
plugin :value_helpers
|
202
|
+
|
203
|
+
validate do |i|
|
204
|
+
i.params(:username, :details, :tags) do |username, details, tags|
|
205
|
+
username.present? #=> false
|
206
|
+
username.blank? #=> true
|
207
|
+
username.empty_string? #=> true
|
208
|
+
|
209
|
+
details.type_of?(Hash) #=> true
|
210
|
+
details.dig(:address, :info, :coordinates, 0) #=> 35.6895
|
211
|
+
|
212
|
+
details.dig(:invalid_key, 0, :name) #=> nil
|
213
|
+
|
214
|
+
tags.dig(0, :id) #=> 1
|
215
|
+
tags.dig(1, :name) #=> "js"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
UserValidation.call(
|
220
|
+
username: "",
|
221
|
+
details: { address: { info: { coordinates: [35.6895, 139.6917] } } },
|
222
|
+
tags: [{ id: 1, name: "ruby"}, { id: 2, name: "js" }]
|
223
|
+
)
|
224
|
+
```
|
225
|
+
|
226
|
+
### Validation helpers
|
227
|
+
|
228
|
+
The `validation_helpers` plugin provides methods for some common validation cases:
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
plugin :validation_helpers
|
232
|
+
|
233
|
+
validation do |i|
|
234
|
+
i.param(:username) do |username|
|
235
|
+
username.validate_presence
|
236
|
+
username.validate_type(String)
|
237
|
+
end
|
238
|
+
|
239
|
+
i.param(:address) do |address|
|
240
|
+
address.validate_type(Hash)
|
241
|
+
|
242
|
+
address.param(:city) do |city|
|
243
|
+
city.validate_presence
|
244
|
+
city.validate_type(String)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
i.param(:tags) do |tags|
|
249
|
+
tags.validate_type(Array)
|
250
|
+
tags.validate_min_length(1)
|
251
|
+
|
252
|
+
tags.each do |tag|
|
253
|
+
tag.validate_type(String)
|
254
|
+
tag.validate_inclusion(%w(ruby js elixir), message: "#{tag.value} is not a valid tag")
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
```
|
259
|
+
|
260
|
+
### Validation options
|
261
|
+
|
262
|
+
Instead of calling validation helpers separately, `validation_options` plugin
|
263
|
+
can be used in order to specify all validations as options.
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
plugin :validation_options
|
267
|
+
|
268
|
+
validation do |i|
|
269
|
+
i.param(:username) { |username| username.validate(presence: true, type: String) }
|
270
|
+
|
271
|
+
i.param(:address) do |address|
|
272
|
+
address.validate_type(Hash)
|
273
|
+
|
274
|
+
address.param(:city) { |city| city.validate(presence: true, type: String) }
|
275
|
+
end
|
276
|
+
|
277
|
+
i.param(:tags) do |tags|
|
278
|
+
tags.validate(type: Array, min_length: 1)
|
279
|
+
|
280
|
+
tags.each do |tag|
|
281
|
+
tag.validate(type: String,
|
282
|
+
inclusion: { in: %w(ruby js elixir), message: "#{tag.value} is not a valid tag" })
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
```
|
287
|
+
|
288
|
+
### Collective params
|
289
|
+
|
290
|
+
Specifying params one by one can be tedious in some/most cases.
|
291
|
+
With `collective_params` plugin, you can specify multiple params at once, by
|
292
|
+
using `#params` method, instead of `#param`:
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
plugin :validation_options
|
296
|
+
plugin :collective_params
|
297
|
+
|
298
|
+
validation do |i|
|
299
|
+
i.params(:username, :address, :tags) do |username, address, tags|
|
300
|
+
username.validate(presence: true, type: String)
|
301
|
+
|
302
|
+
address.validate_type(Hash)
|
303
|
+
address.params(:city, :street) do |street, city|
|
304
|
+
city.validate(presence: true, type: String) }
|
305
|
+
street.validate(presence: true, type: String)
|
306
|
+
end
|
307
|
+
|
308
|
+
tags.validate(type: Array, min_length: 1)
|
309
|
+
tags.each do |tag|
|
310
|
+
tag.validate(type: String,
|
311
|
+
inclusion: { in: %w(ruby js elixir), message: "#{tag.value} is not a valid tag" })
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
```
|
316
|
+
|
317
|
+
### Data processing
|
318
|
+
|
319
|
+
With `data_processing` plugin you can process input/output data.
|
320
|
+
|
321
|
+
You can use some of the built-in processings, like `:symbolize`, for example:
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
class UserValidation < Fend
|
325
|
+
plugin :data_processing, input: [:symbolize]
|
326
|
+
|
327
|
+
# ...
|
328
|
+
end
|
329
|
+
|
330
|
+
UserValidation.call("username" => "john", email: "john@example.com", "admin" => true)
|
331
|
+
```
|
332
|
+
|
333
|
+
You can define custom processings:
|
334
|
+
|
335
|
+
```ruby
|
336
|
+
class UserValidation < Fend
|
337
|
+
plugin :data_processing
|
338
|
+
|
339
|
+
process(:input) do |input|
|
340
|
+
input.merge(foo: "foo")
|
341
|
+
end
|
342
|
+
|
343
|
+
process(:output) do |output|
|
344
|
+
output.merge(timestamp: Time.now.utc)
|
345
|
+
end
|
346
|
+
|
347
|
+
validate do |i|
|
348
|
+
i.param(:username) { |username| username.value #=> "john" }
|
349
|
+
i.param(:foo) { |foo| foo.value #=> "foo" }
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
result = UserValidation.call(username: "john")
|
354
|
+
|
355
|
+
result.input #=> { username: "john" }
|
356
|
+
result.output #=> { username: "john", timestamp: 2018-01-01 00:00:00 UTC }
|
357
|
+
```
|
358
|
+
|
359
|
+
### Dependencies
|
360
|
+
|
361
|
+
The `dependencies` plugin enables you to register and resolve dependencies.
|
362
|
+
|
363
|
+
There are two types of dependencies:
|
364
|
+
|
365
|
+
1. Inheritable - Available in subclasses also
|
366
|
+
2. Local - registered under a key in `deps` registry. Available only in the current
|
367
|
+
class
|
368
|
+
|
369
|
+
To resolve dependencies, pass `:inject` option with dependency list
|
370
|
+
to `.validate` method:
|
371
|
+
|
372
|
+
```ruby
|
373
|
+
plugin :collective_params
|
374
|
+
plugin :validation_options
|
375
|
+
|
376
|
+
plugin :dependencies, user_model: User
|
377
|
+
|
378
|
+
validate(inject: [:user_model, :context]) do |i, user_model, context|
|
379
|
+
|
380
|
+
i.params(:email, :password, :password_confirmation) do |email, password, password_confirmation|
|
381
|
+
|
382
|
+
email.add_error("not found") if email.present? && !user_model.exists?(email: email.value)
|
383
|
+
|
384
|
+
if context == :password_change
|
385
|
+
password.validate(type: String, min_length: 6)
|
386
|
+
if password.valid?
|
387
|
+
password.add_error("must be confirmed") unless password.value == password_confirmation.value
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def initialize(context)
|
394
|
+
deps[:context] = context
|
395
|
+
end
|
396
|
+
```
|
397
|
+
|
398
|
+
Here, `:user_model` is an inheritable dependency, while `:context` is local.
|
399
|
+
|
400
|
+
As you can see, we're expecting for `context` to be passed in the initializer:
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
result = UserValidation.new(:password_change).call(email: "foo@bar.com", password: :invalid)
|
404
|
+
|
405
|
+
result.messages #=> { email: ["not found"], password: ["must be string", "must be confirmed"] }
|
406
|
+
```
|
407
|
+
|
408
|
+
### Coercions
|
409
|
+
|
410
|
+
`coercions` plugin coerces input param values based on provided type schema.
|
411
|
+
By default, incoercible values are returned unmodified.
|
412
|
+
|
413
|
+
```ruby
|
414
|
+
plugin :collective_params
|
415
|
+
|
416
|
+
plugin :coercions
|
417
|
+
|
418
|
+
coerce username: :string,
|
419
|
+
address: { street: :string, city: :string },
|
420
|
+
tags: [:string]
|
421
|
+
|
422
|
+
validate do |i|
|
423
|
+
i.params(:username, :address, :tags) do |username, address, tags|
|
424
|
+
username.value #=> "foobar"
|
425
|
+
address.value #=> {}
|
426
|
+
tags.value #=> ["1", "foo", "cooking"]
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
result = UserValidation.call(username: :foobar, address: "", tags: [1, "foo", :cooking])
|
431
|
+
|
432
|
+
result.input #=> { username: :foobar, address: "", tags: [1, "foo", :cooking] }
|
433
|
+
result.output #=> { username: "foobar", address: {}, tags: ["1", "foo", "cooking"] }
|
434
|
+
```
|
435
|
+
|
436
|
+
### External validation
|
437
|
+
|
438
|
+
With `external_validation` plugin param validations can be delegated to a
|
439
|
+
class/object that responds to `call` and returns error messages.
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
class CustomEmailValidator
|
443
|
+
def initialize
|
444
|
+
@errors = []
|
445
|
+
end
|
446
|
+
|
447
|
+
def call(email_value)
|
448
|
+
@errors << "must be string" unless email_value.is_a?(String)
|
449
|
+
@errors << "must be unique" unless unique?(email_value)
|
450
|
+
|
451
|
+
@errors
|
452
|
+
end
|
453
|
+
|
454
|
+
def unique?(value)
|
455
|
+
UniquenessCheck.call(value)
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
class AddressValidation < Fend
|
460
|
+
plugin :validation_options
|
461
|
+
plugin :collective_params
|
462
|
+
|
463
|
+
validate do |i|
|
464
|
+
i.params(:city, :street) do |city, street|
|
465
|
+
city.validate(type: String)
|
466
|
+
street.validate(type: String)
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
class UserValidation < Fend
|
472
|
+
plugin :collective_params
|
473
|
+
plugin :external_validation
|
474
|
+
|
475
|
+
validate do |i|
|
476
|
+
i.params(:email, :address) do |email, address|
|
477
|
+
email.validate_with(CustomEmailValidation.new)
|
478
|
+
|
479
|
+
address.validate_with(AddressValidation)
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
```
|
484
|
+
|
485
|
+
### Full messages
|
486
|
+
|
487
|
+
`full_messages` plugin defines `Result#full_messages` method which returns
|
488
|
+
error messages with prepended param name.
|
489
|
+
|
490
|
+
```ruby
|
491
|
+
class UserValidation < Fend
|
492
|
+
plugin :full_messages, array_member_names: { tags: :tag }
|
493
|
+
|
494
|
+
# ...
|
495
|
+
end
|
496
|
+
|
497
|
+
result = UserValidation.call(username: nil, address: {}, tags: [1])
|
498
|
+
|
499
|
+
result.full_messages
|
500
|
+
# {
|
501
|
+
# username: ["username must be present"],
|
502
|
+
# address: { city: ["city must be string"] },
|
503
|
+
# tags: { 0 => ["tag must be string"] }
|
504
|
+
# }
|
505
|
+
```
|
506
|
+
|
507
|
+
## Code of Conduct
|
508
|
+
|
509
|
+
Everyone interacting in the Fend project’s codebases, issue trackers, chat rooms
|
510
|
+
and mailing lists is expected to follow the
|
511
|
+
[code of conduct](https://github.com/aradunovic/fend/blob/master/CODE_OF_CONDUCT.md).
|
512
|
+
|
513
|
+
## License
|
514
|
+
|
515
|
+
The gem is available as open source under the terms of the
|
516
|
+
[MIT License](https://opensource.org/licenses/MIT).
|