dry-validation 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.md +35 -499
  4. data/lib/dry/validation/error_compiler.rb +9 -4
  5. data/lib/dry/validation/hint_compiler.rb +69 -0
  6. data/lib/dry/validation/predicate.rb +0 -4
  7. data/lib/dry/validation/result.rb +8 -0
  8. data/lib/dry/validation/rule.rb +44 -0
  9. data/lib/dry/validation/rule/check.rb +15 -0
  10. data/lib/dry/validation/rule/composite.rb +20 -7
  11. data/lib/dry/validation/rule/result.rb +46 -0
  12. data/lib/dry/validation/rule_compiler.rb +14 -0
  13. data/lib/dry/validation/schema.rb +33 -3
  14. data/lib/dry/validation/schema/definition.rb +25 -4
  15. data/lib/dry/validation/schema/key.rb +8 -8
  16. data/lib/dry/validation/schema/result.rb +15 -2
  17. data/lib/dry/validation/schema/rule.rb +32 -5
  18. data/lib/dry/validation/schema/value.rb +15 -6
  19. data/lib/dry/validation/version.rb +1 -1
  20. data/spec/integration/custom_error_messages_spec.rb +1 -1
  21. data/spec/integration/error_compiler_spec.rb +30 -56
  22. data/spec/integration/hints_spec.rb +39 -0
  23. data/spec/integration/localized_error_messages_spec.rb +2 -2
  24. data/spec/integration/schema/check_rules_spec.rb +28 -0
  25. data/spec/integration/schema/each_with_set_spec.rb +71 -0
  26. data/spec/integration/schema/nested_spec.rb +31 -0
  27. data/spec/integration/schema/not_spec.rb +34 -0
  28. data/spec/integration/schema/xor_spec.rb +32 -0
  29. data/spec/integration/schema_form_spec.rb +2 -2
  30. data/spec/integration/schema_spec.rb +1 -1
  31. data/spec/shared/predicates.rb +2 -0
  32. data/spec/spec_helper.rb +2 -2
  33. data/spec/unit/hint_compiler_spec.rb +32 -0
  34. data/spec/unit/predicate_spec.rb +0 -10
  35. data/spec/unit/rule/check_spec.rb +29 -0
  36. data/spec/unit/rule_compiler_spec.rb +44 -7
  37. data/spec/unit/schema/rule_spec.rb +31 -0
  38. data/spec/unit/schema/value_spec.rb +84 -0
  39. metadata +24 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0630ad4e7946a89c0562c0f064220eaa186e2157
4
- data.tar.gz: 10200a87ef1ea0910e3c7acd490f36529df7e6b8
3
+ metadata.gz: 8bb5ae409a8e2f6c8aca26fd536cd36acf079d47
4
+ data.tar.gz: e75a4be1f0dc512db4b4e60b8b3651ee9b1ab480
5
5
  SHA512:
6
- metadata.gz: e829d8b8a77b5ab749700b4a6340684503745008b39985a82bbdd604e00e4ab3e1c41ebbdfda37e1594de822cbc2457bfe88a58a625cfa2d3c51d0fd4ebf9c52
7
- data.tar.gz: 59a24f1ee1d3036f9c6ed2232e2b809b2aa62529916b7e0c0a8490286569992cada68e9324e47b2776eecef144857ddd5cc44803f5da66fcf3572ca5ca253a36
6
+ metadata.gz: 6c186c20423a18a808289dc9823f80e77d5b10239d42918bdbde1c2b2ebb0cd70591f6450edf96a9b7adc526fc98c722ceb553b59fca5d5eba602977015d8459
7
+ data.tar.gz: 30d0e0faaebcdd2fa25ea687f6e07023a8eb9a39094a6feb32a36c6e52d69ff28dbe6bb3d48be2f0077f0d1df9b8df8379e544e3937f84b2a5c78bc8fc755c85
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # v0.4.0 2015-12-21
2
+
3
+ ### Added
4
+
5
+ * Support for high-level rule composition via `rule` interface (solnic)
6
+ * Support for exclusive disjunction (aka xor/^ operator) (solnic)
7
+ * Support for nested schemas within a schema class (solnic)
8
+ * Support for negating rules via `rule(name).not` (solnic)
9
+ * Support for `validation hints` that are included in the error messages (solnic)
10
+
11
+ ### Fixed
12
+
13
+ * Error messages hash has now consistent structure `rule_name => [msgs_array, input_value]` (solnic)
14
+
15
+ [Compare v0.3.1...v0.4.0](https://github.com/dryrb/dry-validation/compare/v0.3.1...v0.4.0)
16
+
1
17
  # v0.3.1 2015-12-08
2
18
 
3
19
  ### Added
data/README.md CHANGED
@@ -23,537 +23,73 @@ a different approach and focuses a lot on explicitness, clarity and preciseness
23
23
  of validation logic. It is designed to work with any data input, whether it's a
24
24
  simple hash, an array or a complex object with deeply nested data.
25
25
 
26
- It is based on a simple idea that each validation is encapsulated by a simple,
26
+ It is based on an idea that each validation is encapsulated by a simple,
27
27
  stateless predicate, that receives some input and returns either `true` or `false`.
28
28
 
29
29
  Those predicates are encapsulated by `rules` which can be composed together using
30
30
  `predicate logic`. This means you can use the common logic operators to build up
31
31
  a validation `schema`.
32
32
 
33
- It's very explicit, powerful and extendible.
34
-
35
33
  Validations can be described with great precision, `dry-validation` eliminates
36
34
  ambigious concepts like `presence` validation where we can't really say whether
37
35
  some attribute or key is *missing* or it's just that the value is `nil`.
38
36
 
39
- There's also the concept of type-safety, completely missing in other validation
40
- libraries, which is quite important and useful. It means you can compose a validation
41
- that does rely on the type of a given value. In example it makes no sense to validate
42
- each element of an array when it turns out to be an empty string.
37
+ In `dry-validation` type-safety is a first-class feature, something that's completely
38
+ missing in other validation libraries, and it's an important and useful feature. It
39
+ means you can compose a validation that does rely on the type of a given value. In
40
+ example it makes no sense to validate each element of an array when it turns out to
41
+ be an empty string.
43
42
 
44
43
  ## The DSL
45
44
 
46
- The core of `dry-validation` is rules composition and predicate logic. The DSL
45
+ The core of `dry-validation` is rule composition and predicate logic. The DSL
47
46
  is a simple front-end for that. It only allows you to define the rules by using
48
47
  predicate identifiers. There are no magical options, conditionals and custom
49
48
  validation blocks known from other libraries. The focus is on pure validation
50
- logic.
51
-
52
- ## Examples
53
-
54
- ### Basic
55
-
56
- Here's a basic example where we validate following things:
57
-
58
- * The input *must have a key* called `:email`
59
- * Provided the email key is present, its value *must be filled*
60
- * The input *must have a key* called `:age`
61
- * Provided the age key is present, its value *must be an integer* and it *must be greater than 18*
62
-
63
- This can be easily expressed through the DSL:
49
+ logic expressed in a concise way.
64
50
 
65
- ``` ruby
66
- require 'dry-validation'
67
-
68
- class Schema < Dry::Validation::Schema
69
- key(:email) { |email| email.filled? }
70
-
71
- key(:age) do |age|
72
- age.int? & age.gt?(18)
73
- end
74
- end
51
+ The DSL is very abstract, it builds [a rule AST](https://github.com/dryrb/dry-validation/wiki/Rule-AST)
52
+ which is compiled into an array of rule objects. This means alternative interfaces could
53
+ be easily build.
75
54
 
76
- schema = Schema.new
55
+ ## When To Use?
77
56
 
78
- errors = schema.call(email: 'jane@doe.org', age: 19).messages
57
+ Always and everywhere. This is a general-purpose validation library that can be used for many things and **it's multiple times faster** than `ActiveRecord`/`ActiveModel::Validations` *and* `strong-parameters`.
79
58
 
80
- puts errors.inspect
81
- # []
59
+ Possible use-cases include validation of:
82
60
 
83
- errors = schema.call(email: nil, age: 19).messages
61
+ * Form params
62
+ * "GET" params
63
+ * JSON documents
64
+ * YAML documents
65
+ * Application configuration (ie stored in ENV)
66
+ * Replacement for `ActiveRecord`/`ActiveModel::Validations`
67
+ * Replacement for `strong-parameters`
68
+ * etc.
84
69
 
85
- puts errors.inspect
86
- # { :email => [["email must be filled", nil]] }
87
- ```
70
+ ## Synopsis
88
71
 
89
- A couple of remarks:
90
-
91
- * `key` assumes that we want to use the `:key?` predicate to check the existance of that key
92
- * `age.gt?(18)` translates to calling a predicate like this: `schema[:gt?].(18, age)`
93
- * `age.int? & age.gt?(18)` is a conjunction, so we don't bother about `gt?` unless `int?` returns `true`
94
- * You can also use `|` for disjunction
95
- * Schema object does not carry the input as its state, nor does it know how to access the input values, we
96
- pass the input to `call` and get error set as the response
97
-
98
- ### Optional Keys
99
-
100
- You can define which keys are optional and define rules for their values:
72
+ Please refer to [the wiki](https://github.com/dryrb/dry-validation/wiki) for full usage documentation.
101
73
 
102
74
  ``` 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.call(email: 'jane@doe.org').messages
116
-
117
- puts errors.inspect
118
- # []
119
-
120
- errors = schema.call(email: 'jane@doe.org', age: 17).messages
121
-
122
- puts errors.inspect
123
- # { :age => [["age must be greater than 18"], 17] }
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.call(email: 'jane@doe.org', age: nil).messages
144
-
145
- puts errors.inspect
146
- # []
147
-
148
- errors = schema.call(email: 'jane@doe.org', age: 19).messages
149
-
150
- puts errors.inspect
151
- # []
152
-
153
- errors = schema.call(email: 'jane@doe.org', age: 17).messages
154
-
155
- puts errors.inspect
156
- # { :age => [["age must be greater than 18"], 17] }
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
-
176
- ### Nested Hash
177
-
178
- We are free to define validations for anything, including deeply nested structures:
179
-
180
- ``` ruby
181
- require 'dry-validation'
182
-
183
- class Schema < Dry::Validation::Schema
75
+ class UserSchema < Dry::Validation::Schema
76
+ key(:name) { |name| name.filled? }
77
+
78
+ key(:email) { |email| email.filled? & email.format?(EMAIL_REGEX) }
79
+
80
+ key(:age) { |age| age.none? | age.int? }
81
+
184
82
  key(:address) do |address|
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
198
- end
199
- end
200
- end
201
-
202
- schema = Schema.new
203
-
204
- errors = schema.call({}).messages
205
-
206
- puts errors.inspect
207
- # { :address => ["address is missing"] }
208
-
209
- errors = schema.call(address: { city: 'NYC' }).messages
210
-
211
- puts errors.to_h.inspect
212
- # {
213
- # :address => [
214
- # { :street => ["street is missing"] },
215
- # { :country => ["country is missing"] }
216
- # ]
217
- # }
218
- ```
219
-
220
- ### Array Elements
221
-
222
- You can use `each` rule for validating each element in an array:
223
-
224
- ``` ruby
225
- class Schema < Dry::Validation::Schema
226
- key(:phone_numbers) do |phone_numbers|
227
- phone_numbers.array? do
228
- phone_numbers.each(&:str?)
229
- end
230
- end
231
- end
232
-
233
- schema = Schema.new
234
-
235
- errors = schema.call(phone_numbers: '').messages
236
-
237
- puts errors.inspect
238
- # { :phone_numbers => [["phone_numbers must be an array", ""]] }
239
-
240
- errors = schema.call(phone_numbers: ['123456789', 123456789]).messages
241
-
242
- puts errors.inspect
243
- # {
244
- # :phone_numbers => [
245
- # {
246
- # :phone_numbers => [
247
- # ["phone_numbers must be a string", 123456789]
248
- # ]
249
- # }
250
- # ]
251
- # }
252
- ```
253
-
254
- ### Rules Depending On Other Rules
255
-
256
- When a rule needs input from other rules and depends on their results you can
257
- define it using `rule` DSL. A common example of this is "confirmation validation":
258
-
259
- ``` ruby
260
- class Schema < Dry::Validation::Schema
261
- key(:password, &:filled?)
262
- key(:password_confirmation, &:filled?)
263
-
264
- rule(:password_confirmation, eql?: [:password, :password_confirmation])
265
- end
266
- ```
267
-
268
- A short version of the same thing:
269
-
270
- ``` ruby
271
- class Schema < Dry::Validation::Schema
272
- confirmation(:password)
273
- end
274
- ```
275
-
276
- Notice that you must add `:password_confirmation` error message configuration if
277
- you want to have the error converted to a message.
278
-
279
- ### Form Validation With Coercions
280
-
281
- Probably the most common use case is to validate form params. This is a special
282
- kind of a validation for a couple of reasons:
283
-
284
- * The input is a hash with stringified keys
285
- * The input include values that are strings, hashes or arrays
286
- * Prior validation, we need to coerce values and symbolize keys based on the
287
- information from rules
288
-
289
- For that reason, `dry-validation` ships with `Schema::Form` class:
290
-
291
- ``` ruby
292
- require 'dry-validation'
293
- require 'dry/validation/schema/form'
294
-
295
- class UserFormSchema < Dry::Validation::Schema::Form
296
- key(:email) { |value| value.str? & value.filled? }
297
-
298
- key(:age) { |value| value.int? & value.gt?(18) }
299
- end
300
-
301
- schema = UserFormSchema.new
302
-
303
- errors = schema.call('email' => '', 'age' => '18').messages
304
-
305
- puts errors.inspect
306
- # {
307
- # :email => [["email must be filled", nil]],
308
- # :age => [["age must be greater than 18 (18 was given)", 18]]
309
- # }
310
- ```
311
-
312
- There are few major differences between how it works here and in `ActiveModel`:
313
-
314
- * We have type checking as predicates, ie `gt?(18)` will not be applied if the value
315
- is not an integer
316
- * Thus, error messages are provided *only for the rules that failed*
317
- * There's a planned feature for generating "validation hints" which lists information
318
- about all possible rules
319
- * Coercion is handled by `dry-data` coercible hash using its `form.*` types that
320
- are dedicated for this type of coercions
321
- * It's very easy to add your own types and coercions (more info/docs coming soon)
322
-
323
- ### Defining Custom Predicates
324
-
325
- You can simply define predicate methods on your schema object:
326
-
327
- ``` ruby
328
- class Schema < Dry::Validation::Schema
329
- key(:email) { |value| value.str? & value.email? }
330
-
331
- def email?(value)
332
- ! /magical-regex-that-matches-emails/.match(value).nil?
83
+ address.key(:street, &:filled?)
84
+ address.key(:city, &:filled?)
85
+ address.key(:zipcode, &:filled?)
333
86
  end
334
87
  end
335
88
  ```
336
89
 
337
- You can also re-use a predicate container across multiple schemas:
338
-
339
- ``` ruby
340
- module MyPredicates
341
- include Dry::Validation::Predicates
342
-
343
- predicate(:email?) do |input|
344
- ! /magical-regex-that-matches-emails/.match(value).nil?
345
- end
346
- end
347
-
348
- class Schema < Dry::Validation::Schema
349
- configure do |config|
350
- config.predicates = MyPredicates
351
- end
352
-
353
- key(:email) { |value| value.str? & value.email? }
354
- end
355
- ```
356
-
357
- You need to provide error messages for your custom predicates if you want them
358
- to work with `Schem#messages` interface.
359
-
360
- You can learn how to do that in the [Error Messages](https://github.com/dryrb/dry-validation#error-messages) section.
361
-
362
- ## List of Built-In Predicates
363
-
364
- ### Basic
365
-
366
- * `none?`
367
- * `eql?`
368
- * `key?`
369
-
370
- ### Types
371
-
372
- * `str?`
373
- * `int?`
374
- * `float?`
375
- * `decimal?`
376
- * `bool?`
377
- * `date?`
378
- * `date_time?`
379
- * `time?`
380
- * `array?`
381
- * `hash?`
382
-
383
- ### Number, String, Collection
384
-
385
- * `empty?`
386
- * `filled?`
387
- * `gt?`
388
- * `gteq?`
389
- * `lt?`
390
- * `lteq?`
391
- * `max_size?`
392
- * `min_size?`
393
- * `size?(int)`
394
- * `size?(range)`
395
- * `format?`
396
- * `inclusion?`
397
- * `exclusion?`
398
-
399
- ## Error Messages
400
-
401
- By default `dry-validation` comes with a set of pre-defined error messages for
402
- every built-in predicate. They are defined in [a yaml file](https://github.com/dryrb/dry-validation/blob/master/config/errors.yml)
403
- which is shipped with the gem. This file is compatible with `I18n` format.
404
-
405
- You can provide your own messages and configure your schemas to use it like that:
406
-
407
- ``` ruby
408
- class Schema < Dry::Validation::Schema
409
- configure { |config| config.messages_file = '/path/to/my/errors.yml' }
410
- end
411
- ```
412
-
413
- You can also provide a namespace per-schema that will be used by default:
414
-
415
- ``` ruby
416
- class Schema < Dry::Validation::Schema
417
- configure { |config| config.namespace = :user }
418
- end
419
- ```
420
-
421
- Lookup rules:
422
-
423
- ``` yaml
424
- en:
425
- errors:
426
- size?:
427
- arg:
428
- default: "%{name} size must be %{num}"
429
- range: "%{name} size must be within %{left} - %{right}"
430
-
431
- value:
432
- string:
433
- arg:
434
- default: "%{name} length must be %{num}"
435
- range: "%{name} length must be within %{left} - %{right}"
436
-
437
- filled?: "%{name} must be filled"
438
-
439
- rules:
440
- email:
441
- filled?: "the email is missing"
442
-
443
- user:
444
- filled?: "%{name} name cannot be blank"
445
-
446
- rules:
447
- address:
448
- filled?: "You gotta tell us where you live"
449
- ```
450
-
451
- Given the yaml file above, messages lookup works as follows:
452
-
453
- ``` ruby
454
- messages = Dry::Validation::Messages.load('/path/to/our/errors.yml')
455
-
456
- # matching arg type for size? predicate
457
- messages[:size?, rule: :name, arg_type: Fixnum] # => "%{name} size must be %{num}"
458
- messages[:size?, rule: :name, arg_type: Range] # => "%{name} size must within %{left} - %{right}"
459
-
460
- # matching val type for size? predicate
461
- messages[:size?, rule: :name, val_type: String] # => "%{name} length must be %{num}"
462
-
463
- # matching predicate
464
- messages[:filled?, rule: :age] # => "%{name} must be filled"
465
- messages[:filled?, rule: :address] # => "%{name} must be filled"
466
-
467
- # matching predicate for a specific rule
468
- messages[:filled?, rule: :email] # => "the email is missing"
469
-
470
- # with namespaced messages
471
- user_messages = messages.namespaced(:user)
472
-
473
- user_messages[:filled?, rule: :age] # "%{name} cannot be blank"
474
- user_messages[:filled?, rule: :address] # "You gotta tell us where you live"
475
- ```
476
-
477
- By configuring `messages_file` and/or `namespace` in a schema, default messages
478
- are going to be automatically merged with your overrides and/or namespaced.
479
-
480
- ## I18n Integration
481
-
482
- If you are using `i18n` gem and load it before `dry-validation` then you'll be
483
- able to configure a schema to use `i18n` messages:
484
-
485
- ``` ruby
486
- require 'i18n'
487
- require 'dry-validation'
488
-
489
- class Schema < Dry::Validation::Schema
490
- configure { config.messages = :i18n }
491
-
492
- key(:email, &:filled?)
493
- end
494
-
495
- schema = Schema.new
496
-
497
- # return default translations
498
- puts schema.call(email: '').messages
499
- { :email => ["email must be filled"] }
500
-
501
- # return other translations (assuming you have it :))
502
- puts schema.call(email: '').messages(locale: :pl)
503
- { :email => ["email musi być wypełniony"] }
504
- ```
505
-
506
- Important: I18n must be initialized before using schema, `dry-validation` does
507
- not try to do it for you, it only sets its default error translations automatically.
508
-
509
- ## Rule AST
510
-
511
- Internally, `dry-validation` uses a simple AST representation of rules and errors
512
- to produce rule objects and error messages. If you would like to programatically
513
- generate rules, it is a very simple process:
514
-
515
- ``` ruby
516
- ast = [
517
- [
518
- :and,
519
- [
520
- [:key, [:age, [:predicate, [:key?, []]]]],
521
- [
522
- :and,
523
- [
524
- [:val, [:age, [:predicate, [:filled?, []]]]],
525
- [:val, [:age, [:predicate, [:gt?, [18]]]]]
526
- ]
527
- ]
528
- ]
529
- ]
530
- ]
531
-
532
- compiler = Dry::Validation::RuleCompiler.new(Dry::Validation::Predicates)
533
-
534
- # compile an array of rule objects
535
- rules = compiler.call(ast)
536
-
537
- puts rules.inspect
538
- # [
539
- # #<Dry::Validation::Rule::Conjunction
540
- # left=#<Dry::Validation::Rule::Key name=:age predicate=#<Dry::Validation::Predicate id=:key?>>
541
- # right=#<Dry::Validation::Rule::Conjunction
542
- # left=#<Dry::Validation::Rule::Value name=:age predicate=#<Dry::Validation::Predicate id=:filled?>>
543
- # right=#<Dry::Validation::Rule::Value name=:age predicate=#<Dry::Validation::Predicate id=:gt?>>>>
544
- # ]
545
-
546
- # dump it back to ast
547
- puts rules.map(&:to_ary).inspect
548
- # [[:and, [:key, [:age, [:predicate, [:key?, [:age]]]]], [[:and, [:val, [:age, [:predicate, [:filled?, []]]]], [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]]]
549
- ```
550
-
551
- Complete docs for the AST format are coming soon, for now please refer to
552
- [this spec](https://github.com/dryrb/dry-validation/blob/master/spec/unit/rule_compiler_spec.rb).
553
-
554
90
  ## Status and Roadmap
555
91
 
556
- This library is in a very early stage of development but you are encauraged to
92
+ This library is in an early stage of development but you are encauraged to
557
93
  try it out and provide feedback.
558
94
 
559
95
  For planned features check out [the issues](https://github.com/dryrb/dry-validation/labels/feature).