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
@@ -5,6 +5,10 @@ module Dry
5
5
  def key(name, &block)
6
6
  Key.new(name, rules).key?(&block)
7
7
  end
8
+
9
+ def optional(name, &block)
10
+ Key.new(name, rules).optional(&block)
11
+ end
8
12
  end
9
13
  end
10
14
  end
@@ -0,0 +1,19 @@
1
+ require 'dry/validation/schema'
2
+ require 'dry/validation/input_type_compiler'
3
+
4
+ module Dry
5
+ module Validation
6
+ class Schema::Form < Schema
7
+ attr_reader :input_type
8
+
9
+ def initialize
10
+ super
11
+ @input_type = InputTypeCompiler.new.(self.class.rules.map(&:to_ary))
12
+ end
13
+
14
+ def call(input)
15
+ super(input_type[input])
16
+ end
17
+ end
18
+ end
19
+ end
@@ -11,6 +11,19 @@ module Dry
11
11
  @rules = rules
12
12
  end
13
13
 
14
+ def optional(&block)
15
+ key_rule = key?
16
+
17
+ val_rule = yield(Value.new(name))
18
+
19
+ rules <<
20
+ if val_rule.is_a?(Array)
21
+ Schema::Rule.new([:implication, [key_rule.to_ary, [:set, [name, val_rule.map(&:to_ary)]]]])
22
+ else
23
+ Schema::Rule.new([:implication, [key_rule.to_ary, val_rule.to_ary]])
24
+ end
25
+ end
26
+
14
27
  private
15
28
 
16
29
  def method_missing(meth, *args, &block)
@@ -21,12 +34,12 @@ module Dry
21
34
 
22
35
  rules <<
23
36
  if val_rule.is_a?(Array)
24
- Definition::Rule.new([:and, [key_rule, [:set, [name, val_rule.map(&:to_ary)]]]])
37
+ Schema::Rule.new([:and, [key_rule, [:set, [name, val_rule.map(&:to_ary)]]]])
25
38
  else
26
- Definition::Rule.new([:and, [key_rule, val_rule.to_ary]])
39
+ Schema::Rule.new([:and, [key_rule, val_rule.to_ary]])
27
40
  end
28
41
  else
29
- Definition::Rule.new(key_rule)
42
+ Schema::Rule.new(key_rule)
30
43
  end
31
44
  end
32
45
 
@@ -0,0 +1,29 @@
1
+ module Dry
2
+ module Validation
3
+ class Schema::Result
4
+ include Dry::Equalizer(:params, :errors)
5
+ include Enumerable
6
+
7
+ attr_reader :params
8
+
9
+ attr_reader :errors
10
+
11
+ def initialize(params, errors)
12
+ @params = params
13
+ @errors = errors
14
+ end
15
+
16
+ def each(&block)
17
+ errors.each(&block)
18
+ end
19
+
20
+ def empty?
21
+ errors.empty?
22
+ end
23
+
24
+ def to_ary
25
+ errors.map(&:to_ary)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,26 +1,24 @@
1
1
  module Dry
2
2
  module Validation
3
3
  class Schema
4
- module Definition
5
- class Rule
6
- attr_reader :node
4
+ class Rule
5
+ attr_reader :node
7
6
 
8
- def initialize(node)
9
- @node = node
10
- end
7
+ def initialize(node)
8
+ @node = node
9
+ end
11
10
 
12
- def to_ary
13
- node
14
- end
15
- alias_method :to_a, :to_ary
11
+ def to_ary
12
+ node
13
+ end
14
+ alias_method :to_a, :to_ary
16
15
 
17
- def &(other)
18
- self.class.new([:and, [node, other.to_ary]])
19
- end
16
+ def &(other)
17
+ self.class.new([:and, [node, other.to_ary]])
18
+ end
20
19
 
21
- def |(other)
22
- self.class.new([:or, [node, other.to_ary]])
23
- end
20
+ def |(other)
21
+ self.class.new([:or, [node, other.to_ary]])
24
22
  end
25
23
  end
26
24
  end
@@ -13,13 +13,25 @@ module Dry
13
13
 
14
14
  def each(&block)
15
15
  rule = yield(self).to_ary
16
- Definition::Rule.new([:each, [name, rule]])
16
+ Schema::Rule.new([:each, [name, rule]])
17
17
  end
18
18
 
19
19
  private
20
20
 
21
21
  def method_missing(meth, *args, &block)
22
- Definition::Rule.new([:val, [name, [:predicate, [meth, args]]]])
22
+ rule = Schema::Rule.new([:val, [name, [:predicate, [meth, args]]]])
23
+
24
+ if block
25
+ val_rule = yield
26
+
27
+ if val_rule.is_a?(Schema::Rule)
28
+ rule & val_rule
29
+ else
30
+ Schema::Rule.new([:and, [rule.to_ary, [:set, [name, rules.map(&:to_ary)]]]])
31
+ end
32
+ else
33
+ rule
34
+ end
23
35
  end
24
36
 
25
37
  def respond_to_missing?(meth, _include_private = false)
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module Validation
3
- VERSION = '0.1.0'.freeze
3
+ VERSION = '0.2.0'.freeze
4
4
  end
5
5
  end
@@ -26,7 +26,7 @@ RSpec.describe Dry::Validation, 'with custom messages' do
26
26
 
27
27
  describe '#messages' do
28
28
  it 'returns compiled error messages' do
29
- expect(validation.messages(attrs.merge(email: ''))).to eql([
29
+ expect(validation.messages(attrs.merge(email: ''))).to match_array([
30
30
  [:email, ["email can't be blank"]]
31
31
  ])
32
32
  end
@@ -0,0 +1,30 @@
1
+ RSpec.describe Dry::Validation::Schema do
2
+ subject(:validation) { schema.new }
3
+
4
+ describe 'defining schema with optional keys' do
5
+ let(:schema) do
6
+ Class.new(Dry::Validation::Schema) do
7
+ optional(:email) { |email| email.filled? }
8
+
9
+ key(:address) do |address|
10
+ address.key(:city, &:filled?)
11
+ address.key(:street, &:filled?)
12
+
13
+ address.optional(:phone_number) do |phone_number|
14
+ phone_number.none? | phone_number.str?
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ describe '#call' do
21
+ it 'skips rules when key is not present' do
22
+ expect(validation.(address: { city: 'NYC', street: 'Street 1/2' })).to be_empty
23
+ end
24
+
25
+ it 'applies rules when key is present' do
26
+ expect(validation.(email: '')).to_not be_empty
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,99 @@
1
+ require 'dry/validation/schema/form'
2
+
3
+ RSpec.describe Dry::Validation::Schema::Form do
4
+ subject(:validation) { schema.new }
5
+
6
+ describe 'defining schema' do
7
+ let(:schema) do
8
+ Class.new(Dry::Validation::Schema::Form) do
9
+ key(:email) { |email| email.filled? }
10
+
11
+ key(:age) { |age| age.none? | (age.int? & age.gt?(18)) }
12
+
13
+ key(:address) do |address|
14
+ address.hash? do
15
+ address.key(:city, &:filled?)
16
+ address.key(:street, &:filled?)
17
+
18
+ address.key(:loc) do |loc|
19
+ loc.key(:lat) { |lat| lat.filled? & lat.float? }
20
+ loc.key(:lng) { |lng| lng.filled? & lng.float? }
21
+ end
22
+ end
23
+ end
24
+
25
+ optional(:phone_number) { |phone_number| phone_number.none? | phone_number.str? }
26
+ end
27
+ end
28
+
29
+ describe '#messages' do
30
+ it 'returns compiled error messages' do
31
+ result = validation.messages('email' => '', 'age' => '19')
32
+
33
+ expect(result).to match_array([
34
+ [:email, ["email must be filled"]],
35
+ [:address, ["address is missing"]]
36
+ ])
37
+
38
+ expect(result.params).to eql(email: '', age: 19)
39
+ end
40
+ end
41
+
42
+ describe '#call' do
43
+ it 'passes when attributes are valid' do
44
+ result = validation.(
45
+ 'email' => 'jane@doe.org',
46
+ 'age' => '19',
47
+ 'address' => {
48
+ 'city' => 'NYC',
49
+ 'street' => 'Street 1/2',
50
+ 'loc' => { 'lat' => '123.456', 'lng' => '456.123' }
51
+ }
52
+ )
53
+
54
+ expect(result).to be_empty
55
+
56
+ expect(result.params).to eql(
57
+ email: 'jane@doe.org', age: 19,
58
+ address: {
59
+ city: 'NYC', street: 'Street 1/2',
60
+ loc: { lat: 123.456, lng: 456.123 }
61
+ }
62
+ )
63
+ end
64
+
65
+ it 'validates presence of an email and min age value' do
66
+ expect(validation.('email' => '', 'age' => '18')).to match_array([
67
+ [:error, [:input, [:age, 18, [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]],
68
+ [:error, [:input, [:email, "", [[:val, [:email, [:predicate, [:filled?, []]]]]]]]],
69
+ [:error, [:input, [:address, nil, [[:key, [:address, [:predicate, [:key?, [:address]]]]]]]]]
70
+ ])
71
+ end
72
+
73
+ it 'handles optionals' do
74
+ result = validation.(
75
+ 'email' => 'jane@doe.org',
76
+ 'age' => '19',
77
+ 'phone_number' => 12,
78
+ 'address' => {
79
+ 'city' => 'NYC',
80
+ 'street' => 'Street 1/2',
81
+ 'loc' => { 'lat' => '123.456', 'lng' => '456.123' }
82
+ }
83
+ )
84
+
85
+ expect(result).to match_array([
86
+ [:error, [:input, [:phone_number, 12, [[:val, [:phone_number, [:predicate, [:str?, []]]]]]]]],
87
+ ])
88
+
89
+ expect(result.params).to eql(
90
+ email: 'jane@doe.org', age: 19, phone_number: 12,
91
+ address: {
92
+ city: 'NYC', street: 'Street 1/2',
93
+ loc: { lat: 123.456, lng: 456.123 }
94
+ }
95
+ )
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,4 +1,4 @@
1
- RSpec.describe Dry::Validation do
1
+ RSpec.describe Dry::Validation::Schema do
2
2
  subject(:validation) { schema.new }
3
3
 
4
4
  describe 'defining schema' do
@@ -7,26 +7,28 @@ RSpec.describe Dry::Validation do
7
7
  key(:email) { |email| email.filled? }
8
8
 
9
9
  key(:age) do |age|
10
- age.int? & age.gt?(18)
10
+ age.none? | (age.int? & age.gt?(18))
11
11
  end
12
12
 
13
13
  key(:address) do |address|
14
- address.key(:city) do |city|
15
- city.min_size?(3)
16
- end
14
+ address.hash? do
15
+ address.key(:city) do |city|
16
+ city.min_size?(3)
17
+ end
17
18
 
18
- address.key(:street) do |street|
19
- street.filled?
20
- end
19
+ address.key(:street) do |street|
20
+ street.filled?
21
+ end
21
22
 
22
- address.key(:country) do |country|
23
- country.key(:name, &:filled?)
24
- country.key(:code, &:filled?)
23
+ address.key(:country) do |country|
24
+ country.key(:name, &:filled?)
25
+ country.key(:code, &:filled?)
26
+ end
25
27
  end
26
28
  end
27
29
 
28
30
  key(:phone_numbers) do |phone_numbers|
29
- phone_numbers.each(&:str?)
31
+ phone_numbers.array? { phone_numbers.each(&:str?) }
30
32
  end
31
33
  end
32
34
  end
@@ -44,7 +46,7 @@ RSpec.describe Dry::Validation do
44
46
 
45
47
  describe '#messages' do
46
48
  it 'returns compiled error messages' do
47
- expect(validation.messages(attrs.merge(email: ''))).to eql([
49
+ expect(validation.messages(attrs.merge(email: ''))).to match_array([
48
50
  [:email, ["email must be filled"]]
49
51
  ])
50
52
  end
@@ -91,6 +93,12 @@ RSpec.describe Dry::Validation do
91
93
  ])
92
94
  end
93
95
 
96
+ it 'validates address type' do
97
+ expect(validation.(attrs.merge(address: 'totally not a hash'))).to match_array([
98
+ [:error, [:input, [:address, "totally not a hash", [[:val, [:address, [:predicate, [:hash?, []]]]]]]]]
99
+ ])
100
+ end
101
+
94
102
  it 'validates address code and name values' do
95
103
  expect(validation.(attrs.merge(address: attrs[:address].merge(country: { code: 'US', name: '' })))).to match_array([
96
104
  [:error, [
@@ -113,6 +121,25 @@ RSpec.describe Dry::Validation do
113
121
  ]]
114
122
  ])
115
123
  end
124
+
125
+ it 'validates each phone number' do
126
+ expect(validation.(attrs.merge(phone_numbers: ['123', 312]))).to match_array([
127
+ [:error, [
128
+ :input, [
129
+ :phone_numbers, ["123", 312],
130
+ [
131
+ [
132
+ :input, [
133
+ :phone_numbers, 312, [
134
+ [:val, [:phone_numbers, [:predicate, [:str?, []]]]]
135
+ ]
136
+ ]
137
+ ]
138
+ ]
139
+ ]
140
+ ]]
141
+ ])
142
+ end
116
143
  end
117
144
  end
118
145
  end
@@ -1,7 +1,7 @@
1
1
  require 'dry/validation/predicates'
2
2
 
3
3
  RSpec.shared_examples 'predicates' do
4
- let(:nil?) { Dry::Validation::Predicates[:nil?] }
4
+ let(:none?) { Dry::Validation::Predicates[:none?] }
5
5
 
6
6
  let(:str?) { Dry::Validation::Predicates[:str?] }
7
7
 
@@ -92,6 +92,22 @@ RSpec.describe Dry::Validation::ErrorCompiler do
92
92
  end
93
93
  end
94
94
 
95
+ describe ':hash?' do
96
+ it 'returns valid message' do
97
+ msg = error_compiler.visit_predicate([:hash?, []], '', :address)
98
+
99
+ expect(msg).to eql('address must be a hash')
100
+ end
101
+ end
102
+
103
+ describe ':array?' do
104
+ it 'returns valid message' do
105
+ msg = error_compiler.visit_predicate([:array?, []], '', :phone_numbers)
106
+
107
+ expect(msg).to eql('phone_numbers must be an array')
108
+ end
109
+ end
110
+
95
111
  describe ':int?' do
96
112
  it 'returns valid message' do
97
113
  msg = error_compiler.visit_predicate([:int?, []], '2', :num)
@@ -100,6 +116,46 @@ RSpec.describe Dry::Validation::ErrorCompiler do
100
116
  end
101
117
  end
102
118
 
119
+ describe ':float?' do
120
+ it 'returns valid message' do
121
+ msg = error_compiler.visit_predicate([:float?, []], '2', :num)
122
+
123
+ expect(msg).to eql('num must be a float')
124
+ end
125
+ end
126
+
127
+ describe ':decimal?' do
128
+ it 'returns valid message' do
129
+ msg = error_compiler.visit_predicate([:decimal?, []], '2', :num)
130
+
131
+ expect(msg).to eql('num must be a decimal')
132
+ end
133
+ end
134
+
135
+ describe ':date?' do
136
+ it 'returns valid message' do
137
+ msg = error_compiler.visit_predicate([:date?, []], '2', :num)
138
+
139
+ expect(msg).to eql('num must be a date')
140
+ end
141
+ end
142
+
143
+ describe ':date_time?' do
144
+ it 'returns valid message' do
145
+ msg = error_compiler.visit_predicate([:date_time?, []], '2', :num)
146
+
147
+ expect(msg).to eql('num must be a date time')
148
+ end
149
+ end
150
+
151
+ describe ':time?' do
152
+ it 'returns valid message' do
153
+ msg = error_compiler.visit_predicate([:time?, []], '2', :num)
154
+
155
+ expect(msg).to eql('num must be a time')
156
+ end
157
+ end
158
+
103
159
  describe ':max_size?' do
104
160
  it 'returns valid message' do
105
161
  msg = error_compiler.visit_predicate([:max_size?, [3]], 'abcd', :num)
@@ -146,6 +202,14 @@ RSpec.describe Dry::Validation::ErrorCompiler do
146
202
  end
147
203
  end
148
204
 
205
+ describe ':bool?' do
206
+ it 'returns valid message' do
207
+ msg = error_compiler.visit_predicate([:bool?, []], 3, :num)
208
+
209
+ expect(msg).to eql('num must be boolean')
210
+ end
211
+ end
212
+
149
213
  describe ':format?' do
150
214
  it 'returns valid message' do
151
215
  msg = error_compiler.visit_predicate([:format?, [/^F/]], 'Bar', :str)