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
@@ -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)