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.
@@ -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
@@ -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.
@@ -0,0 +1,516 @@
1
+ # Fend [![Build Status](https://travis-ci.org/aradunovic/fend.svg?branch=master)](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).