dry-validation 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module Validation
3
- VERSION = '0.5.0'.freeze
3
+ VERSION = '0.6.0'.freeze
4
4
  end
5
5
  end
@@ -9,6 +9,7 @@ RSpec.describe Dry::Validation::ErrorCompiler do
9
9
  en: {
10
10
  errors: {
11
11
  key?: '+%{name}+ key is missing in the hash',
12
+ attr?: 'Object does not respond to the +%{name}+ attr',
12
13
  rules: {
13
14
  address: {
14
15
  filled?: 'Please provide your address'
@@ -23,6 +24,7 @@ RSpec.describe Dry::Validation::ErrorCompiler do
23
24
  let(:ast) do
24
25
  [
25
26
  [:error, [:input, [:name, nil, [[:key, [:name, [:predicate, [:key?, []]]]]]]]],
27
+ [:error, [:input, [:phone, nil, [[:attr, [:phone, [:predicate, [:attr?, []]]]]]]]],
26
28
  [:error, [:input, [:age, 18, [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]],
27
29
  [:error, [:input, [:email, "", [[:val, [:email, [:predicate, [:filled?, []]]]]]]]],
28
30
  [:error, [:input, [:address, "", [[:val, [:address, [:predicate, [:filled?, []]]]]]]]]
@@ -32,6 +34,7 @@ RSpec.describe Dry::Validation::ErrorCompiler do
32
34
  it 'converts error ast into another format' do
33
35
  expect(error_compiler.(ast)).to eql(
34
36
  name: [["+name+ key is missing in the hash"], nil],
37
+ phone: [["Object does not respond to the +phone+ attr"], nil],
35
38
  age: [["age must be greater than 18"], 18],
36
39
  email: [["email must be filled"], ''],
37
40
  address: [["Please provide your address"], '']
@@ -0,0 +1,23 @@
1
+ RSpec.describe 'Schema / Injecting Rules' do
2
+ let(:validate) { schema.new(other.rules) }
3
+
4
+ let(:other) do
5
+ Class.new(Dry::Validation::Schema) do
6
+ key(:login) { |value| value.bool? }
7
+ end
8
+ end
9
+
10
+ let(:schema) do
11
+ Class.new(Dry::Validation::Schema) do
12
+ key(:email) { |email| email.none? | email.filled? }
13
+
14
+ rule(email: :filled?) { value(:login).true? > value(:email).filled? }
15
+ end
16
+ end
17
+
18
+ it 'appends rules from another schema' do
19
+ expect(validate.(login: true, email: 'jane@doe')).to be_empty
20
+ expect(validate.(login: false, email: nil)).to be_empty
21
+ expect(validate.(login: true, email: nil)).to_not be_empty
22
+ end
23
+ end
@@ -1,6 +1,14 @@
1
1
  RSpec.describe Dry::Validation::Schema do
2
2
  subject(:validation) { schema.new }
3
3
 
4
+ before do
5
+ def schema.messages
6
+ Messages.default.merge(
7
+ en: { errors: { password_confirmation: 'does not match' } }
8
+ )
9
+ end
10
+ end
11
+
4
12
  describe 'defining schema with rule groups' do
5
13
  let(:schema) do
6
14
  Class.new(Dry::Validation::Schema) do
@@ -24,6 +32,12 @@ RSpec.describe Dry::Validation::Schema do
24
32
  ])
25
33
  end
26
34
 
35
+ it 'returns messages for a failed group rule' do
36
+ expect(validation.(password: 'foo', password_confirmation: 'bar').messages).to eql(
37
+ password_confirmation: [['does not match'], ['foo', 'bar']]
38
+ )
39
+ end
40
+
27
41
  it 'returns errors for the dependent predicates, not the group rule, when any of the dependent predicates fail' do
28
42
  expect(validation.(password: '', password_confirmation: '')).to match_array([
29
43
  [:error, [:input, [:password, "", [[:val, [:password, [:predicate, [:filled?, []]]]]]]]],
@@ -31,5 +45,50 @@ RSpec.describe Dry::Validation::Schema do
31
45
  ])
32
46
  end
33
47
  end
48
+
49
+ describe 'confirmation' do
50
+ shared_examples_for 'confirmation behavior' do
51
+ it 'applies custom rules' do
52
+ expect(validation.(password: 'abcd').messages).to include(
53
+ password: [['password size cannot be less than 6'], 'abcd']
54
+ )
55
+ end
56
+
57
+ it 'applies confirmation equality predicate' do
58
+ expect(validation.(password: 'abcdef', password_confirmation: 'abcd').messages).to include(
59
+ password_confirmation: [['does not match'], ['abcdef', 'abcd']]
60
+ )
61
+ end
62
+
63
+ it 'skips default predicate' do
64
+ expect(validation.(password: '', password_confirmation: '').messages).to include(
65
+ password: [['password size cannot be less than 6'], ''],
66
+ password_confirmation: [['password_confirmation must be filled'], '']
67
+ )
68
+ end
69
+ end
70
+
71
+ describe 'custom predicates' do
72
+ let(:schema) do
73
+ Class.new(Dry::Validation::Schema) do
74
+ key(:password) { |value| value.min_size?(6) }
75
+
76
+ confirmation(:password)
77
+ end
78
+ end
79
+
80
+ it_behaves_like 'confirmation behavior'
81
+ end
82
+
83
+ describe 'custom predicates using shortcut options' do
84
+ let(:schema) do
85
+ Class.new(Dry::Validation::Schema) do
86
+ confirmation(:password, min_size: 6)
87
+ end
88
+ end
89
+
90
+ it_behaves_like 'confirmation behavior'
91
+ end
92
+ end
34
93
  end
35
94
  end
@@ -0,0 +1,38 @@
1
+ require 'ostruct'
2
+
3
+ RSpec.describe Schema, 'defining schema with attrs' do
4
+ subject(:validation) { schema.new }
5
+
6
+ let(:schema) do
7
+ Class.new(Dry::Validation::Schema) do
8
+ attr(:email) { |email| email.filled? }
9
+
10
+ attr(:address) do |address|
11
+ address.attr(:city, &:filled?)
12
+ address.attr(:street, &:filled?)
13
+ end
14
+ end
15
+ end
16
+
17
+ describe '#call' do
18
+ context 'when valid input' do
19
+ let(:input) do
20
+ struct_from_hash(email: "email@test.com", address: { city: 'NYC', street: 'Street 1/2' })
21
+ end
22
+
23
+ it 'should be valid' do
24
+ expect(validation.(input)).to be_empty
25
+ end
26
+ end
27
+
28
+ context 'when input does not have proper attributes' do
29
+ let(:input) do
30
+ struct_from_hash(name: "John", address: { country: 'US', street: 'Street 1/2' })
31
+ end
32
+
33
+ it 'should not be valid' do
34
+ expect(validation.(input)).to_not be_empty
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,28 +1,74 @@
1
- RSpec.describe Schema, 'using generic rules' do
1
+ RSpec.describe Schema, 'using high-level rules' do
2
2
  subject(:validate) { schema.new }
3
3
 
4
- let(:schema) do
5
- Class.new(Schema) do
6
- def self.messages
7
- Messages.default.merge(
8
- en: { errors: { destiny: 'you must select either red or blue' } }
9
- )
4
+ context 'composing rules' do
5
+ let(:schema) do
6
+ Class.new(Schema) do
7
+ def self.messages
8
+ Messages.default.merge(
9
+ en: { errors: { destiny: 'you must select either red or blue' } }
10
+ )
11
+ end
12
+
13
+ optional(:red, &:filled?)
14
+ optional(:blue, &:filled?)
15
+
16
+ rule(:destiny) { rule(:red) | rule(:blue) }
10
17
  end
18
+ end
11
19
 
12
- optional(:red, &:filled?)
13
- optional(:blue, &:filled?)
20
+ it 'passes when only red is filled' do
21
+ expect(validate.(red: '1')).to be_empty
22
+ end
14
23
 
15
- rule(:destiny) { rule(:red) | rule(:blue) }
24
+ it 'fails when red and blue are not filled ' do
25
+ expect(validate.(red: '', blue: '').messages[:destiny]).to eql(
26
+ [['you must select either red or blue'], '']
27
+ )
16
28
  end
17
29
  end
18
30
 
19
- it 'passes when only red is filled' do
20
- expect(validate.(red: '1')).to be_empty
21
- end
31
+ context 'composing specific predicates' do
32
+ let(:schema) do
33
+ Class.new(Schema) do
34
+ def self.messages
35
+ Messages.default.merge(
36
+ en: {
37
+ errors: {
38
+ email_presence: 'email must be present when login is set to true',
39
+ email_absence: 'email must not be present when login is set to false'
40
+ }
41
+ }
42
+ )
43
+ end
22
44
 
23
- it 'fails when red and blue are not filled ' do
24
- expect(validate.(red: '', blue: '').messages[:destiny]).to eql(
25
- [['you must select either red or blue'], '']
26
- )
45
+ key(:login) { |login| login.bool? }
46
+ key(:email) { |email| email.none? | email.filled? }
47
+
48
+ rule(:email_presence) { value(:login).true?.then(value(:email).filled?) }
49
+
50
+ rule(:email_absence) { value(:login).false?.then(value(:email).none?) }
51
+ end
52
+ end
53
+
54
+ it 'passes when login is false and email is nil' do
55
+ expect(validate.(login: false, email: nil)).to be_empty
56
+ end
57
+
58
+ it 'fails when login is false and email is present' do
59
+ expect(validate.(login: false, email: 'jane@doe').messages[:email_absence]).to eql(
60
+ [['email must not be present when login is set to false'], nil]
61
+ )
62
+ end
63
+
64
+ it 'passes when login is true and email is present' do
65
+ expect(validate.(login: true, email: 'jane@doe')).to be_empty
66
+ end
67
+
68
+ it 'fails when login is true and email is not present' do
69
+ expect(validate.(login: true, email: nil).messages[:email_presence]).to eql(
70
+ [['email must be present when login is set to true'], nil]
71
+ )
72
+ end
27
73
  end
28
74
  end
@@ -0,0 +1,23 @@
1
+ RSpec.describe 'Schema::Form / Default key behavior' do
2
+ subject(:validate) { schema.new }
3
+
4
+ let(:schema) do
5
+ Class.new(Dry::Validation::Schema::Form) do
6
+ key(:name)
7
+ key(:age, &:int?)
8
+ optional(:address)
9
+ end
10
+ end
11
+
12
+ it 'applies filled? predicate by default' do
13
+ expect(validate.('name' => 'jane', 'age' => '21').params).to eql(
14
+ name: 'jane', age: 21
15
+ )
16
+ end
17
+
18
+ it 'applies filled? predicate by default to optional key' do
19
+ expect(validate.('name' => 'jane', 'age' => '21', 'address' => 'Earth').params).to eql(
20
+ name: 'jane', age: 21, address: 'Earth'
21
+ )
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ RSpec.describe Schema, 'using high-level grouped rules' do
2
+ subject(:validate) { schema.new }
3
+
4
+ let(:schema) do
5
+ Class.new(Schema) do
6
+ def self.messages
7
+ Messages.default.merge(
8
+ en: {
9
+ errors: {
10
+ email: {
11
+ absence: 'email must not be selected',
12
+ presence: 'email must be selected',
13
+ format: 'this is not an email lol',
14
+ inclusion: 'sorry, did not expect this lol'
15
+ }
16
+ }
17
+ }
18
+ )
19
+ end
20
+
21
+ key(:email) { |email| email.none? | email.filled? }
22
+ key(:login) { |login| login.bool? }
23
+
24
+ rule(email: :absence) do
25
+ value(:login).false? > value(:email).none?
26
+ end
27
+
28
+ rule(email: :presence) do
29
+ value(:login).true? > value(:email).filled?
30
+ end
31
+
32
+ rule(email: :format) do
33
+ value(:email).filled? > value(:email).format?(/[a-z]@[a-z]/)
34
+ end
35
+
36
+ rule(email: :inclusion) do
37
+ value(:email).filled? > value(:email).inclusion?(%w[jane@doe])
38
+ end
39
+ end
40
+ end
41
+
42
+ it 'passes when login is true and email is present' do
43
+ expect(validate.(login: true, email: 'jane@doe').messages).to be_empty
44
+ end
45
+
46
+ it 'fails when login is false and email is not present' do
47
+ expect(validate.(login: true, email: nil).messages).to_not be_empty
48
+ end
49
+
50
+ it 'provides merged error messages' do
51
+ expect(validate.(login: true, email: 'not-an-email-lol').messages).to eql(
52
+ email: [
53
+ ["sorry, did not expect this lol", "this is not an email lol"], nil
54
+ ]
55
+ )
56
+ end
57
+ end
@@ -1,7 +1,7 @@
1
1
  RSpec.describe Dry::Validation::Schema do
2
2
  subject(:validation) { schema.new }
3
3
 
4
- describe 'defining schema' do
4
+ describe 'defining key-based schema (hash-like)' do
5
5
  let(:schema) do
6
6
  Class.new(Dry::Validation::Schema) do
7
7
  key(:email) { |email| email.filled? }
@@ -33,7 +33,7 @@ RSpec.describe Dry::Validation::Schema do
33
33
  end
34
34
  end
35
35
 
36
- let(:attrs) do
36
+ let(:input) do
37
37
  {
38
38
  email: 'jane@doe.org',
39
39
  age: 19,
@@ -46,7 +46,7 @@ RSpec.describe Dry::Validation::Schema do
46
46
 
47
47
  describe '#messages' do
48
48
  it 'returns compiled error messages' do
49
- expect(validation.(attrs.merge(email: '')).messages).to match_array([
49
+ expect(validation.(input.merge(email: '')).messages).to match_array([
50
50
  [:email, [['email must be filled'], '']]
51
51
  ])
52
52
  end
@@ -54,18 +54,18 @@ RSpec.describe Dry::Validation::Schema do
54
54
 
55
55
  describe '#call' do
56
56
  it 'passes when attributes are valid' do
57
- expect(validation.(attrs)).to be_empty
57
+ expect(validation.(input)).to be_empty
58
58
  end
59
59
 
60
60
  it 'validates presence of an email and min age value' do
61
- expect(validation.(attrs.merge(email: '', age: 18))).to match_array([
61
+ expect(validation.(input.merge(email: '', age: 18))).to match_array([
62
62
  [:error, [:input, [:age, 18, [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]],
63
63
  [:error, [:input, [:email, "", [[:val, [:email, [:predicate, [:filled?, []]]]]]]]]
64
64
  ])
65
65
  end
66
66
 
67
67
  it 'validates presence of the email key and type of age value' do
68
- expect(validation.(name: 'Jane', age: '18', address: attrs[:address], phone_numbers: attrs[:phone_numbers])).to match_array([
68
+ expect(validation.(name: 'Jane', age: '18', address: input[:address], phone_numbers: input[:phone_numbers])).to match_array([
69
69
  [:error, [:input, [:age, "18", [[:val, [:age, [:predicate, [:int?, []]]]]]]]],
70
70
  [:error, [:input, [:email, nil, [[:key, [:email, [:predicate, [:key?, [:email]]]]]]]]]
71
71
  ])
@@ -79,7 +79,7 @@ RSpec.describe Dry::Validation::Schema do
79
79
  end
80
80
 
81
81
  it 'validates presence of keys under address and min size of the city value' do
82
- expect(validation.(attrs.merge(address: { city: 'NY' }))).to match_array([
82
+ expect(validation.(input.merge(address: { city: 'NY' }))).to match_array([
83
83
  [:error, [
84
84
  :input, [
85
85
  :address, {city: "NY"},
@@ -94,13 +94,13 @@ RSpec.describe Dry::Validation::Schema do
94
94
  end
95
95
 
96
96
  it 'validates address type' do
97
- expect(validation.(attrs.merge(address: 'totally not a hash'))).to match_array([
97
+ expect(validation.(input.merge(address: 'totally not a hash'))).to match_array([
98
98
  [:error, [:input, [:address, "totally not a hash", [[:val, [:address, [:predicate, [:hash?, []]]]]]]]]
99
99
  ])
100
100
  end
101
101
 
102
102
  it 'validates address code and name values' do
103
- expect(validation.(attrs.merge(address: attrs[:address].merge(country: { code: 'US', name: '' })))).to match_array([
103
+ expect(validation.(input.merge(address: input[:address].merge(country: { code: 'US', name: '' })))).to match_array([
104
104
  [:error, [
105
105
  :input, [
106
106
  :address, {city: "NYC", street: "Street 1/2", country: {code: "US", name: ""}},
@@ -123,7 +123,7 @@ RSpec.describe Dry::Validation::Schema do
123
123
  end
124
124
 
125
125
  it 'validates each phone number' do
126
- expect(validation.(attrs.merge(phone_numbers: ['123', 312]))).to match_array([
126
+ expect(validation.(input.merge(phone_numbers: ['123', 312]))).to match_array([
127
127
  [:error, [
128
128
  :input, [
129
129
  :phone_numbers, ["123", 312],
@@ -142,4 +142,169 @@ RSpec.describe Dry::Validation::Schema do
142
142
  end
143
143
  end
144
144
  end
145
+
146
+ describe 'defining attr-based schema (model-like)' do
147
+ let(:schema) do
148
+ Class.new(Dry::Validation::Schema) do
149
+ attr(:email) { |email| email.filled? }
150
+
151
+ attr(:age) do |age|
152
+ age.none? | (age.int? & age.gt?(18))
153
+ end
154
+
155
+ attr(:address) do |address|
156
+ address.attr(:city) do |city|
157
+ city.min_size?(3)
158
+ end
159
+
160
+ address.attr(:street) do |street|
161
+ street.filled?
162
+ end
163
+
164
+ address.attr(:country) do |country|
165
+ country.attr(:name, &:filled?)
166
+ country.attr(:code, &:filled?)
167
+ end
168
+ end
169
+
170
+ attr(:phone_numbers) do |phone_numbers|
171
+ phone_numbers.array? { phone_numbers.each(&:str?) }
172
+ end
173
+ end
174
+ end
175
+
176
+ let(:input_data) do
177
+ {
178
+ email: 'jane@doe.org',
179
+ age: 19,
180
+ address: { city: 'NYC', street: 'Street 1/2', country: { code: 'US', name: 'USA' } },
181
+ phone_numbers: [
182
+ '123456', '234567'
183
+ ]
184
+ }.freeze
185
+ end
186
+
187
+ def input(data = input_data)
188
+ struct_from_hash(data)
189
+ end
190
+
191
+ describe '#messages' do
192
+ it 'returns compiled error messages' do
193
+ expect(validation.(input(input_data.merge(email: ''))).messages).to match_array([
194
+ [:email, [["email must be filled"], '']]
195
+ ])
196
+ end
197
+ end
198
+
199
+ describe '#call' do
200
+ it 'passes when attributes are valid' do
201
+ expect(validation.(input)).to be_empty
202
+ end
203
+
204
+ it 'validates presence of an email and min age value' do
205
+ expect(validation.(input(input_data.merge(email: '', age: 18)))).to match_array([
206
+ [:error, [:input, [:age, 18, [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]],
207
+ [:error, [:input, [:email, "", [[:val, [:email, [:predicate, [:filled?, []]]]]]]]]
208
+ ])
209
+ end
210
+
211
+ it 'validates presence of the email attr and type of age value' do
212
+ input_object = input(input_data.reject { |k, v| k == :email }.merge(age: '18'))
213
+
214
+ expect(validation.(input_object)).to match_array([
215
+ [:error, [:input, [:age, "18", [[:val, [:age, [:predicate, [:int?, []]]]]]]]],
216
+ [:error, [:input, [:email, input_object, [[:attr, [:email, [:predicate, [:attr?, [:email]]]]]]]]]
217
+ ])
218
+ end
219
+
220
+ it 'validates presence of the address and phone_number keys' do
221
+ input_object = input(email: 'jane@doe.org', age: 19)
222
+
223
+ expect(validation.(input_object)).to match_array([
224
+ [:error, [
225
+ :input, [
226
+ :address, input_object,
227
+ [
228
+ [:attr, [:address, [:predicate, [:attr?, [:address]]]]]
229
+ ]
230
+ ]
231
+ ]],
232
+ [:error, [
233
+ :input, [
234
+ :phone_numbers, input_object,
235
+ [
236
+ [:attr, [:phone_numbers, [:predicate, [:attr?, [:phone_numbers]]]]]
237
+ ]
238
+ ]
239
+ ]]
240
+ ])
241
+ end
242
+
243
+ it 'validates presence of keys under address and min size of the city value' do
244
+ address = { city: 'NY' }
245
+ input_object = input(input_data.merge(address: address))
246
+ address_object = input_object.address.class.from_hash(address)
247
+
248
+ expect(validation.(input_object)).to match_array([
249
+ [:error, [
250
+ :input, [
251
+ :address, address_object,
252
+ [
253
+ [:input, [:city, "NY", [[:val, [:city, [:predicate, [:min_size?, [3]]]]]]]],
254
+ [:input, [:street, address_object, [[:attr, [:street, [:predicate, [:attr?, [:street]]]]]]]],
255
+ [:input, [:country, address_object, [[:attr, [:country, [:predicate, [:attr?, [:country]]]]]]]]
256
+ ]
257
+ ]
258
+ ]]
259
+ ])
260
+ end
261
+
262
+ it 'validates address code and name values' do
263
+ input_object = input(input_data.merge(address: input_data[:address].merge(country: { code: 'US', name: '' })))
264
+
265
+ country_object = input_object.address.country.class.from_hash(code: "US", name: "")
266
+
267
+ expect(validation.(input_object)).to match_array([
268
+ [:error, [
269
+ :input, [
270
+ :address, input_object.address.class.from_hash(city: "NYC", street: "Street 1/2", country: country_object),
271
+ [
272
+ [
273
+ :input, [
274
+ :country, country_object, [
275
+ [
276
+ :input, [
277
+ :name, "", [[:val, [:name, [:predicate, [:filled?, []]]]]]
278
+ ]
279
+ ]
280
+ ]
281
+ ]
282
+ ]
283
+ ]
284
+ ]
285
+ ]]
286
+ ])
287
+ end
288
+
289
+ it 'validates each phone number' do
290
+ input_object = input(input_data.merge(phone_numbers: ['123', 312]))
291
+
292
+ expect(validation.(input_object)).to match_array([
293
+ [:error, [
294
+ :input, [
295
+ :phone_numbers, ["123", 312],[
296
+ [
297
+ :input, [
298
+ :phone_numbers, 312, [
299
+ [:val, [:phone_numbers, [:predicate, [:str?, []]]]]
300
+ ]
301
+ ]
302
+ ]
303
+ ]
304
+ ]
305
+ ]]
306
+ ])
307
+ end
308
+ end
309
+ end
145
310
  end