dry-validation 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -0
  3. data/.travis.yml +3 -2
  4. data/CHANGELOG.md +42 -0
  5. data/Gemfile +8 -1
  6. data/README.md +13 -89
  7. data/config/errors.yml +35 -29
  8. data/dry-validation.gemspec +2 -2
  9. data/examples/basic.rb +3 -7
  10. data/examples/each.rb +3 -8
  11. data/examples/form.rb +3 -6
  12. data/examples/nested.rb +7 -15
  13. data/lib/dry/validation.rb +33 -5
  14. data/lib/dry/validation/error.rb +10 -26
  15. data/lib/dry/validation/error_compiler.rb +69 -99
  16. data/lib/dry/validation/error_compiler/input.rb +148 -0
  17. data/lib/dry/validation/hint_compiler.rb +83 -33
  18. data/lib/dry/validation/input_processor_compiler.rb +98 -0
  19. data/lib/dry/validation/input_processor_compiler/form.rb +46 -0
  20. data/lib/dry/validation/input_processor_compiler/sanitizer.rb +46 -0
  21. data/lib/dry/validation/messages/abstract.rb +30 -10
  22. data/lib/dry/validation/messages/i18n.rb +2 -1
  23. data/lib/dry/validation/messages/namespaced.rb +1 -0
  24. data/lib/dry/validation/messages/yaml.rb +8 -5
  25. data/lib/dry/validation/result.rb +33 -25
  26. data/lib/dry/validation/schema.rb +168 -61
  27. data/lib/dry/validation/schema/attr.rb +5 -27
  28. data/lib/dry/validation/schema/check.rb +24 -0
  29. data/lib/dry/validation/schema/dsl.rb +97 -0
  30. data/lib/dry/validation/schema/form.rb +2 -26
  31. data/lib/dry/validation/schema/key.rb +32 -28
  32. data/lib/dry/validation/schema/rule.rb +88 -32
  33. data/lib/dry/validation/schema/value.rb +77 -27
  34. data/lib/dry/validation/schema_compiler.rb +38 -0
  35. data/lib/dry/validation/version.rb +1 -1
  36. data/spec/fixtures/locales/pl.yml +1 -1
  37. data/spec/integration/attr_spec.rb +122 -0
  38. data/spec/integration/custom_error_messages_spec.rb +9 -11
  39. data/spec/integration/custom_predicates_spec.rb +68 -18
  40. data/spec/integration/error_compiler_spec.rb +259 -65
  41. data/spec/integration/hints_spec.rb +28 -9
  42. data/spec/integration/injecting_rules_spec.rb +11 -12
  43. data/spec/integration/localized_error_messages_spec.rb +16 -16
  44. data/spec/integration/messages/i18n_spec.rb +9 -5
  45. data/spec/integration/optional_keys_spec.rb +9 -11
  46. data/spec/integration/schema/array_schema_spec.rb +23 -0
  47. data/spec/integration/schema/check_rules_spec.rb +39 -31
  48. data/spec/integration/schema/check_with_nth_el_spec.rb +25 -0
  49. data/spec/integration/schema/each_with_set_spec.rb +23 -24
  50. data/spec/integration/schema/form_spec.rb +122 -0
  51. data/spec/integration/schema/inheriting_schema_spec.rb +31 -0
  52. data/spec/integration/schema/input_processor_spec.rb +46 -0
  53. data/spec/integration/schema/macros/confirmation_spec.rb +33 -0
  54. data/spec/integration/schema/macros/maybe_spec.rb +32 -0
  55. data/spec/integration/schema/macros/required_spec.rb +59 -0
  56. data/spec/integration/schema/macros/when_spec.rb +65 -0
  57. data/spec/integration/schema/nested_values_spec.rb +41 -0
  58. data/spec/integration/schema/not_spec.rb +14 -14
  59. data/spec/integration/schema/option_with_default_spec.rb +30 -0
  60. data/spec/integration/schema/reusing_schema_spec.rb +33 -0
  61. data/spec/integration/schema/using_types_spec.rb +29 -0
  62. data/spec/integration/schema/xor_spec.rb +17 -14
  63. data/spec/integration/schema_spec.rb +75 -245
  64. data/spec/shared/rule_compiler.rb +8 -0
  65. data/spec/spec_helper.rb +13 -0
  66. data/spec/unit/hint_compiler_spec.rb +10 -10
  67. data/spec/unit/{input_type_compiler_spec.rb → input_processor_compiler/form_spec.rb} +88 -73
  68. data/spec/unit/schema/key_spec.rb +33 -0
  69. data/spec/unit/schema/rule_spec.rb +7 -6
  70. data/spec/unit/schema/value_spec.rb +187 -54
  71. metadata +53 -31
  72. data/.rubocop.yml +0 -16
  73. data/.rubocop_todo.yml +0 -7
  74. data/lib/dry/validation/input_type_compiler.rb +0 -83
  75. data/lib/dry/validation/schema/definition.rb +0 -74
  76. data/lib/dry/validation/schema/result.rb +0 -68
  77. data/rakelib/rubocop.rake +0 -18
  78. data/spec/integration/rule_groups_spec.rb +0 -94
  79. data/spec/integration/schema/attrs_spec.rb +0 -38
  80. data/spec/integration/schema/default_key_behavior_spec.rb +0 -23
  81. data/spec/integration/schema/grouped_rules_spec.rb +0 -57
  82. data/spec/integration/schema/nested_spec.rb +0 -31
  83. data/spec/integration/schema_form_spec.rb +0 -97
@@ -0,0 +1,30 @@
1
+ RSpec.describe Dry::Validation::Schema, 'defining an option with default value' do
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ configure do
5
+ option :db, -> { DB }
6
+
7
+ def unique?(name, value)
8
+ DB.none? { |item| item[name] == value }
9
+ end
10
+ end
11
+
12
+ key(:email) { filled? & unique?(:email) }
13
+ end
14
+ end
15
+
16
+ before do
17
+ DB = [{ email: 'jane@doe' }]
18
+ end
19
+
20
+ after do
21
+ Object.send(:remove_const, :DB)
22
+ end
23
+
24
+ it 'uses external dependency set by option with a default value' do
25
+ expect(schema.db).to be(DB)
26
+
27
+ expect(schema.(email: 'jade@doe')).to be_success
28
+ expect(schema.(email: 'jane@doe')).to be_failure
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ RSpec.describe 'Reusing schemas' do
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ key(:city).required
5
+
6
+ key(:location).schema(LocationSchema)
7
+ end
8
+ end
9
+
10
+ before do
11
+ LocationSchema = Dry::Validation.Schema do
12
+ configure { config.input_processor = :form }
13
+
14
+ key(:lat).required(:float?)
15
+ key(:lng).required(:float?)
16
+ end
17
+ end
18
+
19
+ after do
20
+ Object.send(:remove_const, :LocationSchema)
21
+ end
22
+
23
+ it 're-uses existing schema' do
24
+ expect(schema.(city: 'NYC', location: { lat: 1.23, lng: 45.6 })).to be_success
25
+
26
+ expect(schema.(city: 'NYC', location: { lat: nil, lng: '45.6' }).messages).to eql(
27
+ location: {
28
+ lat: ['must be filled'],
29
+ lng: ['must be a float']
30
+ }
31
+ )
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ RSpec.describe Dry::Validation::Schema, 'defining schema using dry types' do
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ key(:email).required(Email)
5
+ key(:age).maybe(Age)
6
+ end
7
+ end
8
+
9
+ before do
10
+ Email = Dry::Types['strict.string']
11
+ Age = Dry::Types['strict.int'].constrained(gt: 18)
12
+ end
13
+
14
+ after do
15
+ Object.send(:remove_const, :Email)
16
+ Object.send(:remove_const, :Age)
17
+ end
18
+
19
+ it 'passes when input is valid' do
20
+ expect(schema.(email: 'jane@doe', age: 19)).to be_success
21
+ expect(schema.(email: 'jane@doe', age: nil)).to be_success
22
+ end
23
+
24
+ it 'fails when input is not valid' do
25
+ expect(schema.(email: '', age: 19)).to_not be_success
26
+ expect(schema.(email: 'jane@doe', age: 17)).to_not be_success
27
+ expect(schema.(email: 'jane@doe', age: '19')).to_not be_success
28
+ end
29
+ end
@@ -1,32 +1,35 @@
1
1
  RSpec.describe 'Schema with xor rules' do
2
- subject(:validate) { schema.new }
3
-
4
- let(:schema) do
5
- Class.new(Dry::Validation::Schema) do
6
- def self.messages
7
- Messages.default.merge(
8
- en: { errors: { be_reasonable: 'you cannot eat cake and have cake!' } }
9
- )
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ configure do
5
+ def self.messages
6
+ Messages.default.merge(
7
+ en: { errors: { be_reasonable: 'you cannot eat cake and have cake!' } }
8
+ )
9
+ end
10
10
  end
11
11
 
12
- key(:eat_cake) { |v| v.eql?('yes!') }
13
- key(:have_cake) { |v| v.eql?('yes!') }
12
+ key(:eat_cake).required
13
+
14
+ key(:have_cake).required
14
15
 
15
- rule(:be_reasonable) { rule(:eat_cake) ^ rule(:have_cake) }
16
+ rule(:be_reasonable) do
17
+ value(:eat_cake).eql?('yes!') ^ value(:have_cake).eql?('yes!')
18
+ end
16
19
  end
17
20
  end
18
21
 
19
22
  describe '#messages' do
20
23
  it 'passes when only one option is selected' do
21
- messages = validate.(eat_cake: 'yes!', have_cake: 'no!').messages[:be_reasonable]
24
+ messages = schema.(eat_cake: 'yes!', have_cake: 'no!').messages[:be_reasonable]
22
25
 
23
26
  expect(messages).to be(nil)
24
27
  end
25
28
 
26
29
  it 'fails when both options are selected' do
27
- messages = validate.(eat_cake: 'yes!', have_cake: 'yes!').messages[:be_reasonable]
30
+ messages = schema.(eat_cake: 'yes!', have_cake: 'yes!').messages[:be_reasonable]
28
31
 
29
- expect(messages).to eql([['you cannot eat cake and have cake!'], 'yes!'])
32
+ expect(messages).to eql(['you cannot eat cake and have cake!'])
30
33
  end
31
34
  end
32
35
  end
@@ -1,179 +1,55 @@
1
- RSpec.describe Dry::Validation::Schema do
2
- subject(:validation) { schema.new }
3
-
4
- describe 'defining key-based schema (hash-like)' do
5
- let(:schema) do
6
- Class.new(Dry::Validation::Schema) do
7
- key(:email) { |email| email.filled? }
8
-
9
- key(:age) do |age|
10
- age.none? | (age.int? & age.gt?(18))
11
- end
12
-
13
- key(:address) do |address|
14
- address.hash? do
15
- address.key(:city) do |city|
16
- city.min_size?(3)
17
- end
18
-
19
- address.key(:street) do |street|
20
- street.filled?
21
- end
22
-
23
- address.key(:country) do |country|
24
- country.key(:name, &:filled?)
25
- country.key(:code, &:filled?)
26
- end
27
- end
28
- end
29
-
30
- key(:phone_numbers) do |phone_numbers|
31
- phone_numbers.array? { phone_numbers.each(&:str?) }
32
- end
1
+ RSpec.describe Dry::Validation::Schema, 'defining key-based schema' do
2
+ describe 'with a flat structure' do
3
+ subject(:schema) do
4
+ Dry::Validation.Schema do
5
+ key(:email).required
6
+ key(:age) { none? | (int? & gt?(18)) }
33
7
  end
34
8
  end
35
9
 
36
- let(:input) do
37
- {
38
- email: 'jane@doe.org',
39
- age: 19,
40
- address: { city: 'NYC', street: 'Street 1/2', country: { code: 'US', name: 'USA' } },
41
- phone_numbers: [
42
- '123456', '234567'
43
- ]
44
- }.freeze
10
+ it 'passes when input is valid' do
11
+ expect(schema.(email: 'jane@doe', age: 19)).to be_success
12
+ expect(schema.(email: 'jane@doe', age: nil)).to be_success
45
13
  end
46
14
 
47
- describe '#messages' do
48
- it 'returns compiled error messages' do
49
- expect(validation.(input.merge(email: '')).messages).to match_array([
50
- [:email, [['email must be filled'], '']]
51
- ])
52
- end
15
+ it 'fails when input is not valid' do
16
+ expect(schema.(email: 'jane@doe', age: 17)).to_not be_success
53
17
  end
54
18
 
55
- describe '#call' do
56
- it 'passes when attributes are valid' do
57
- expect(validation.(input)).to be_empty
58
- end
59
-
60
- it 'validates presence of an email and min age value' do
61
- expect(validation.(input.merge(email: '', age: 18))).to match_array([
62
- [:error, [:input, [:age, 18, [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]],
63
- [:error, [:input, [:email, "", [[:val, [:email, [:predicate, [:filled?, []]]]]]]]]
64
- ])
65
- end
66
-
67
- it 'validates presence of the email key and type of age value' do
68
- expect(validation.(name: 'Jane', age: '18', address: input[:address], phone_numbers: input[:phone_numbers])).to match_array([
69
- [:error, [:input, [:age, "18", [[:val, [:age, [:predicate, [:int?, []]]]]]]]],
70
- [:error, [:input, [:email, nil, [[:key, [:email, [:predicate, [:key?, [:email]]]]]]]]]
71
- ])
72
- end
73
-
74
- it 'validates presence of the address and phone_number keys' do
75
- expect(validation.(email: 'jane@doe.org', age: 19)).to match_array([
76
- [:error, [:input, [:address, nil, [[:key, [:address, [:predicate, [:key?, [:address]]]]]]]]],
77
- [:error, [:input, [:phone_numbers, nil, [[:key, [:phone_numbers, [:predicate, [:key?, [:phone_numbers]]]]]]]]]
78
- ])
79
- end
80
-
81
- it 'validates presence of keys under address and min size of the city value' do
82
- expect(validation.(input.merge(address: { city: 'NY' }))).to match_array([
83
- [:error, [
84
- :input, [
85
- :address, {city: "NY"},
86
- [
87
- [:input, [:city, "NY", [[:val, [:city, [:predicate, [:min_size?, [3]]]]]]]],
88
- [:input, [:street, nil, [[:key, [:street, [:predicate, [:key?, [:street]]]]]]]],
89
- [:input, [:country, nil, [[:key, [:country, [:predicate, [:key?, [:country]]]]]]]]
90
- ]
91
- ]
92
- ]]
93
- ])
94
- end
19
+ it 'returns result which quacks like hash' do
20
+ input = { email: 'jane@doe', age: 19 }
21
+ result = schema.(input)
95
22
 
96
- it 'validates address type' do
97
- expect(validation.(input.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
-
102
- it 'validates address code and name values' do
103
- expect(validation.(input.merge(address: input[:address].merge(country: { code: 'US', name: '' })))).to match_array([
104
- [:error, [
105
- :input, [
106
- :address, {city: "NYC", street: "Street 1/2", country: {code: "US", name: ""}},
107
- [
108
- [
109
- :input, [
110
- :country, {code: "US", name: ""}, [
111
- [
112
- :input, [
113
- :name, "", [[:val, [:name, [:predicate, [:filled?, []]]]]]
114
- ]
115
- ]
116
- ]
117
- ]
118
- ]
119
- ]
120
- ]
121
- ]]
122
- ])
123
- end
23
+ expect(result[:email]).to eql('jane@doe')
24
+ expect(Hash[result]).to eql(input)
124
25
 
125
- it 'validates each phone number' do
126
- expect(validation.(input.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
26
+ expect(result.to_a).to eql([[:email, 'jane@doe'], [:age, 19]])
143
27
  end
144
28
  end
145
29
 
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? }
30
+ describe 'with nested structures' do
31
+ subject(:schema) do
32
+ Dry::Validation.Schema do
33
+ key(:email).required
150
34
 
151
- attr(:age) do |age|
152
- age.none? | (age.int? & age.gt?(18))
153
- end
35
+ key(:age).maybe(:int?, gt?: 18)
154
36
 
155
- attr(:address) do |address|
156
- address.attr(:city) do |city|
157
- city.min_size?(3)
158
- end
37
+ key(:address).schema do
38
+ key(:city).required(min_size?: 3)
159
39
 
160
- address.attr(:street) do |street|
161
- street.filled?
162
- end
40
+ key(:street).required
163
41
 
164
- address.attr(:country) do |country|
165
- country.attr(:name, &:filled?)
166
- country.attr(:code, &:filled?)
42
+ key(:country).schema do
43
+ key(:name).required
44
+ key(:code).required
167
45
  end
168
46
  end
169
47
 
170
- attr(:phone_numbers) do |phone_numbers|
171
- phone_numbers.array? { phone_numbers.each(&:str?) }
172
- end
48
+ key(:phone_numbers).each(:str?)
173
49
  end
174
50
  end
175
51
 
176
- let(:input_data) do
52
+ let(:input) do
177
53
  {
178
54
  email: 'jane@doe.org',
179
55
  age: 19,
@@ -184,126 +60,80 @@ RSpec.describe Dry::Validation::Schema do
184
60
  }.freeze
185
61
  end
186
62
 
187
- def input(data = input_data)
188
- struct_from_hash(data)
189
- end
190
-
191
63
  describe '#messages' do
192
64
  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
- ])
65
+ expect(schema.(input.merge(email: '')).messages).to eql(
66
+ email: ['must be filled']
67
+ )
196
68
  end
197
69
  end
198
70
 
199
71
  describe '#call' do
200
72
  it 'passes when attributes are valid' do
201
- expect(validation.(input)).to be_empty
73
+ expect(schema.(input)).to be_success
202
74
  end
203
75
 
204
76
  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
- ])
77
+ expect(schema.(input.merge(email: '', age: 18)).messages).to eql(
78
+ email: ['must be filled'], age: ['must be greater than 18']
79
+ )
209
80
  end
210
81
 
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'))
82
+ it 'validates presence of the email key and type of age value' do
83
+ attrs = {
84
+ name: 'Jane',
85
+ age: '18',
86
+ address: input[:address], phone_numbers: input[:phone_numbers]
87
+ }
213
88
 
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
- ])
89
+ expect(schema.(attrs).messages).to eql(
90
+ email: ['is missing'],
91
+ age: ['must be an integer', 'must be greater than 18']
92
+ )
218
93
  end
219
94
 
220
95
  it 'validates presence of the address and phone_number keys' do
221
- input_object = input(email: 'jane@doe.org', age: 19)
96
+ attrs = { email: 'jane@doe.org', age: 19 }
222
97
 
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
- ])
98
+ expect(schema.(attrs).messages).to eql(
99
+ address: ['is missing'], phone_numbers: ['is missing']
100
+ )
241
101
  end
242
102
 
243
103
  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)
104
+ attrs = input.merge(address: { city: 'NY' })
247
105
 
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
- ])
106
+ expect(schema.(attrs).messages).to eql(
107
+ address: {
108
+ street: ['is missing'],
109
+ country: ['is missing'],
110
+ city: ['size cannot be less than 3']
111
+ }
112
+ )
260
113
  end
261
114
 
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: '' })))
115
+ it 'validates address type' do
116
+ expect(schema.(input.merge(address: 'totally not a hash')).messages).to eql(
117
+ address: ['must be a hash']
118
+ )
119
+ end
264
120
 
265
- country_object = input_object.address.country.class.from_hash(code: "US", name: "")
121
+ it 'validates address code and name values' do
122
+ attrs = input.merge(
123
+ address: input[:address].merge(country: { code: 'US', name: '' })
124
+ )
266
125
 
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
- ])
126
+ expect(schema.(attrs).messages).to eql(
127
+ address: { country: { name: ['must be filled'] } }
128
+ )
287
129
  end
288
130
 
289
131
  it 'validates each phone number' do
290
- input_object = input(input_data.merge(phone_numbers: ['123', 312]))
132
+ attrs = input.merge(phone_numbers: ['123', 312])
291
133
 
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
- ])
134
+ expect(schema.(attrs).messages).to eql(
135
+ phone_numbers: { 1 => ['must be a string'] }
136
+ )
307
137
  end
308
138
  end
309
139
  end