dry-validation 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +23 -0
  4. data/README.md +203 -26
  5. data/config/errors.yml +16 -0
  6. data/dry-validation.gemspec +1 -0
  7. data/examples/each.rb +19 -0
  8. data/examples/form.rb +15 -0
  9. data/examples/nested.rb +13 -11
  10. data/lib/dry/validation.rb +5 -0
  11. data/lib/dry/validation/error_compiler.rb +2 -18
  12. data/lib/dry/validation/input_type_compiler.rb +78 -0
  13. data/lib/dry/validation/messages.rb +1 -7
  14. data/lib/dry/validation/predicates.rb +33 -1
  15. data/lib/dry/validation/result.rb +8 -0
  16. data/lib/dry/validation/rule.rb +11 -90
  17. data/lib/dry/validation/rule/composite.rb +50 -0
  18. data/lib/dry/validation/rule/each.rb +13 -0
  19. data/lib/dry/validation/rule/key.rb +17 -0
  20. data/lib/dry/validation/rule/set.rb +22 -0
  21. data/lib/dry/validation/rule/value.rb +13 -0
  22. data/lib/dry/validation/rule_compiler.rb +5 -0
  23. data/lib/dry/validation/schema.rb +6 -2
  24. data/lib/dry/validation/schema/definition.rb +4 -0
  25. data/lib/dry/validation/schema/form.rb +19 -0
  26. data/lib/dry/validation/schema/key.rb +16 -3
  27. data/lib/dry/validation/schema/result.rb +29 -0
  28. data/lib/dry/validation/schema/rule.rb +14 -16
  29. data/lib/dry/validation/schema/value.rb +14 -2
  30. data/lib/dry/validation/version.rb +1 -1
  31. data/spec/integration/custom_error_messages_spec.rb +1 -1
  32. data/spec/integration/optional_keys_spec.rb +30 -0
  33. data/spec/integration/schema_form_spec.rb +99 -0
  34. data/spec/integration/{validation_spec.rb → schema_spec.rb} +40 -13
  35. data/spec/shared/predicates.rb +1 -1
  36. data/spec/unit/error_compiler_spec.rb +64 -0
  37. data/spec/unit/input_type_compiler_spec.rb +205 -0
  38. data/spec/unit/predicates/bool_spec.rb +34 -0
  39. data/spec/unit/predicates/date_spec.rb +31 -0
  40. data/spec/unit/predicates/date_time_spec.rb +31 -0
  41. data/spec/unit/predicates/decimal_spec.rb +32 -0
  42. data/spec/unit/predicates/float_spec.rb +31 -0
  43. data/spec/unit/predicates/{nil_spec.rb → none_spec.rb} +2 -2
  44. data/spec/unit/predicates/time_spec.rb +31 -0
  45. data/spec/unit/rule/conjunction_spec.rb +28 -0
  46. data/spec/unit/rule/disjunction_spec.rb +36 -0
  47. data/spec/unit/rule/implication_spec.rb +14 -0
  48. data/spec/unit/rule/value_spec.rb +1 -1
  49. metadata +60 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6acd14c8b93a29eed518ef8934921cf35d0faf17
4
- data.tar.gz: 83c9f549fb92f5ff4cf3f5c95ec8029e81c045d2
3
+ metadata.gz: ee26d9a3fbca2ae3faf18827ad28f02bd323239b
4
+ data.tar.gz: a007eccefbc3456f8a481549512896074dcf3f86
5
5
  SHA512:
6
- metadata.gz: b7411edfe3d00dbc645d9c48a7df9dd952bf36c01e59f1a55e39114e0a757afd6b4e66b57a07380b658a146b2757651f4a1b47d61f29565d464d2b7896cdb04f
7
- data.tar.gz: 50201382e0ed26bcebcf19f1d9dafe2c7c3ee260c9654afdb3b6002ce7cbf4bc83ad549e6ffff9f6636ca1fb9aa6eaadea1371044a08e33425443768eef2e512
6
+ metadata.gz: fe7c8c42376862ea8fbe14d3563ad65f7e3bc3f8d9b8bbbb9bff2fc239b7e16bfb9a11fd6cba5f2d0957650cd365470beaacb158a5a078998a589a15d92ca571
7
+ data.tar.gz: 8d337dd7bbf3fef8ca79f41ea962bc692ac6dd25b6fd93fca7c55559f0f3723f37e1bdd5fea804d3fcccbda1358f0360c8576268ecf766eed589208ac4771a31
@@ -19,6 +19,7 @@ matrix:
19
19
  allow_failures:
20
20
  - rvm: ruby-head
21
21
  - rvm: jruby-head
22
+ - rvm: jruby-9000
22
23
  notifications:
23
24
  email: false
24
25
  webhooks:
@@ -1,3 +1,26 @@
1
+ # v0.2.0 to-be-released
2
+
3
+ ### Added
4
+
5
+ * `Schema::Form` with a built-in coercer inferred from type-check predicates (solnic)
6
+ * Ability to pass a block to predicate check in the DSL ie `value.hash? { ... }` (solnic)
7
+ * Optional keys using `option(:key_name) { ... }` interface in the DSL (solnic)
8
+ * New predicates:
9
+ - `bool?`
10
+ - `date?`
11
+ - `date_time?`
12
+ - `time?`
13
+ - `float?`
14
+ - `decimal?`
15
+ - `hash?`
16
+ - `array?`
17
+
18
+ ### Fixed
19
+
20
+ * Added missing `and` / `or` interfaces to composite rules (solnic)
21
+
22
+ [Compare v0.1.0...HEAD](https://github.com/dryrb/dry-validation/compare/v0.1.0...HEAD)
23
+
1
24
  # v0.1.0 2015-11-25
2
25
 
3
26
  First public release
data/README.md CHANGED
@@ -1,10 +1,18 @@
1
- # dry-validation <a href="https://gitter.im/dryrb/chat" target="_blank">![Join the chat at https://gitter.im/dryrb/chat](https://badges.gitter.im/Join%20Chat.svg)</a>
2
-
3
- <a href="https://rubygems.org/gems/dry-validation" target="_blank">![Gem Version](https://badge.fury.io/rb/dry-validation.svg)</a>
4
- <a href="https://travis-ci.org/dryrb/dry-validation" target="_blank">![Build Status](https://travis-ci.org/dryrb/dry-validation.svg?branch=master)</a>
5
- <a href="https://gemnasium.com/dryrb/dry-validation" target="_blank">![Dependency Status](https://gemnasium.com/dryrb/dry-validation.svg)</a>
6
- <a href="https://codeclimate.com/github/dryrb/dry-validation" target="_blank">![Code Climate](https://codeclimate.com/github/dryrb/dry-validation/badges/gpa.svg)</a>
7
- <a href="http://inch-ci.org/github/dryrb/dry-validation" target="_blank">![Documentation Status](http://inch-ci.org/github/dryrb/dry-validation.svg?branch=master&style=flat)</a>
1
+ [gem]: https://rubygems.org/gems/dry-validation
2
+ [travis]: https://travis-ci.org/dryrb/dry-validation
3
+ [gemnasium]: https://gemnasium.com/dryrb/dry-validation
4
+ [codeclimate]: https://codeclimate.com/github/dryrb/dry-validation
5
+ [coveralls]: https://coveralls.io/r/dryrb/dry-validation
6
+ [inchpages]: http://inch-ci.org/github/dryrb/dry-validation
7
+
8
+ # dry-validation [![Join the chat at https://gitter.im/dryrb/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dryrb/chat)
9
+
10
+ [![Gem Version](https://badge.fury.io/rb/dry-validation.svg)][gem]
11
+ [![Build Status](https://travis-ci.org/dryrb/dry-validation.svg?branch=master)][travis]
12
+ [![Dependency Status](https://gemnasium.com/dryrb/dry-validation.svg)][gemnasium]
13
+ [![Code Climate](https://codeclimate.com/github/dryrb/dry-validation/badges/gpa.svg)][codeclimate]
14
+ [![Test Coverage](https://codeclimate.com/github/dryrb/dry-validation/badges/coverage.svg)][codeclimate]
15
+ [![Inline docs](http://inch-ci.org/github/dryrb/dry-validation.svg?branch=master)][inchpages]
8
16
 
9
17
  Data validation library based on predicate logic and rule composition.
10
18
 
@@ -87,6 +95,84 @@ A couple of remarks:
87
95
  * Schema object does not carry the input as its state, nor does it know how to access the input values, we
88
96
  pass the input to `call` and get error set as the response
89
97
 
98
+ ### Optional Keys
99
+
100
+ You can define which keys are optional and define rules for their values:
101
+
102
+ ``` ruby
103
+ require 'dry-validation'
104
+
105
+ class Schema < Dry::Validation::Schema
106
+ key(:email) { |email| email.filled? }
107
+
108
+ optional(:age) do |age|
109
+ age.int? & age.gt?(18)
110
+ end
111
+ end
112
+
113
+ schema = Schema.new
114
+
115
+ errors = schema.messages(email: 'jane@doe.org')
116
+
117
+ puts errors.inspect
118
+ # []
119
+
120
+ errors = schema.messages(email: 'jane@doe.org', age: 17)
121
+
122
+ puts errors.inspect
123
+ # [[:age, ["age must be greater than 18 (17 was given)"]]]
124
+ ```
125
+
126
+ ### Optional Values
127
+
128
+ When it is valid for a given value to be `nil` you can use `none?` predicate:
129
+
130
+ ``` ruby
131
+ require 'dry-validation'
132
+
133
+ class Schema < Dry::Validation::Schema
134
+ key(:email) { |email| email.filled? }
135
+
136
+ key(:age) do |age|
137
+ age.none? | (age.int? & age.gt?(18))
138
+ end
139
+ end
140
+
141
+ schema = Schema.new
142
+
143
+ errors = schema.messages(email: 'jane@doe.org', age: nil)
144
+
145
+ puts errors.inspect
146
+ # []
147
+
148
+ errors = schema.messages(email: 'jane@doe.org', age: 19)
149
+
150
+ puts errors.inspect
151
+ # []
152
+
153
+ errors = schema.messages(email: 'jane@doe.org', age: 17)
154
+
155
+ puts errors.inspect
156
+ # [[:age, ["age must be greater than 18 (17 was given)"]]]
157
+ ```
158
+
159
+ ### Optional Key vs Value
160
+
161
+ We make a clear distinction between specifying an optional `key` and an optional
162
+ `value`. This gives you a way of being very specific about validation rules. You
163
+ can define a schema which can give you precise errors when a key was missing or
164
+ key was present but the value was nil.
165
+
166
+ This also comes with the benefit of being explicit about the type expectation.
167
+ In the example above we explicitly state that `:age` *can be nil* or it *can be an integer*
168
+ and when it *is an integer* we specify that it *must be greater than 18*.
169
+
170
+ Another benefit is that we can infer specific coercion rules when types are specified.
171
+ In example [`Schema::Form`](https://github.com/dryrb/dry-validation#form-validation-with-coercions)
172
+ will use `form.nil` type from dry-data to coerce empty strings into `nil` for you
173
+ whenever you specify `value.none? | value.int?`. Furthermore it will try to coerce
174
+ to `int` since that is our type expectation.
175
+
90
176
  ### Nested Hash
91
177
 
92
178
  We are free to define validations for anything, including deeply nested structures:
@@ -96,17 +182,19 @@ require 'dry-validation'
96
182
 
97
183
  class Schema < Dry::Validation::Schema
98
184
  key(:address) do |address|
99
- address.key(:city) do |city|
100
- city.min_size?(3)
101
- end
102
-
103
- address.key(:street) do |street|
104
- street.filled?
105
- end
106
-
107
- address.key(:country) do |country|
108
- country.key(:name, &:filled?)
109
- country.key(:code, &:filled?)
185
+ address.hash? do
186
+ address.key(:city) do |city|
187
+ city.min_size?(3)
188
+ end
189
+
190
+ address.key(:street) do |street|
191
+ street.filled?
192
+ end
193
+
194
+ address.key(:country) do |country|
195
+ country.key(:name, &:filled?)
196
+ country.key(:code, &:filled?)
197
+ end
110
198
  end
111
199
  end
112
200
  end
@@ -124,6 +212,74 @@ puts errors.inspect
124
212
  # [[:address, [[:street, ["street is missing"]], [:country, ["country is missing"]]]]]
125
213
  ```
126
214
 
215
+ ### Array Elements
216
+
217
+ You can use `each` rule for validating each element in an array:
218
+
219
+ ``` ruby
220
+ class Schema < Dry::Validation::Schema
221
+ key(:phone_numbers) do |phone_numbers|
222
+ phone_numbers.array? do
223
+ phone_numbers.each(&:str?)
224
+ end
225
+ end
226
+ end
227
+
228
+ schema = Schema.new
229
+
230
+ errors = schema.messages(phone_numbers: '')
231
+
232
+ puts errors.inspect
233
+ # [[:phone_numbers, ["phone_numbers must be an array"]]]
234
+
235
+ errors = schema.messages(phone_numbers: ['123456789', 123456789])
236
+
237
+ puts errors.inspect
238
+ # [[:phone_numbers, [[:phone_numbers, ["phone_numbers must be a string"]]]]]
239
+ ```
240
+
241
+ ### Form Validation With Coercions
242
+
243
+ Probably the most common use case is to validate form params. This is a special
244
+ kind of a validation for a couple of reasons:
245
+
246
+ * The input is a hash with stringified keys
247
+ * The input include values that are strings, hashes or arrays
248
+ * Prior validation, we need to coerce values and symbolize keys based on the
249
+ information from rules
250
+
251
+ For that reason, `dry-validation` ships with `Schema::Form` class:
252
+
253
+ ``` ruby
254
+ require 'dry-validation'
255
+ require 'dry/validation/schema/form'
256
+
257
+ class UserFormSchema < Dry::Validation::Schema::Form
258
+ key(:email) { |value| value.str? & value.filled? }
259
+
260
+ key(:age) { |value| value.int? & value.gt?(18) }
261
+ end
262
+
263
+ schema = UserFormSchema.new
264
+
265
+ errors = schema.messages('email' => '', 'age' => '18')
266
+
267
+ puts errors.inspect
268
+
269
+ # [[:email, ["email must be filled"]], [:age, ["age must be greater than 18 (18 was given)"]]]
270
+ ```
271
+
272
+ There are few major differences between how it works here and in `ActiveModel`:
273
+
274
+ * We have type checking as predicates, ie `gt?(18)` will not be applied if the value
275
+ is not an integer
276
+ * Thus, error messages are provided *only for the rules that failed*
277
+ * There's a planned feature for generating "validation hints" which lists information
278
+ about all possible rules
279
+ * Coercion is handled by `dry-data` coercible hash using its `form.*` types that
280
+ are dedicated for this type of coercions
281
+ * It's very easy to add your own types and coercions (more info/docs coming soon)
282
+
127
283
  ### Defining Custom Predicates
128
284
 
129
285
  You can simply define predicate methods on your schema object:
@@ -158,25 +314,46 @@ class Schema < Dry::Validation::Schema
158
314
  end
159
315
  ```
160
316
 
317
+ You need to provide error messages for your custom predicates if you want them
318
+ to work with `Schem#messages` interface.
319
+
320
+ You can learn how to do that in the [Error Messages](https://github.com/dryrb/dry-validation#error-messages) section.
321
+
161
322
  ## List of Built-In Predicates
162
323
 
163
- * `empty?`
324
+ ### Basic
325
+
326
+ * `none?`
164
327
  * `eql?`
165
- * `exclusion?`
328
+ * `key?`
329
+
330
+ ### Types
331
+
332
+ * `str?`
333
+ * `int?`
334
+ * `float?`
335
+ * `decimal?`
336
+ * `bool?`
337
+ * `date?`
338
+ * `date_time?`
339
+ * `time?`
340
+ * `array?`
341
+ * `hash?`
342
+
343
+ ### Number, String, Collection
344
+
345
+ * `empty?`
166
346
  * `filled?`
167
- * `format?`
168
347
  * `gt?`
169
348
  * `gteq?`
170
- * `inclusion?`
171
- * `int?`
172
- * `key?`
173
349
  * `lt?`
174
350
  * `lteq?`
175
351
  * `max_size?`
176
352
  * `min_size?`
177
- * `nil?`
178
353
  * `size?`
179
- * `str?`
354
+ * `format?`
355
+ * `inclusion?`
356
+ * `exclusion?`
180
357
 
181
358
  ## Error Messages
182
359
 
@@ -1,3 +1,5 @@
1
+ array?: "%{name} must be an array"
2
+
1
3
  empty?: "%{name} cannot be empty"
2
4
 
3
5
  exclusion?: "%{name} must not be one of: %{list}"
@@ -12,10 +14,24 @@ gt?: "%{name} must be greater than %{num} (%{value} was given)"
12
14
 
13
15
  gteq?: "%{name} must be greater than or equal to %{num}"
14
16
 
17
+ hash?: "%{name} must be a hash"
18
+
15
19
  inclusion?: "%{name} must be one of: %{list}"
16
20
 
21
+ bool?: "%{name} must be boolean"
22
+
17
23
  int?: "%{name} must be an integer"
18
24
 
25
+ float?: "%{name} must be a float"
26
+
27
+ decimal?: "%{name} must be a decimal"
28
+
29
+ date?: "%{name} must be a date"
30
+
31
+ date_time?: "%{name} must be a date time"
32
+
33
+ time?: "%{name} must be a time"
34
+
19
35
  key?: "%{name} is missing"
20
36
 
21
37
  lt?: "%{name} must be less than %{num} (%{value} was given)"
@@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.add_runtime_dependency 'dry-configurable', '~> 0.1'
19
19
  spec.add_runtime_dependency 'dry-container', '~> 0.2', '>= 0.2.6'
20
20
  spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
21
+ spec.add_runtime_dependency 'dry-data', '~> 0.2', '>= 0.2.1'
21
22
 
22
23
  spec.add_development_dependency 'bundler'
23
24
  spec.add_development_dependency 'rake'
@@ -0,0 +1,19 @@
1
+ require 'dry-validation'
2
+
3
+ class Schema < Dry::Validation::Schema
4
+ key(:phone_numbers) do |phone_numbers|
5
+ phone_numbers.array? do
6
+ phone_numbers.each(&:str?)
7
+ end
8
+ end
9
+ end
10
+
11
+ schema = Schema.new
12
+
13
+ errors = schema.messages(phone_numbers: '')
14
+
15
+ puts errors.inspect
16
+
17
+ errors = schema.messages(phone_numbers: ['123456789', 123456789])
18
+
19
+ puts errors.inspect
@@ -0,0 +1,15 @@
1
+ require 'dry-validation'
2
+ require 'dry/validation/schema/form'
3
+
4
+ class UserFormSchema < Dry::Validation::Schema::Form
5
+ key(:email) { |value| value.str? & value.filled? }
6
+
7
+ key(:age) { |value| value.int? & value.gt?(18) }
8
+ end
9
+
10
+ schema = UserFormSchema.new
11
+
12
+ errors = schema.messages('email' => '', 'age' => '18')
13
+
14
+ puts errors.inspect
15
+ # [[:email, ["email must be filled"]], [:age, ["age must be greater than 18 (18 was given)"]]]
@@ -2,17 +2,19 @@ require 'dry-validation'
2
2
 
3
3
  class Schema < Dry::Validation::Schema
4
4
  key(:address) do |address|
5
- address.key(:city) do |city|
6
- city.min_size?(3)
7
- end
8
-
9
- address.key(:street) do |street|
10
- street.filled?
11
- end
12
-
13
- address.key(:country) do |country|
14
- country.key(:name, &:filled?)
15
- country.key(:code, &:filled?)
5
+ address.hash? do
6
+ address.key(:city) do |city|
7
+ city.min_size?(3)
8
+ end
9
+
10
+ address.key(:street) do |street|
11
+ street.filled?
12
+ end
13
+
14
+ address.key(:country) do |country|
15
+ country.key(:name, &:filled?)
16
+ country.key(:code, &:filled?)
17
+ end
16
18
  end
17
19
  end
18
20
  end
@@ -6,6 +6,11 @@ require 'dry-container'
6
6
  # a common task in Ruby
7
7
  module Dry
8
8
  module Validation
9
+ def self.symbolize_keys(hash)
10
+ hash.each_with_object({}) do |(k, v), r|
11
+ r[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v
12
+ end
13
+ end
9
14
  end
10
15
  end
11
16
 
@@ -42,10 +42,6 @@ module Dry
42
42
  { name: args[0][0] }
43
43
  end
44
44
 
45
- def visit_empty?(*args, value)
46
- { value: value }
47
- end
48
-
49
45
  def visit_exclusion?(*args, value)
50
46
  { list: args[0][0].join(', ') }
51
47
  end
@@ -96,20 +92,8 @@ module Dry
96
92
  end
97
93
  end
98
94
 
99
- def visit_str?(*args, value)
100
- { value: value }
101
- end
102
-
103
- def visit_format?(*args, value)
104
- {}
105
- end
106
-
107
- def visit_nil?(*args, value)
108
- {}
109
- end
110
-
111
- def visit_filled?(*args)
112
- {}
95
+ def method_missing(meth, *args)
96
+ { value: args[1] }
113
97
  end
114
98
  end
115
99
  end