dry-validation 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.
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