dry-validation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +16 -0
  5. data/.rubocop_todo.yml +7 -0
  6. data/.travis.yml +29 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE +20 -0
  10. data/README.md +297 -0
  11. data/Rakefile +12 -0
  12. data/config/errors.yml +35 -0
  13. data/dry-validation.gemspec +25 -0
  14. data/examples/basic.rb +21 -0
  15. data/examples/nested.rb +30 -0
  16. data/examples/rule_ast.rb +33 -0
  17. data/lib/dry-validation.rb +1 -0
  18. data/lib/dry/validation.rb +12 -0
  19. data/lib/dry/validation/error.rb +43 -0
  20. data/lib/dry/validation/error_compiler.rb +116 -0
  21. data/lib/dry/validation/messages.rb +71 -0
  22. data/lib/dry/validation/predicate.rb +39 -0
  23. data/lib/dry/validation/predicate_set.rb +22 -0
  24. data/lib/dry/validation/predicates.rb +88 -0
  25. data/lib/dry/validation/result.rb +64 -0
  26. data/lib/dry/validation/rule.rb +125 -0
  27. data/lib/dry/validation/rule_compiler.rb +57 -0
  28. data/lib/dry/validation/schema.rb +74 -0
  29. data/lib/dry/validation/schema/definition.rb +15 -0
  30. data/lib/dry/validation/schema/key.rb +39 -0
  31. data/lib/dry/validation/schema/rule.rb +28 -0
  32. data/lib/dry/validation/schema/value.rb +31 -0
  33. data/lib/dry/validation/version.rb +5 -0
  34. data/rakelib/rubocop.rake +18 -0
  35. data/spec/fixtures/errors.yml +4 -0
  36. data/spec/integration/custom_error_messages_spec.rb +35 -0
  37. data/spec/integration/custom_predicates_spec.rb +57 -0
  38. data/spec/integration/validation_spec.rb +118 -0
  39. data/spec/shared/predicates.rb +31 -0
  40. data/spec/spec_helper.rb +18 -0
  41. data/spec/unit/error_compiler_spec.rb +165 -0
  42. data/spec/unit/predicate_spec.rb +37 -0
  43. data/spec/unit/predicates/empty_spec.rb +38 -0
  44. data/spec/unit/predicates/eql_spec.rb +21 -0
  45. data/spec/unit/predicates/exclusion_spec.rb +35 -0
  46. data/spec/unit/predicates/filled_spec.rb +38 -0
  47. data/spec/unit/predicates/format_spec.rb +21 -0
  48. data/spec/unit/predicates/gt_spec.rb +40 -0
  49. data/spec/unit/predicates/gteq_spec.rb +40 -0
  50. data/spec/unit/predicates/inclusion_spec.rb +35 -0
  51. data/spec/unit/predicates/int_spec.rb +34 -0
  52. data/spec/unit/predicates/key_spec.rb +29 -0
  53. data/spec/unit/predicates/lt_spec.rb +40 -0
  54. data/spec/unit/predicates/lteq_spec.rb +40 -0
  55. data/spec/unit/predicates/max_size_spec.rb +49 -0
  56. data/spec/unit/predicates/min_size_spec.rb +49 -0
  57. data/spec/unit/predicates/nil_spec.rb +28 -0
  58. data/spec/unit/predicates/size_spec.rb +49 -0
  59. data/spec/unit/predicates/str_spec.rb +32 -0
  60. data/spec/unit/rule/each_spec.rb +20 -0
  61. data/spec/unit/rule/key_spec.rb +27 -0
  62. data/spec/unit/rule/set_spec.rb +32 -0
  63. data/spec/unit/rule/value_spec.rb +42 -0
  64. data/spec/unit/rule_compiler_spec.rb +86 -0
  65. metadata +230 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6acd14c8b93a29eed518ef8934921cf35d0faf17
4
+ data.tar.gz: 83c9f549fb92f5ff4cf3f5c95ec8029e81c045d2
5
+ SHA512:
6
+ metadata.gz: b7411edfe3d00dbc645d9c48a7df9dd952bf36c01e59f1a55e39114e0a757afd6b4e66b57a07380b658a146b2757651f4a1b47d61f29565d464d2b7896cdb04f
7
+ data.tar.gz: 50201382e0ed26bcebcf19f1d9dafe2c7c3ee260c9654afdb3b6002ce7cbf4bc83ad549e6ffff9f6636ca1fb9aa6eaadea1371044a08e33425443768eef2e512
@@ -0,0 +1,8 @@
1
+ .DS_Store
2
+ coverage
3
+ /.bundle
4
+ vendor/bundle
5
+ bin/
6
+ tmp/
7
+ .idea/
8
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --order random
@@ -0,0 +1,16 @@
1
+ # Generated by `rubocop --auto-gen-config`
2
+ inherit_from: .rubocop_todo.yml
3
+
4
+ Metrics/LineLength:
5
+ Max: 100
6
+
7
+ Style/Documentation:
8
+ Enabled: false
9
+
10
+ Lint/HandleExceptions:
11
+ Exclude:
12
+ - rakelib/*.rake
13
+
14
+ Style/FileName:
15
+ Exclude:
16
+ - lib/dry-validation.rb
@@ -0,0 +1,7 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2015-10-30 01:32:46 +0000 using RuboCop version 0.34.2.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
@@ -0,0 +1,29 @@
1
+ language: ruby
2
+ sudo: false
3
+ cache: bundler
4
+ bundler_args: --without console
5
+ script:
6
+ - bundle exec rake spec
7
+ rvm:
8
+ - 2.0
9
+ - 2.1
10
+ - 2.2
11
+ - rbx-2
12
+ - jruby-9000
13
+ - ruby-head
14
+ - jruby-head
15
+ env:
16
+ global:
17
+ - JRUBY_OPTS='--dev -J-Xmx1024M'
18
+ matrix:
19
+ allow_failures:
20
+ - rvm: ruby-head
21
+ - rvm: jruby-head
22
+ notifications:
23
+ email: false
24
+ webhooks:
25
+ urls:
26
+ - https://webhooks.gitter.im/e/19098b4253a72c9796db
27
+ on_success: change # options: [always|never|change] default: always
28
+ on_failure: always # options: [always|never|change] default: always
29
+ on_start: false # default: false
@@ -0,0 +1,3 @@
1
+ # v0.1.0 2015-11-25
2
+
3
+ First public release
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :tools do
6
+ gem 'rubocop'
7
+ gem 'guard'
8
+ gem 'guard-rspec'
9
+ gem 'guard-rubocop'
10
+ gem 'byebug', platform: :mri
11
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Ruby Object Mapper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,297 @@
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>
8
+
9
+ Data validation library based on predicate logic and rule composition.
10
+
11
+ ## Overview
12
+
13
+ Unlike other, well known, validation solutions in Ruby, `dry-validation` takes
14
+ a different approach and focuses a lot on explicitness, clarity and preciseness
15
+ of validation logic. It is designed to work with any data input, whether it's a
16
+ simple hash, an array or a complex object with deeply nested data.
17
+
18
+ It is based on a simple idea that each validation is encapsulated by a simple,
19
+ stateless predicate, that receives some input and returns either `true` or `false`.
20
+
21
+ Those predicates are encapsulated by `rules` which can be composed together using
22
+ `predicate logic`. This means you can use the common logic operators to build up
23
+ a validation `schema`.
24
+
25
+ It's very explicit, powerful and extendible.
26
+
27
+ Validations can be described with great precision, `dry-validation` eliminates
28
+ ambigious concepts like `presence` validation where we can't really say whether
29
+ some attribute or key is *missing* or it's just that the value is `nil`.
30
+
31
+ There's also the concept of type-safety, completely missing in other validation
32
+ libraries, which is quite important and useful. It means you can compose a validation
33
+ that does rely on the type of a given value. In example it makes no sense to validate
34
+ each element of an array when it turns out to be an empty string.
35
+
36
+ ## The DSL
37
+
38
+ The core of `dry-validation` is rules composition and predicate logic. The DSL
39
+ is a simple front-end for that. It only allows you to define the rules by using
40
+ predicate identifiers. There are no magical options, conditionals and custom
41
+ validation blocks known from other libraries. The focus is on pure validation
42
+ logic.
43
+
44
+ ## Examples
45
+
46
+ ### Basic
47
+
48
+ Here's a basic example where we validate following things:
49
+
50
+ * The input *must have a key* called `:email`
51
+ * Provided the email key is present, its value *must be filled*
52
+ * The input *must have a key* called `:age`
53
+ * Provided the age key is present, its value *must be an integer* and it *must be greater than 18*
54
+
55
+ This can be easily expressed through the DSL:
56
+
57
+ ``` ruby
58
+ require 'dry-validation'
59
+
60
+ class Schema < Dry::Validation::Schema
61
+ key(:email) { |email| email.filled? }
62
+
63
+ key(:age) do |age|
64
+ age.int? & age.gt?(18)
65
+ end
66
+ end
67
+
68
+ schema = Schema.new
69
+
70
+ errors = schema.messages(email: 'jane@doe.org', age: 19)
71
+
72
+ puts errors.inspect
73
+ # []
74
+
75
+ errors = schema.messages(email: nil, age: 19)
76
+
77
+ puts errors.inspect
78
+ # [[:email, ["email must be filled"]]]
79
+ ```
80
+
81
+ A couple of remarks:
82
+
83
+ * `key` assumes that we want to use the `:key?` predicate to check the existance of that key
84
+ * `age.gt?(18)` translates to calling a predicate like this: `schema[:gt?].(18, age)`
85
+ * `age.int? & age.gt?(18)` is a conjunction, so we don't bother about `gt?` unless `int?` returns `true`
86
+ * You can also use `|` for disjunction
87
+ * Schema object does not carry the input as its state, nor does it know how to access the input values, we
88
+ pass the input to `call` and get error set as the response
89
+
90
+ ### Nested Hash
91
+
92
+ We are free to define validations for anything, including deeply nested structures:
93
+
94
+ ``` ruby
95
+ require 'dry-validation'
96
+
97
+ class Schema < Dry::Validation::Schema
98
+ 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?)
110
+ end
111
+ end
112
+ end
113
+
114
+ schema = Schema.new
115
+
116
+ errors = schema.messages({})
117
+
118
+ puts errors.inspect
119
+ # [[:address, ["address is missing"]]]
120
+
121
+ errors = schema.messages(address: { city: 'NYC' })
122
+
123
+ puts errors.inspect
124
+ # [[:address, [[:street, ["street is missing"]], [:country, ["country is missing"]]]]]
125
+ ```
126
+
127
+ ### Defining Custom Predicates
128
+
129
+ You can simply define predicate methods on your schema object:
130
+
131
+ ``` ruby
132
+ class Schema < Dry::Validation::Schema
133
+ key(:email) { |value| value.str? & value.email? }
134
+
135
+ def email?(value)
136
+ ! /magical-regex-that-matches-emails/.match(value).nil?
137
+ end
138
+ end
139
+ ```
140
+
141
+ You can also re-use a predicate container across multiple schemas:
142
+
143
+ ``` ruby
144
+ module MyPredicates
145
+ include Dry::Validation::Predicates
146
+
147
+ predicate(:email?) do |input|
148
+ ! /magical-regex-that-matches-emails/.match(value).nil?
149
+ end
150
+ end
151
+
152
+ class Schema < Dry::Validation::Schema
153
+ configure do |config|
154
+ config.predicates = MyPredicates
155
+ end
156
+
157
+ key(:email) { |value| value.str? & value.email? }
158
+ end
159
+ ```
160
+
161
+ ## List of Built-In Predicates
162
+
163
+ * `empty?`
164
+ * `eql?`
165
+ * `exclusion?`
166
+ * `filled?`
167
+ * `format?`
168
+ * `gt?`
169
+ * `gteq?`
170
+ * `inclusion?`
171
+ * `int?`
172
+ * `key?`
173
+ * `lt?`
174
+ * `lteq?`
175
+ * `max_size?`
176
+ * `min_size?`
177
+ * `nil?`
178
+ * `size?`
179
+ * `str?`
180
+
181
+ ## Error Messages
182
+
183
+ By default `dry-validation` comes with a set of pre-defined error messages for
184
+ every built-in predicate. They are defined in [a yaml file](https://github.com/dryrb/dry-validation/blob/master/config/errors.yml)
185
+ which is shipped with the gem.
186
+
187
+ You can provide your own messages and configure your schemas to use it like that:
188
+
189
+ ``` ruby
190
+ class Schema < Dry::Validation::Schema
191
+ configure { |config| config.messages_file = '/path/to/my/errors.yml' }
192
+ end
193
+ ```
194
+
195
+ You can also provide a namespace per-schema that will be used by default:
196
+
197
+ ``` ruby
198
+ class Schema < Dry::Validation::Schema
199
+ configure { |config| config.namespace = :user }
200
+ end
201
+ ```
202
+
203
+ Lookup rules:
204
+
205
+ ``` yaml
206
+ filled?: "%{name} must be filled"
207
+
208
+ attributes:
209
+ email:
210
+ filled?: "the email is missing"
211
+
212
+ user:
213
+ filled?: "%{name} name cannot be blank"
214
+
215
+ attributes:
216
+ address:
217
+ filled?: "You gotta tell us where you live"
218
+ ```
219
+
220
+ Given the yaml file above, messages lookup works as follows:
221
+
222
+ ``` ruby
223
+ messages = Dry::Validation::Messages.load('/path/to/our/errors.yml')
224
+
225
+ messages.lookup(:filled?, :age) # => "age must be filled"
226
+ messages.lookup(:filled?, :address) # => "address must be filled"
227
+ messages.lookup(:filled?, :email) # => "the email is missing"
228
+
229
+ # with namespaced messages
230
+ user_messages = messages.namespaced(:user)
231
+
232
+ user_messages.lookup(:filled?, :age) # "age cannot be blank"
233
+ user_messages.lookup(:filled?, :address) # "You gotta tell us where you live"
234
+ ```
235
+
236
+ By configuring `messages_file` and/or `namespace` in a schema, default messages
237
+ are going to be automatically merged with your overrides and/or namespaced.
238
+
239
+ ## I18n Integration
240
+
241
+ Coming (very) soon...
242
+
243
+ ## Rule AST
244
+
245
+ Internally, `dry-validation` uses a simple AST representation of rules and errors
246
+ to produce rule objects and error messages. If you would like to programatically
247
+ generate rules, it is a very simple process:
248
+
249
+ ``` ruby
250
+ ast = [
251
+ [
252
+ :and,
253
+ [
254
+ [:key, [:age, [:predicate, [:key?, []]]]],
255
+ [
256
+ :and,
257
+ [
258
+ [:val, [:age, [:predicate, [:filled?, []]]]],
259
+ [:val, [:age, [:predicate, [:gt?, [18]]]]]
260
+ ]
261
+ ]
262
+ ]
263
+ ]
264
+ ]
265
+
266
+ compiler = Dry::Validation::RuleCompiler.new(Dry::Validation::Predicates)
267
+
268
+ # compile an array of rule objects
269
+ rules = compiler.call(ast)
270
+
271
+ puts rules.inspect
272
+ # [
273
+ # #<Dry::Validation::Rule::Conjunction
274
+ # left=#<Dry::Validation::Rule::Key name=:age predicate=#<Dry::Validation::Predicate id=:key?>>
275
+ # right=#<Dry::Validation::Rule::Conjunction
276
+ # left=#<Dry::Validation::Rule::Value name=:age predicate=#<Dry::Validation::Predicate id=:filled?>>
277
+ # right=#<Dry::Validation::Rule::Value name=:age predicate=#<Dry::Validation::Predicate id=:gt?>>>>
278
+ # ]
279
+
280
+ # dump it back to ast
281
+ puts rules.map(&:to_ary).inspect
282
+ # [[:and, [:key, [:age, [:predicate, [:key?, [:age]]]]], [[:and, [:val, [:age, [:predicate, [:filled?, []]]]], [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]]]
283
+ ```
284
+
285
+ Complete docs for the AST format are coming soon, for now please refer to
286
+ [this spec](https://github.com/dryrb/dry-validation/blob/master/spec/unit/rule_compiler_spec.rb).
287
+
288
+ ## Status and Roadmap
289
+
290
+ This library is in a very early stage of development but you are encauraged to
291
+ try it out and provide feedback.
292
+
293
+ For planned features check out [the issues](https://github.com/dryrb/dry-validation/labels/feature).
294
+
295
+ ## License
296
+
297
+ See `LICENSE` file.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
5
+
6
+ require 'rspec/core'
7
+ require 'rspec/core/rake_task'
8
+
9
+ task default: :spec
10
+
11
+ desc 'Run all specs in spec directory'
12
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,35 @@
1
+ empty?: "%{name} cannot be empty"
2
+
3
+ exclusion?: "%{name} must not be one of: %{list}"
4
+
5
+ eql?: "%{name} must be equal to %{eql_value}"
6
+
7
+ filled?: "%{name} must be filled"
8
+
9
+ format?: "%{name} is in invalid format"
10
+
11
+ gt?: "%{name} must be greater than %{num} (%{value} was given)"
12
+
13
+ gteq?: "%{name} must be greater than or equal to %{num}"
14
+
15
+ inclusion?: "%{name} must be one of: %{list}"
16
+
17
+ int?: "%{name} must be an integer"
18
+
19
+ key?: "%{name} is missing"
20
+
21
+ lt?: "%{name} must be less than %{num} (%{value} was given)"
22
+
23
+ lteq?: "%{name} must be less than or equal to %{num}"
24
+
25
+ max_size?: "%{name} size cannot be greater than %{num}"
26
+
27
+ min_size?: "%{name} size cannot be less than %{num}"
28
+
29
+ nil?: "%{name} cannot be nil"
30
+
31
+ size?:
32
+ range: "%{name} size must be within %{left} - %{right}"
33
+ default: "%{name} size must be %{num}"
34
+
35
+ str?: "%{name} must be a string"