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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/CHANGELOG.md +23 -0
- data/README.md +203 -26
- data/config/errors.yml +16 -0
- data/dry-validation.gemspec +1 -0
- data/examples/each.rb +19 -0
- data/examples/form.rb +15 -0
- data/examples/nested.rb +13 -11
- data/lib/dry/validation.rb +5 -0
- data/lib/dry/validation/error_compiler.rb +2 -18
- data/lib/dry/validation/input_type_compiler.rb +78 -0
- data/lib/dry/validation/messages.rb +1 -7
- data/lib/dry/validation/predicates.rb +33 -1
- data/lib/dry/validation/result.rb +8 -0
- data/lib/dry/validation/rule.rb +11 -90
- data/lib/dry/validation/rule/composite.rb +50 -0
- data/lib/dry/validation/rule/each.rb +13 -0
- data/lib/dry/validation/rule/key.rb +17 -0
- data/lib/dry/validation/rule/set.rb +22 -0
- data/lib/dry/validation/rule/value.rb +13 -0
- data/lib/dry/validation/rule_compiler.rb +5 -0
- data/lib/dry/validation/schema.rb +6 -2
- data/lib/dry/validation/schema/definition.rb +4 -0
- data/lib/dry/validation/schema/form.rb +19 -0
- data/lib/dry/validation/schema/key.rb +16 -3
- data/lib/dry/validation/schema/result.rb +29 -0
- data/lib/dry/validation/schema/rule.rb +14 -16
- data/lib/dry/validation/schema/value.rb +14 -2
- data/lib/dry/validation/version.rb +1 -1
- data/spec/integration/custom_error_messages_spec.rb +1 -1
- data/spec/integration/optional_keys_spec.rb +30 -0
- data/spec/integration/schema_form_spec.rb +99 -0
- data/spec/integration/{validation_spec.rb → schema_spec.rb} +40 -13
- data/spec/shared/predicates.rb +1 -1
- data/spec/unit/error_compiler_spec.rb +64 -0
- data/spec/unit/input_type_compiler_spec.rb +205 -0
- data/spec/unit/predicates/bool_spec.rb +34 -0
- data/spec/unit/predicates/date_spec.rb +31 -0
- data/spec/unit/predicates/date_time_spec.rb +31 -0
- data/spec/unit/predicates/decimal_spec.rb +32 -0
- data/spec/unit/predicates/float_spec.rb +31 -0
- data/spec/unit/predicates/{nil_spec.rb → none_spec.rb} +2 -2
- data/spec/unit/predicates/time_spec.rb +31 -0
- data/spec/unit/rule/conjunction_spec.rb +28 -0
- data/spec/unit/rule/disjunction_spec.rb +36 -0
- data/spec/unit/rule/implication_spec.rb +14 -0
- data/spec/unit/rule/value_spec.rb +1 -1
- metadata +60 -6
@@ -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
|
-
|
37
|
+
Schema::Rule.new([:and, [key_rule, [:set, [name, val_rule.map(&:to_ary)]]]])
|
25
38
|
else
|
26
|
-
|
39
|
+
Schema::Rule.new([:and, [key_rule, val_rule.to_ary]])
|
27
40
|
end
|
28
41
|
else
|
29
|
-
|
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
|
-
|
5
|
-
|
6
|
-
attr_reader :node
|
4
|
+
class Rule
|
5
|
+
attr_reader :node
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
def initialize(node)
|
8
|
+
@node = node
|
9
|
+
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
11
|
+
def to_ary
|
12
|
+
node
|
13
|
+
end
|
14
|
+
alias_method :to_a, :to_ary
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
def &(other)
|
17
|
+
self.class.new([:and, [node, other.to_ary]])
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
-
|
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)
|
@@ -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
|
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.
|
15
|
-
|
16
|
-
|
14
|
+
address.hash? do
|
15
|
+
address.key(:city) do |city|
|
16
|
+
city.min_size?(3)
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
address.key(:street) do |street|
|
20
|
+
street.filled?
|
21
|
+
end
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
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
|
data/spec/shared/predicates.rb
CHANGED
@@ -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)
|