fend 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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