fend 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f9f96fc948bc1fc0e30a18c67ce1597c6ab6388d
4
- data.tar.gz: 85cdf09079a2a087c1be08703a7f203d6920b325
3
+ metadata.gz: 5d13258d558a3a951a5b468994dd43d71e5146cb
4
+ data.tar.gz: 961c725b800fee07ed70e6f937fab1b8e97c42f3
5
5
  SHA512:
6
- metadata.gz: 595c8b6770e4ed1e28c63edb07b5745a130d0025810a926153e420729b107b2d9ac72ed927cba975416bcbd9b7e9168a0a374305381742428db1bb069799bf45
7
- data.tar.gz: 010a595c487f5408a351d4679bf9b17d1d6722269a6f3b79a579ec53998313af8f7f9c2fdcd17cdaf3dbca912ae0e08b9a3f49bc57533d1272f8fcd4771af201
6
+ metadata.gz: 78062b89d15fb139ef304be7764a1e0f3bba18f3f8f53d58a7ae70456c8740e2cbf284e36e5e2b3696cf4693db73a4219ec12bddf043b826b4333ff9ef3b037c
7
+ data.tar.gz: 4849a34a6212b5495a4cad9aa3d6635564164f820669136791cdc36b5f0c1f4fd194feaabe41a6849a171c66ac5c45afc75338cf496f6c9552de5d9af539bbaa
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Fend [![Build Status](https://travis-ci.org/aradunovic/fend.svg?branch=master)](https://travis-ci.org/aradunovic/fend)
1
+ # Fend [![Gem Version](https://badge.fury.io/rb/fend.svg)](https://badge.fury.io/rb/fend) [![Build Status](https://travis-ci.org/aradunovic/fend.svg?branch=master)](https://travis-ci.org/aradunovic/fend)
2
2
 
3
3
  Fend is a small and extensible data validation toolkit.
4
4
 
@@ -16,12 +16,13 @@ Fend is a small and extensible data validation toolkit.
16
16
  * [Value helpers](#value-helpers)
17
17
  * [Validation helpers](#validation-helpers)
18
18
  * [Validation options](#validation-options)
19
- * [Collective params](#collective-params)
19
+ * [Contexts](#contexts)
20
20
  * [Data processing](#data-processing)
21
21
  * [Dependencies](#dependencies)
22
22
  * [Coercions](#coercions)
23
23
  * [External validation](#external-validation)
24
24
  * [Full messages](#full-messages)
25
+ * [Object validation](#object-validation)
25
26
  * [**Code of Conduct**](#code-of-conduct)
26
27
  * [**License**](#license)
27
28
 
@@ -34,6 +35,8 @@ Some of the features include:
34
35
  * Dependency management
35
36
  * Custom/external validation support
36
37
  * Data processing
38
+ * Contextual validations
39
+ * Object validation
37
40
 
38
41
  ## Documentation
39
42
 
@@ -80,14 +83,31 @@ class UserValidation < Fend
80
83
  # define validation block
81
84
  validation do |i|
82
85
  # specify :username param that needs to be validated
83
- i.param(:username) do |username|
86
+ i.params(:username, :email) do |username, email|
84
87
  # append error if username value is not string
85
88
  username.add_error("must be string") unless username.value.is_a?(String)
86
89
 
90
+ email.add_error("is not a valid email address") unless email.match?(EMAIL_REGEX)
91
+ email.add_error("must be unique") if email.valid? && !unique?(email: email.value)
92
+
87
93
  username.valid? #=> false
88
- username.invalid? #=> true
94
+ email.invalid? #=> true
89
95
  end
90
96
  end
97
+
98
+ # you have full access to the constructor
99
+ def initialize(user_model)
100
+ @user_model = user_model
101
+ end
102
+
103
+ # custom methods are available in validation block
104
+ def unique?(args)
105
+ user_model.exists?(args)
106
+ end
107
+
108
+ def user_model
109
+ @user_model
110
+ end
91
111
  end
92
112
  ```
93
113
 
@@ -98,7 +118,7 @@ Let's run the validation:
98
118
 
99
119
  ```ruby
100
120
  # run the validation and store the result
101
- result = UserValidation.call(username: 1234)
121
+ result = UserValidation.new(User).call(username: 1234, email: "invalid@email")
102
122
 
103
123
  # check if result is a success
104
124
  result.success? #=> false
@@ -107,13 +127,13 @@ result.success? #=> false
107
127
  result.failure? #=> true
108
128
 
109
129
  # get validation input
110
- result.input #=> { username: 1234 }
130
+ result.input #=> { username: 1234, email: "invalid@email" }
111
131
 
112
132
  # get result output
113
- result.input #=> { username: 1234 }
133
+ result.input #=> { username: 1234, email: "invalid@email" }
114
134
 
115
135
  # get error messages
116
- result.messages #=> { username: ["must be string"] }
136
+ result.messages #=> { username: ["must be string"], email: ["is not a valid email address"] }
117
137
  ```
118
138
 
119
139
  `result` is an instance of `Result` class.
@@ -123,14 +143,11 @@ result.messages #=> { username: ["must be string"] }
123
143
  Nested params are defined in the same way as regular params:
124
144
 
125
145
  ```ruby
126
- i.param(:address) do |address|
146
+ i.params(:address) do |address|
127
147
  address.add_error("must be hash") unless address.value.is_a?(Hash)
128
148
 
129
- address.param(:city) do |city|
149
+ address.params(:city, :street) do |city, :street|
130
150
  city.add_error("must be string") unless city.value.is_a?(String)
131
- end
132
-
133
- address.param(:street) do |street|
134
151
  street.add_error("must be string") unless street.value.is_a?(String)
135
152
  end
136
153
  end
@@ -196,8 +213,6 @@ The `value_helpers` plugin provides additional `Param` methods that can be used
196
213
  check or fetch param values.
197
214
 
198
215
  ```ruby
199
- plugin :collective_params
200
-
201
216
  plugin :value_helpers
202
217
 
203
218
  validate do |i|
@@ -231,21 +246,17 @@ The `validation_helpers` plugin provides methods for some common validation case
231
246
  plugin :validation_helpers
232
247
 
233
248
  validation do |i|
234
- i.param(:username) do |username|
249
+ i.params(:username, :address, :tags) do |username, address, tags|
235
250
  username.validate_presence
236
251
  username.validate_type(String)
237
- end
238
252
 
239
- i.param(:address) do |address|
240
253
  address.validate_type(Hash)
241
254
 
242
255
  address.param(:city) do |city|
243
256
  city.validate_presence
244
257
  city.validate_type(String)
245
258
  end
246
- end
247
259
 
248
- i.param(:tags) do |tags|
249
260
  tags.validate_type(Array)
250
261
  tags.validate_min_length(1)
251
262
 
@@ -266,17 +277,13 @@ can be used in order to specify all validations as options.
266
277
  plugin :validation_options
267
278
 
268
279
  validation do |i|
269
- i.param(:username) { |username| username.validate(presence: true, type: String) }
280
+ i.params(:username, :address, :tags) do |username, address, tags|
281
+ username.validate(presence: true, type: String)
270
282
 
271
- i.param(:address) do |address|
272
283
  address.validate_type(Hash)
284
+ address.params(:city) { |city| city.validate(presence: true, type: String) }
273
285
 
274
- address.param(:city) { |city| city.validate(presence: true, type: String) }
275
- end
276
-
277
- i.param(:tags) do |tags|
278
286
  tags.validate(type: Array, min_length: 1)
279
-
280
287
  tags.each do |tag|
281
288
  tag.validate(type: String,
282
289
  inclusion: { in: %w(ruby js elixir), message: "#{tag.value} is not a valid tag" })
@@ -285,35 +292,45 @@ validation do |i|
285
292
  end
286
293
  ```
287
294
 
288
- ### Collective params
295
+ `:allow_nil` and `:allow_blank` options are also supported:
296
+
297
+ ```ruby
298
+ username.validate(type: String, allow_nil: true)
299
+ ```
300
+
301
+ ### Contexts
289
302
 
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`:
303
+ `contexts` plugin adds support for contextual validation, which basically
304
+ means you can branch validation logic depending on provided context.
293
305
 
294
306
  ```ruby
295
- plugin :validation_options
296
- plugin :collective_params
307
+ class UserValidation < Fend
308
+ plugin :contexts
297
309
 
298
- validation do |i|
299
- i.params(:username, :address, :tags) do |username, address, tags|
300
- username.validate(presence: true, type: String)
310
+ validate do |i|
311
+ i.params(:account_type) do |acc_type|
312
+ context(:admin) do
313
+ acc_type.validate_equality("admin")
314
+ end
301
315
 
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
316
+ context(:editor) do
317
+ acc_type.validate_equality("editor")
318
+ end
307
319
 
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" })
320
+ # you can check context against multiple values
321
+ context(:visitor, :demo) do
322
+ acc_type.validate_equality(nil)
323
+ end
312
324
  end
313
325
  end
314
326
  end
327
+
328
+ user_validation = UserValidation.new(context: :editor)
329
+ user_validation.call(account_type: "invalid").messages #=> { account_type: ["must be equal to 'editor'"] }
315
330
  ```
316
331
 
332
+ If no context is provided, context will be set to `:default`.
333
+
317
334
  ### Data processing
318
335
 
319
336
  With `data_processing` plugin you can process input/output data.
@@ -358,51 +375,29 @@ result.output #=> { username: "john", timestamp: 2018-01-01 00:00:00 UTC }
358
375
 
359
376
  ### Dependencies
360
377
 
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
378
+ The `dependencies` plugin enables you to register and resolve global dependencies.
368
379
 
369
380
  To resolve dependencies, pass `:inject` option with dependency list
370
381
  to `.validate` method:
371
382
 
372
383
  ```ruby
373
- plugin :collective_params
374
384
  plugin :validation_options
375
-
376
385
  plugin :dependencies, user_model: User
377
386
 
378
- validate(inject: [:user_model, :context]) do |i, user_model, context|
379
-
387
+ validate(inject: [:user_model]) do |i, user_model|
380
388
  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
+ if email.present? && !user_model.exists?(email: email.value)
390
+ email.add_error("not found")
389
391
  end
390
392
  end
391
393
  end
392
-
393
- def initialize(context)
394
- deps[:context] = context
395
- end
396
394
  ```
397
395
 
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:
396
+ Here, `:user_model` is an inheritable dependency, which means it will be
397
+ available in subclasses also. Global dependencies can be defined on `Fend` directly:
401
398
 
402
399
  ```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"] }
400
+ Fend.plugin :dependencies, user_model: User
406
401
  ```
407
402
 
408
403
  ### Coercions
@@ -411,8 +406,6 @@ result.messages #=> { email: ["not found"], password: ["must be string", "must b
411
406
  By default, incoercible values are returned unmodified.
412
407
 
413
408
  ```ruby
414
- plugin :collective_params
415
-
416
409
  plugin :coercions
417
410
 
418
411
  coerce username: :string,
@@ -458,7 +451,6 @@ end
458
451
 
459
452
  class AddressValidation < Fend
460
453
  plugin :validation_options
461
- plugin :collective_params
462
454
 
463
455
  validate do |i|
464
456
  i.params(:city, :street) do |city, street|
@@ -469,7 +461,6 @@ class AddressValidation < Fend
469
461
  end
470
462
 
471
463
  class UserValidation < Fend
472
- plugin :collective_params
473
464
  plugin :external_validation
474
465
 
475
466
  validate do |i|
@@ -504,6 +495,43 @@ result.full_messages
504
495
  # }
505
496
  ```
506
497
 
498
+ ### Object validation
499
+
500
+ `object_validation` plugin adds support for validating object attributes
501
+ and methods.
502
+
503
+ ```ruby
504
+ class UserModelValidation < Fend
505
+ plugin :object_validation
506
+ plugin :validation_options
507
+
508
+ validate do |user|
509
+ # use #attrs when validating object attributes/methods
510
+ user.attrs(:username, :email, :address) do |username, email, address|
511
+ username.validate(presence: true, max_length: 20, type: String)
512
+ email.validate(presence: true, format: EMAIL_REGEX, type: String)
513
+
514
+ # keep using #params if attribute value is expected to be hash
515
+ address.params(:city, :street) do |city, street|
516
+ city.validate(presence: true)
517
+ street.validate(presence: true)
518
+ end
519
+ end
520
+ end
521
+ end
522
+
523
+ user = User.new(username: "", email: "invalid@email", address: {})
524
+ validation = UserModelValidation.call(user)
525
+
526
+ validation.success? #=> false
527
+ validation.messages
528
+ #=> {
529
+ # username: ["must be present"],
530
+ # email: ["is in invalid format"],
531
+ # address: { city: ["must be present"], street: ["must be present"] }
532
+ # }
533
+ ```
534
+
507
535
  ## Code of Conduct
508
536
 
509
537
  Everyone interacting in the Fend project’s codebases, issue trackers, chat rooms
data/fend.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |gem|
11
11
  gem.homepage = "https://fend.radunovic.io"
12
12
  gem.license = "MIT"
13
13
 
14
- gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "fend.gemspec"]
14
+ gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "fend.gemspec", "doc/*.md"]
15
15
  gem.require_path = "lib"
16
16
 
17
17
  gem.required_ruby_version = ">= 2.0"
data/lib/fend.rb CHANGED
@@ -85,7 +85,7 @@ class Fend
85
85
  # Store validation block for later execution:
86
86
  #
87
87
  # validate do |i|
88
- # i.param(:foo) do |foo|
88
+ # i.params(:foo) do |foo|
89
89
  # # foo validation logic
90
90
  # end
91
91
  # end
@@ -96,6 +96,11 @@ class Fend
96
96
  def call(input)
97
97
  new.call(input)
98
98
  end
99
+
100
+ # Prints a deprecation warning to standard error.
101
+ def deprecation(message)
102
+ warn "FEND DEPRECATION WARNING: #{message}"
103
+ end
99
104
  end
100
105
 
101
106
  module InstanceMethods
@@ -116,7 +121,7 @@ class Fend
116
121
  @_raw_data = raw_data
117
122
  @_input_data = process_input(raw_data) || raw_data
118
123
  @_output_data = process_output(@_input_data) || @_input_data
119
- @_input_param = param_class.new(@_input_data)
124
+ @_input_param = param_class.new(:input, @_input_data)
120
125
  end
121
126
 
122
127
  # Returns validation block set on class level
@@ -140,9 +145,9 @@ class Fend
140
145
  # Process output data
141
146
  def process_output(output); end
142
147
 
143
- # Start validation
148
+ # Execute validation block
144
149
  def validate(&block)
145
- yield(@_input_param) if block_given?
150
+ instance_exec(@_input_param, &block) if block_given?
146
151
  end
147
152
 
148
153
  # Instantiate and return result
@@ -160,37 +165,61 @@ class Fend
160
165
  # Get param value
161
166
  attr_reader :value
162
167
 
168
+ # Get param name
169
+ attr_reader :name
170
+
163
171
  # Get param validation errors
164
172
  attr_reader :errors
165
173
 
166
- def initialize(value)
174
+ def initialize(name, value)
175
+ @name = name
167
176
  @value = value
168
177
  @errors = []
169
178
  end
170
179
 
171
180
  # Fetch nested value
172
181
  def [](name)
182
+ fetch(name)
183
+ end
184
+
185
+ def fetch(name)
173
186
  @value.fetch(name, nil) if @value.respond_to?(:fetch)
174
187
  end
175
188
 
176
189
  # Define child param and execute validation block
177
190
  def param(name, &block)
191
+ Fend.deprecation("Calling Param#param to specify params is deprecated and will not be supported in Fend 0.3.0. Use Param#params method instead.")
192
+
178
193
  return if flat? && invalid?
179
194
 
180
195
  value = self[name]
181
- param = _build_param(value)
196
+ param = _build_param(name, value)
182
197
 
183
198
  yield(param)
184
199
 
185
200
  _nest_errors(name, param.errors) if param.invalid?
186
201
  end
187
202
 
203
+ # Define child params and execute validation block
204
+ def params(*names, &block)
205
+ return if flat? && invalid?
206
+
207
+ params = names.each_with_object({}) do |name, result|
208
+ param = _build_param(name, self[name])
209
+ result[name] = param
210
+ end
211
+
212
+ yield(*params.values)
213
+
214
+ params.each { |name, param| _nest_errors(name, param.errors) if param.invalid? }
215
+ end
216
+
188
217
  # Define array member param and execute validation block
189
218
  def each(&block)
190
- return if (flat? && invalid?) || !@value.is_a?(Array)
219
+ return if (flat? && invalid?) || !@value.is_a?(Enumerable)
191
220
 
192
221
  @value.each_with_index do |value, index|
193
- param = _build_param(value)
222
+ param = _build_param(index, value)
194
223
 
195
224
  yield(param, index)
196
225
 
@@ -218,7 +247,7 @@ class Fend
218
247
  end
219
248
 
220
249
  def to_s
221
- "#{fend_class.inspect}::Param"
250
+ "#{fend_class.inspect}::Param #{super}"
222
251
  end
223
252
 
224
253
  # Return Fend class under which Param class is namespaced
@@ -2,7 +2,7 @@
2
2
 
3
3
  class Fend
4
4
  module Plugins
5
- # `collective_params` plugin allows you to specify multiple params at once,
5
+ # *[DEPRECATED] `collective_params` plugin allows you to specify multiple params at once,
6
6
  # instead of defining each one separately.
7
7
  #
8
8
  # Example:
@@ -40,11 +40,13 @@ class Fend
40
40
  # end
41
41
  module CollectiveParams
42
42
  module ParamMethods
43
+ warn("collective_params plugin is deprecated and will be removed in Fend 0.3.0. #params method is now provided out of the box.")
44
+
43
45
  def params(*names, &block)
44
46
  return if flat? && invalid?
45
47
 
46
48
  params = names.each_with_object({}) do |name, result|
47
- param = _build_param(self[name])
49
+ param = _build_param(name, self[name])
48
50
  result[name] = param
49
51
  end
50
52
 
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fend
4
+ module Plugins
5
+ # `contexts` plugin adds support for contextual validation, which basically
6
+ # means you can branch validation logic depending on provided context.
7
+ #
8
+ # class UserValidation < Fend
9
+ # plugin :contexts
10
+ #
11
+ # validate do |i|
12
+ # i.params(:account_type) do |acc_type|
13
+ # context(:admin) do
14
+ # acc_type.validate_equality("admin")
15
+ # end
16
+ #
17
+ # context(:editor) do
18
+ # acc_type.validate_equality("editor")
19
+ # end
20
+ #
21
+ # # you can check context against multiple values
22
+ # context(:visitor, :demo) do
23
+ # acc_type.validate_equality(nil)
24
+ # end
25
+ # end
26
+ # end
27
+ # end
28
+ #
29
+ # You can pass the context on initialization
30
+ #
31
+ # user_validation = UserValidation.new(context: :editor)
32
+ # user_validation.call(account_type: "invalid").messages
33
+ # #=> { account_type: ["must be equal to 'editor'"] }
34
+ #
35
+ # `#context` can be called anywhere in the validation block. You can also
36
+ # specify contextual params
37
+ #
38
+ # validate do |i|
39
+ # context(:admin) do
40
+ # i.params(:admin_specific_param) do |asp|
41
+ # # ...
42
+ # end
43
+ # end
44
+ #
45
+ # context(:editor) do
46
+ # i.params(:editor_specific_param) do |esp|
47
+ # # ...
48
+ # end
49
+ # end
50
+ # end
51
+ #
52
+ # ## Default context
53
+ #
54
+ # If no context is provided, context will be set to `:default`
55
+ #
56
+ # context(:default) do
57
+ # # default validation logic
58
+ # end
59
+ #
60
+ # ## Overriding constructor
61
+ #
62
+ # Since context value is set in the constructor, you should always call
63
+ # `super` when/if overriding it.
64
+ module Contexts
65
+ module InstanceMethods
66
+ def initialize(*args)
67
+ opts = if (_opts = args.last) && _opts.is_a?(Hash)
68
+ _opts
69
+ else
70
+ {}
71
+ end
72
+
73
+ @_context = opts.fetch(:context, :default)
74
+ end
75
+
76
+ def context(*values, &block)
77
+ values = Array(values)
78
+
79
+ yield if values.include?(@_context)
80
+ end
81
+ end
82
+ end
83
+
84
+ register_plugin(:contexts, Contexts)
85
+ end
86
+ end
@@ -13,7 +13,7 @@ class Fend
13
13
  #
14
14
  # 1. **Inheritable dependencies** - available in current validation class
15
15
  # and in subclasses
16
- # 2. **Local dependencies** - available only in current validation class
16
+ # 2. *[DEPRECATED] **Local dependencies** - available only in current validation class
17
17
  #
18
18
  # ### Inheritable dependencies
19
19
  #
@@ -30,7 +30,7 @@ class Fend
30
30
  #
31
31
  # Now, all `Fend` subclasses will be able to resolve `address_checker`
32
32
  #
33
- # ### Local dependencies
33
+ # ### Local dependencies *[DEPRECATED]
34
34
  #
35
35
  # Local dependencies can be registered in `deps` registry, on instance level.
36
36
  # Recommended place to do this is the initializer.
@@ -59,13 +59,17 @@ class Fend
59
59
  # class UserValidation < Fend
60
60
  # plugin :dependencies, user_model: User
61
61
  #
62
- # validate(inject: [:user_model, :address_checker]) do |i, user_model, address_checker|
62
+ # validate(inject: [:user_model]) do |i, user_model|
63
63
  # user_model #=> User
64
64
  # address_checker #=> #<AddressChecker ...>
65
65
  # end
66
66
  #
67
67
  # def initialize(address_checker)
68
- # deps[:address_checker] = address_checker
68
+ # @address_checker = address_checker
69
+ # end
70
+ #
71
+ # def address_checker
72
+ # @address_checker
69
73
  # end
70
74
  # end
71
75
  #
@@ -87,7 +91,7 @@ class Fend
87
91
  # Here's an example of email uniqueness validation:
88
92
  #
89
93
  # validate(inject: [:user_model]) do |i, user_model|
90
- # i.param(:email) do |email|
94
+ # i.params(:email) do |email|
91
95
  # email.add_error("must be unique") if user_model.exists?(email: email.value)
92
96
  # end
93
97
  # end
@@ -112,6 +116,8 @@ class Fend
112
116
 
113
117
  module InstanceMethods
114
118
  def deps
119
+ Fend.deprecation("Local dependencies are deprecated and will not be supported in Fend 0.3.0. Instead, you can set attributes or define custom methods which will be available in validation block.")
120
+
115
121
  @_deps ||= self.class.opts[:dependencies].dup
116
122
  end
117
123
 
@@ -30,7 +30,6 @@ class Fend
30
30
  #
31
31
  # class AddressValidation < Fend
32
32
  # plugin :validation_options
33
- # plugin :collective_params
34
33
  #
35
34
  # validate do |i|
36
35
  # i.params(:city, :street) do |city, street|
@@ -42,7 +41,6 @@ class Fend
42
41
  #
43
42
  # class UserValidation < Fend
44
43
  # plugin :external_validation
45
- # plugin :collective_params
46
44
  #
47
45
  # validate do |i|
48
46
  # i.params(:email, :address) do |email, address|
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fend
4
+ module Plugins
5
+ # `object_validation` plugin adds support for validating object attributes
6
+ # and methods.
7
+ #
8
+ # class UserModelValidation < Fend
9
+ # plugin :object_validation
10
+ # plugin :validation_options
11
+ #
12
+ # validate do |user|
13
+ # user.attrs(:username, :email) do |username, email|
14
+ # username.validate(presence: true, max_length: 20, type: String)
15
+ # email.validate(presence: true, format: EMAIL_REGEX, type: String)
16
+ # end
17
+ # end
18
+ # end
19
+ #
20
+ # user = User.new(username: "", email: "invalid@email")
21
+ # validation = UserModelValidation.call(user)
22
+ #
23
+ # validation.success? #=> false
24
+ # validation.messages #=> { username: ["must be present"], email: ["is in invalid format"] }
25
+ #
26
+ # As the example shows, the only change is that instread of the `#params` you
27
+ # should use `#attrs` method.
28
+ #
29
+ # ## Handling hash values
30
+ #
31
+ # If attribute value should be a hash, you can still use the `#params`
32
+ # method:
33
+ #
34
+ # # user.address #=> { city: "My city", street: "My street" }
35
+ # user.attrs(:address) do |address|
36
+ # address.params(:city, :street) do |city, street|
37
+ # # ...
38
+ # end
39
+ # end
40
+ module ObjectValidation
41
+ module ParamMethods
42
+ def attrs(*names, &block)
43
+ return if flat? && invalid?
44
+
45
+ attrs = names.each_with_object({}) do |name, result|
46
+ attr = _build_param(name, @value.public_send(name))
47
+ result[name] = attr
48
+ end
49
+
50
+ yield(*attrs.values)
51
+
52
+ attrs.each { |name, attr| _nest_errors(name, attr.errors) if attr.invalid? }
53
+ end
54
+ end
55
+ end
56
+
57
+ register_plugin(:object_validation, ObjectValidation)
58
+ end
59
+ end
@@ -8,7 +8,7 @@ class Fend
8
8
  # plugin :validation_helpers
9
9
  #
10
10
  # validate do |i|
11
- # i.param(:username) do |username|
11
+ # i.params(:username) do |username|
12
12
  # username.validate_presence
13
13
  # username.validate_max_length(20)
14
14
  # username.validate_type(String)
@@ -9,7 +9,7 @@ class Fend
9
9
  # plugin :validation_options
10
10
  #
11
11
  # validate do |i|
12
- # i.param(:email) do |email|
12
+ # i.params(:email) do |email|
13
13
  # email.validate(presence: true, type: String, format: EMAIL_REGEX)
14
14
  # end
15
15
  # end
@@ -34,6 +34,20 @@ class Fend
34
34
  #
35
35
  # email.validate type: { value: String }, format: { value: EMAIL_REGEX }
36
36
  #
37
+ # ## Allowing nil and blank values
38
+ #
39
+ # You can skip validation if param value is `nil` or blank by passing
40
+ # `:allow_nil` or `:allow_blank` options:
41
+ #
42
+ # # will skip type validation if name.value.nil?
43
+ # name.validate(type: String, allow_nil: true)
44
+ #
45
+ # # will skip type validation if email.blank?
46
+ # email.validate(type: String, allow_blank: true)
47
+ #
48
+ # To see what values are considered as blank,
49
+ # check ValueHelpers::ParamMethods#blank?.
50
+ #
37
51
  # `validation_options` supports ExternalValidation plugin:
38
52
  #
39
53
  # plugin :external_validation
@@ -41,6 +55,7 @@ class Fend
41
55
  # # ...
42
56
  #
43
57
  # email.validate(with: CustomEmailValidator)
58
+
44
59
  module ValidationOptions
45
60
  NO_ARG_METHODS = [:absence, :presence, :acceptance].freeze
46
61
  ARRAY_ARG_METHODS = [:exclusion, :inclusion, :length_range].freeze
@@ -75,6 +90,11 @@ class Fend
75
90
  def validate(opts = {})
76
91
  return if opts.empty?
77
92
 
93
+ allow_nil = opts.delete(:allow_nil)
94
+ allow_blank = opts.delete(:allow_blank)
95
+
96
+ return if (allow_nil == true && value.nil?) || (allow_blank == true && blank?)
97
+
78
98
  opts.each do |validator_name, args|
79
99
  method_name = "validate_#{validator_name}"
80
100
 
@@ -89,9 +109,6 @@ class Fend
89
109
  validation_method_args = [args]
90
110
  end
91
111
  elsif args.is_a?(Hash)
92
- next if args[:allow_nil] == true && value.nil?
93
- next if args[:allow_blank] == true && blank?
94
-
95
112
  mandatory_arg_key = MANDATORY_ARG_KEYS[validator_name]
96
113
 
97
114
  unless args.key?(mandatory_arg_key) || args.key?(DEFAULT_ARG_KEY)
data/lib/fend/version.rb CHANGED
@@ -5,7 +5,7 @@ class Fend
5
5
 
6
6
  module VERSION
7
7
  MAJOR = 0
8
- MINOR = 1
8
+ MINOR = 2
9
9
  PATCH = 0
10
10
 
11
11
  STRING = [MAJOR, MINOR, PATCH].compact.join(".")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fend
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksandar Radunovic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-18 00:00:00.000000000 Z
11
+ date: 2018-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -51,10 +51,12 @@ files:
51
51
  - lib/fend.rb
52
52
  - lib/fend/plugins/coercions.rb
53
53
  - lib/fend/plugins/collective_params.rb
54
+ - lib/fend/plugins/contexts.rb
54
55
  - lib/fend/plugins/data_processing.rb
55
56
  - lib/fend/plugins/dependencies.rb
56
57
  - lib/fend/plugins/external_validation.rb
57
58
  - lib/fend/plugins/full_messages.rb
59
+ - lib/fend/plugins/object_validation.rb
58
60
  - lib/fend/plugins/validation_helpers.rb
59
61
  - lib/fend/plugins/validation_options.rb
60
62
  - lib/fend/plugins/value_helpers.rb