dry-validation 0.5.0 → 0.6.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.
@@ -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