compel 0.2.0 → 0.3.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -20
  3. data/lib/compel/builder/array.rb +31 -0
  4. data/lib/compel/builder/boolean.rb +7 -0
  5. data/lib/compel/builder/common.rb +2 -2
  6. data/lib/compel/builder/common_value.rb +4 -4
  7. data/lib/compel/builder/hash.rb +3 -1
  8. data/lib/compel/builder/methods.rb +5 -0
  9. data/lib/compel/builder/schema.rb +4 -0
  10. data/lib/compel/coercion/coercion.rb +47 -0
  11. data/lib/compel/coercion/nil_result.rb +13 -0
  12. data/lib/compel/coercion/result.rb +37 -0
  13. data/lib/compel/coercion/types/array.rb +15 -0
  14. data/lib/compel/coercion/{boolean.rb → types/boolean.rb} +1 -3
  15. data/lib/compel/coercion/types/date.rb +36 -0
  16. data/lib/compel/coercion/types/datetime.rb +36 -0
  17. data/lib/compel/coercion/{float.rb → types/float.rb} +2 -2
  18. data/lib/compel/coercion/{hash.rb → types/hash.rb} +2 -2
  19. data/lib/compel/coercion/{integer.rb → types/integer.rb} +2 -2
  20. data/lib/compel/coercion/{json.rb → types/json.rb} +2 -2
  21. data/lib/compel/coercion/{regexp.rb → types/regexp.rb} +2 -4
  22. data/lib/compel/coercion/{string.rb → types/string.rb} +3 -5
  23. data/lib/compel/coercion/types/time.rb +36 -0
  24. data/lib/compel/coercion/types/type.rb +29 -0
  25. data/lib/compel/contract.rb +14 -42
  26. data/lib/compel/errors.rb +13 -17
  27. data/lib/compel/exceptions/invalid_object_error.rb +9 -0
  28. data/lib/compel/result.rb +30 -0
  29. data/lib/compel/validation.rb +6 -4
  30. data/lib/compel/validators/array_validator.rb +107 -0
  31. data/lib/compel/validators/base.rb +11 -1
  32. data/lib/compel/validators/hash_validator.rb +77 -19
  33. data/lib/compel/validators/type_validator.rb +42 -11
  34. data/lib/compel/version.rb +1 -1
  35. data/lib/compel.rb +6 -15
  36. data/spec/compel/builder_spec.rb +354 -144
  37. data/spec/compel/coercion_spec.rb +16 -1
  38. data/spec/compel/compel_spec.rb +315 -302
  39. data/spec/compel/validation_spec.rb +97 -115
  40. data/spec/support/sinatra_app.rb +2 -2
  41. metadata +21 -15
  42. data/lib/compel/coercion/date.rb +0 -30
  43. data/lib/compel/coercion/datetime.rb +0 -30
  44. data/lib/compel/coercion/time.rb +0 -30
  45. data/lib/compel/coercion/type.rb +0 -17
  46. data/lib/compel/coercion.rb +0 -31
  47. data/lib/compel/exceptions/invalid_hash_error.rb +0 -9
@@ -2,219 +2,429 @@ describe Compel::Builder do
2
2
 
3
3
  context 'Schema' do
4
4
 
5
- subject(:builder) { Compel::Builder::String.new }
6
-
7
- it 'shoudl' do
8
- object = {
9
- first_name: 'Joaquim',
10
- birth_date: '1989-0',
11
- address: {
12
- line_one: 'Lisboa',
13
- post_code: '1100',
14
- country_code: 'PT'
15
- }
16
- }
17
-
18
- schema = Compel.hash.keys({
19
- first_name: Compel.string.required,
20
- last_name: Compel.string.required,
21
- birth_date: Compel.date.iso8601,
22
- address: Compel.hash.keys({
23
- line_one: Compel.string.required,
24
- line_two: Compel.string.default('-'),
25
- post_code: Compel.string.format(/^\d{4}-\d{3}$/).required,
26
- country_code: Compel.string.in(['PT', 'GB']).default('PT')
27
- })
28
- })
29
-
30
- result = Compel.run(object, schema)
31
-
32
- expect(result).to \
33
- eq({
34
- "first_name" => "Joaquim",
35
- "birth_date" => "1989-0",
36
- "address" => {
37
- "line_one" => "Lisboa",
38
- "post_code" => "1100",
39
- "country_code" => "PT",
40
- "line_two" => "-"
41
- },
42
- "errors" => {
43
- "last_name" => ["is required"],
44
- "birth_date" => ["'1989-0' is not a parsable date with format: %Y-%m-%d"],
45
- "address" => {
46
- "post_code" => ["must match format ^\\d{4}-\\d{3}$"]
47
- }
48
- }
49
- })
50
- end
5
+ context 'Build' do
51
6
 
52
- it 'should build new Schema for givin type' do
53
- expect(builder.type).to be(Compel::Coercion::String)
54
- expect(builder.options.keys).to eq([])
55
- expect(builder.required?).to be false
56
- expect(builder.default_value).to be nil
57
- end
7
+ it 'should build new Schema for givin type' do
8
+ builder = Compel.string
58
9
 
59
- it 'should build Schema' do
60
- schema = Compel.hash.keys({
61
- a: Compel.float,
62
- b: Compel.string,
63
- c: Compel.hash.keys({
64
- cc: Compel.integer.length(1)
65
- }),
66
- d: Compel.json,
67
- e: Compel.time,
68
- f: Compel.datetime,
69
- g: Compel.date,
70
- h: Compel.integer
71
- })
72
-
73
- keys_schemas = schema.options[:keys]
74
-
75
- expect(keys_schemas.a.type).to be Compel::Coercion::Float
76
- expect(keys_schemas.b.type).to be Compel::Coercion::String
77
- expect(keys_schemas.c.type).to be Compel::Coercion::Hash
78
- expect(keys_schemas.d.type).to be Compel::Coercion::JSON
79
- expect(keys_schemas.e.type).to be Compel::Coercion::Time
80
- expect(keys_schemas.f.type).to be Compel::Coercion::DateTime
81
- expect(keys_schemas.g.type).to be Compel::Coercion::Date
82
- expect(keys_schemas.h.type).to be Compel::Coercion::Integer
83
- end
10
+ expect(builder.type).to be(Compel::Coercion::String)
11
+ expect(builder.options.keys).to eq([])
12
+ expect(builder.required?).to be false
13
+ expect(builder.default_value).to be nil
14
+ end
84
15
 
85
- it 'should validate hash object from schema' do
86
- schema = Compel.hash.keys({
87
- a: Compel.float.required
88
- })
16
+ it 'should build Schema' do
17
+ schema = Compel.hash.keys({
18
+ a: Compel.float,
19
+ b: Compel.string,
20
+ c: Compel.hash.keys({
21
+ cc: Compel.integer.length(1)
22
+ }),
23
+ d: Compel.json,
24
+ e: Compel.time,
25
+ f: Compel.datetime,
26
+ g: Compel.date,
27
+ h: Compel.integer
28
+ })
89
29
 
90
- expect(schema.validate({ a: nil }).errors.a).to \
91
- include('is required')
92
- end
30
+ keys_schemas = schema.options[:keys]
31
+
32
+ expect(keys_schemas.a.type).to be Compel::Coercion::Float
33
+ expect(keys_schemas.b.type).to be Compel::Coercion::String
34
+ expect(keys_schemas.c.type).to be Compel::Coercion::Hash
35
+ expect(keys_schemas.d.type).to be Compel::Coercion::JSON
36
+ expect(keys_schemas.e.type).to be Compel::Coercion::Time
37
+ expect(keys_schemas.f.type).to be Compel::Coercion::DateTime
38
+ expect(keys_schemas.g.type).to be Compel::Coercion::Date
39
+ expect(keys_schemas.h.type).to be Compel::Coercion::Integer
40
+ end
41
+
42
+ context 'Builder::CommonValue' do
93
43
 
94
- context 'Builder::CommonValue' do
44
+ context '#in, #range, #min, #max' do
95
45
 
96
- context '#in, #range, #min, #max' do
46
+ subject(:builder) { Compel.string }
97
47
 
98
- it 'should have value' do
99
- [:in, :range, :min, :max].each do |method|
100
- builder.send(method, "#{method}")
101
- expect(builder.options[method]).to eq("#{method}")
48
+ it 'should have value' do
49
+ [:in, :range, :min, :max].each do |method|
50
+ builder.send(method, "#{method}")
51
+ expect(builder.options[method]).to eq("#{method}")
52
+ end
102
53
  end
54
+
103
55
  end
104
56
 
105
57
  end
106
58
 
107
- end
59
+ context 'Builder::Common' do
60
+
61
+ subject(:builder) { Compel.string }
108
62
 
109
- context 'Builder::Common' do
63
+ context '#is, #default' do
110
64
 
111
- context '#is, #default' do
65
+ it 'should have value' do
112
66
 
113
- it 'should have value' do
114
- [:is, :default].each do |method|
115
- builder.send(method, "#{method}")
116
- expect(builder.options[method]).to eq("#{method}")
67
+ [:is, :default].each do |method|
68
+ builder.send(method, "#{method}")
69
+ expect(builder.options[method]).to eq("#{method}")
70
+ end
117
71
  end
72
+
118
73
  end
119
74
 
120
- end
75
+ context '#required' do
76
+
77
+ it 'should be false' do
78
+ expect(builder.required?).to be false
79
+ end
80
+
81
+ it 'should be true' do
82
+ builder.required
121
83
 
122
- context '#required' do
84
+ expect(builder.required?).to be true
85
+ end
123
86
 
124
- it 'should be false' do
125
- expect(builder.required?).to be false
126
87
  end
127
88
 
128
- it 'should be true' do
129
- builder.required
89
+ context '#length' do
90
+
91
+ it 'should have value' do
92
+ builder.length(5)
93
+
94
+ expect(builder.options[:length]).to be 5
95
+ end
96
+
97
+ it 'should raise exception for invalid type' do
98
+ expect { builder.length('abc') }.to \
99
+ raise_error Compel::TypeError, "'abc' is not a valid Integer"
100
+ end
130
101
 
131
- expect(builder.required?).to be true
132
102
  end
133
103
 
134
104
  end
135
105
 
136
- context '#length' do
106
+ context 'Date' do
137
107
 
138
- it 'should have value' do
139
- builder.length(5)
108
+ subject(:builder) { Compel.date }
140
109
 
141
- expect(builder.options[:length]).to be 5
142
- end
110
+ context '#format' do
111
+
112
+ it 'should have value' do
113
+ builder.format('%d-%m-%Y')
114
+
115
+ expect(builder.options[:format]).to eq('%d-%m-%Y')
116
+ end
117
+
118
+ it 'should have value' do
119
+ builder.iso8601
120
+
121
+ expect(builder.options[:format]).to eq('%Y-%m-%d')
122
+ end
143
123
 
144
- it 'should raise exception for invalid type' do
145
- expect { builder.length('abc') }.to \
146
- raise_error Compel::TypeError, "'abc' is not a valid Integer"
147
124
  end
148
125
 
149
126
  end
150
127
 
151
- end
128
+ context 'String' do
152
129
 
153
- context 'Date' do
130
+ subject(:builder) { Compel.string }
154
131
 
155
- subject(:builder) { Compel::Builder::Date.new }
132
+ context '#format' do
156
133
 
157
- context '#format' do
134
+ it 'should raise exception for invalid type' do
135
+ expect { builder.format('abc') }.to \
136
+ raise_error Compel::TypeError, "'abc' is not a valid Regexp"
137
+ end
138
+
139
+ it 'should have value' do
140
+ builder.format(/1/)
158
141
 
159
- it 'should have value' do
160
- builder.format('%d-%m-%Y')
142
+ expect(builder.options[:format]).to eq(/1/)
143
+ end
161
144
 
162
- expect(builder.options[:format]).to eq('%d-%m-%Y')
163
145
  end
164
146
 
165
- it 'should have value' do
166
- builder.iso8601
147
+ context '#min_length' do
148
+
149
+ it 'should raise exception for invalid type' do
150
+ expect { builder.min_length('a') }.to \
151
+ raise_error Compel::TypeError, "'a' is not a valid Integer"
152
+ end
153
+
154
+ it 'should have value' do
155
+ builder.min_length(4)
156
+
157
+ expect(builder.options[:min_length]).to eq(4)
158
+ end
159
+
160
+ end
161
+
162
+ context '#max_length' do
163
+
164
+ it 'should raise exception for invalid type' do
165
+ expect { builder.max_length('a') }.to \
166
+ raise_error Compel::TypeError, "'a' is not a valid Integer"
167
+ end
168
+
169
+ it 'should have value' do
170
+ builder.max_length(10)
171
+
172
+ expect(builder.options[:max_length]).to eq(10)
173
+ end
167
174
 
168
- expect(builder.options[:format]).to eq('%Y-%m-%d')
169
175
  end
170
176
 
171
177
  end
172
178
 
173
179
  end
174
180
 
175
- context 'String' do
181
+ context 'Validate' do
182
+
183
+ context 'Hash' do
176
184
 
177
- context '#format' do
185
+ it 'should validate Hash schema' do
186
+ object = {
187
+ first_name: 'Joaquim',
188
+ birth_date: '1989-0',
189
+ address: {
190
+ line_one: 'Lisboa',
191
+ post_code: '1100',
192
+ country_code: 'PT'
193
+ }
194
+ }
178
195
 
179
- it 'should raise exception for invalid type' do
180
- expect { builder.format('abc') }.to \
181
- raise_error Compel::TypeError, "'abc' is not a valid Regexp"
196
+ schema = Compel.hash.keys({
197
+ first_name: Compel.string.required,
198
+ last_name: Compel.string.required,
199
+ birth_date: Compel.date.iso8601,
200
+ address: Compel.hash.keys({
201
+ line_one: Compel.string.required,
202
+ line_two: Compel.string.default('-'),
203
+ post_code: Compel.string.format(/^\d{4}-\d{3}$/).required,
204
+ country_code: Compel.string.in(['PT', 'GB']).default('PT')
205
+ })
206
+ })
207
+
208
+ result = schema.validate(object)
209
+
210
+ expect(result.value).to \
211
+ eq({
212
+ "first_name" => "Joaquim",
213
+ "birth_date" => "1989-0",
214
+ "address" => {
215
+ "line_one" => "Lisboa",
216
+ "post_code" => "1100",
217
+ "country_code" => "PT",
218
+ "line_two" => "-"
219
+ },
220
+ "errors" => {
221
+ "last_name" => ["is required"],
222
+ "birth_date" => ["'1989-0' is not a parsable date with format: %Y-%m-%d"],
223
+ "address" => {
224
+ "post_code" => ["must match format ^\\d{4}-\\d{3}$"]
225
+ }
226
+ }
227
+ })
182
228
  end
183
229
 
184
- it 'should have value' do
185
- builder.format(/1/)
230
+ it 'should validate hash object from schema' do
231
+ schema = Compel.hash.keys({
232
+ a: Compel.float.required
233
+ })
186
234
 
187
- expect(builder.options[:format]).to eq(/1/)
235
+ expect(schema.validate({ a: nil }).errors.a).to \
236
+ include('is required')
188
237
  end
189
238
 
190
239
  end
191
240
 
192
- context '#min_length' do
241
+ context 'String' do
193
242
 
194
- it 'should raise exception for invalid type' do
195
- expect { builder.min_length('a') }.to \
196
- raise_error Compel::TypeError, "'a' is not a valid Integer"
243
+ it 'should validate Type schema' do
244
+ schema = Compel.string.format(/^\d{4}-\d{3}$/).required
245
+ response = schema.validate('1234')
246
+
247
+ expect(response.errors).to \
248
+ include("must match format ^\\d{4}-\\d{3}$")
197
249
  end
198
250
 
199
- it 'should have value' do
200
- builder.min_length(4)
251
+ end
252
+
253
+ context 'Array' do
254
+
255
+ subject(:builder) { Compel.array }
256
+
257
+ it 'should validate nil without errors' do
258
+ result = builder.validate(nil)
259
+
260
+ expect(result.valid?).to be true
261
+ end
262
+
263
+ it 'should validate nil with errors' do
264
+ result = builder.required.validate(nil)
265
+
266
+ expect(result.errors[:base]).to include('is required')
267
+ end
268
+
269
+ it 'should validate with errors for invalid array' do
270
+ result = builder.required.validate(1)
271
+
272
+ expect(result.errors[:base]).to include("'1' is not a valid Array")
273
+ end
274
+
275
+ context '#items' do
276
+
277
+ it 'should validate without items' do
278
+ result = builder.validate([1, 2, 3])
279
+
280
+ expect(result.valid?).to be true
281
+ expect(result.value).to eq([1, 2, 3])
282
+ end
283
+
284
+ it 'should raise exception for invalid type' do
285
+ expect { builder.items('a') }.to \
286
+ raise_error Compel::TypeError, "#items must be a valid Schema"
287
+ end
288
+
289
+ it 'should raise exception for invalid type' do
290
+ expect { builder.items('a') }.to \
291
+ raise_error Compel::TypeError, "#items must be a valid Schema"
292
+ end
293
+
294
+ it 'should have value' do
295
+ builder.items(Compel.integer)
296
+
297
+ expect(builder.options[:items].class).to be(Compel::Builder::Integer)
298
+ end
299
+
300
+ it 'should validate all items' do
301
+ builder.items(Compel.integer)
302
+
303
+ result = builder.validate([1, '2', nil])
304
+
305
+ expect(result.valid?).to be true
306
+ expect(result.value).to eq([1, 2])
307
+ end
308
+
309
+ it 'should validate all items with errors' do
310
+ builder.items(Compel.float.required)
311
+
312
+ result = builder.validate([1, 'a', nil])
313
+
314
+ expect(result.valid?).to be false
315
+ expect(result.errors['1']).to include("'a' is not a valid Float")
316
+ expect(result.errors['2']).to include('is required')
317
+ end
318
+
319
+ it 'should coerce all hash items' do
320
+ builder.items(Compel.hash.keys({
321
+ a: Compel.string.required,
322
+ b: Compel.integer
323
+ }))
324
+
325
+ result = builder.validate([
326
+ { a: 'A', b: '1' },
327
+ { a: 'B' },
328
+ { a: 'C', b: 3 },
329
+ ])
330
+
331
+ expect(result.valid?).to be true
332
+ expect(result.value).to eq \
333
+ [
334
+ Hashie::Mash.new({ a: 'A', b: 1 }),
335
+ Hashie::Mash.new({ a: 'B' }),
336
+ Hashie::Mash.new({ a: 'C', b: 3 })
337
+ ]
338
+ end
339
+
340
+ it 'should coerce all hash items with errors' do
341
+ builder.items(Compel.hash.keys({
342
+ a: Compel.string.required,
343
+ b: Compel.string.format(/^abc$/).required
344
+ }))
345
+
346
+ result = builder.validate([
347
+ { a: 'A', b: 'abc' },
348
+ { a: 'B' },
349
+ { a: 'C', b: 'abcd' },
350
+ ])
351
+
352
+ expect(result.valid?).to be false
353
+ expect(result.errors['1'][:b]).to include('is required')
354
+ expect(result.errors['2'][:b]).to include('must match format ^abc$')
355
+
356
+ expect(result.value[0][:a]).to eq('A')
357
+ expect(result.value[0][:b]).to eq('abc')
358
+
359
+ expect(result.value[1][:a]).to eq('B')
360
+ expect(result.value[1][:b]).to be_nil
361
+ expect(result.value[1][:errors][:b]).to include('is required')
362
+
363
+ expect(result.value[2][:a]).to eq('C')
364
+ expect(result.value[2][:b]).to eq('abcd')
365
+ expect(result.value[2][:errors][:b]).to \
366
+ include('must match format ^abc$')
367
+ end
368
+
369
+ it 'should coerce array with hash items and nested array keys with errors' do
370
+ builder.items(Compel.hash.keys({
371
+ a: Compel.string,
372
+ b: Compel.array.items(Compel.integer.required).required
373
+ }))
374
+
375
+ result = builder.validate([
376
+ { a: 'C' },
377
+ { b: [1, 2, 3] },
378
+ { b: ['1', nil, 'a'] }
379
+ ])
380
+
381
+ expect(result.valid?).to be false
382
+
383
+ expect(result.value[0][:a]).to eq('C')
384
+ expect(result.value[0][:b]).to be_nil
385
+ expect(result.value[1][:a]).to be_nil
386
+ expect(result.value[1][:b]).to eq([1, 2, 3])
387
+ expect(result.value[2][:a]).to be_nil
388
+ expect(result.value[2][:b]).to eq([1, nil, 'a'])
389
+
390
+ expect(result.errors['0'][:b]).to include('is required')
391
+ expect(result.errors['2'][:b]['1']).to include('is required')
392
+ expect(result.errors['2'][:b]['2']).to \
393
+ include("'a' is not a valid Integer")
394
+ end
201
395
 
202
- expect(builder.options[:min_length]).to eq(4)
203
396
  end
204
397
 
205
398
  end
206
399
 
207
- context '#max_length' do
400
+ context 'Hash' do
401
+
402
+ subject(:builder) { Compel.hash }
208
403
 
209
- it 'should raise exception for invalid type' do
210
- expect { builder.max_length('a') }.to \
211
- raise_error Compel::TypeError, "'a' is not a valid Integer"
404
+ it 'should validate empty keys option' do
405
+ schema = builder.required
406
+
407
+ expect(schema.validate({ a: 1 }).valid?).to be true
408
+ end
409
+
410
+ it 'should validate nil' do
411
+ schema = builder.required
412
+
413
+ result = schema.validate(nil)
414
+
415
+ expect(result.valid?).to be false
416
+ expect(result.errors[:base]).to \
417
+ include('is required')
212
418
  end
213
419
 
214
- it 'should have value' do
215
- builder.max_length(10)
420
+ it 'should validate empty keys with errors' do
421
+ schema = builder.required.length(2)
422
+
423
+ result = schema.validate({ a: 1 })
216
424
 
217
- expect(builder.options[:max_length]).to eq(10)
425
+ expect(result.valid?).to be false
426
+ expect(result.errors[:base]).to \
427
+ include('cannot have length different than 2')
218
428
  end
219
429
 
220
430
  end
@@ -73,7 +73,7 @@ describe Compel::Coercion do
73
73
  end
74
74
 
75
75
  it 'should coerce with format' do
76
- value = Compel::Coercion.coerce!('22-12-2015', Date, { format: '%d-%m-%Y'})
76
+ value = Compel::Coercion.coerce!('22-12-2015', Date, { format: '%d-%m-%Y' })
77
77
 
78
78
  expect(value).to eq(Date.strptime('22-12-2015', '%d-%m-%Y'))
79
79
  end
@@ -248,6 +248,21 @@ describe Compel::Coercion do
248
248
 
249
249
  end
250
250
 
251
+ context 'Array' do
252
+
253
+ it 'should not coerce' do
254
+ expect { Compel::Coercion.coerce!(123, Compel::Coercion::Array) }.to \
255
+ raise_error Compel::TypeError, "'123' is not a valid Array"
256
+ end
257
+
258
+ it 'should coerce' do
259
+ value = Compel::Coercion.coerce!([1, 2], Compel::Coercion::Array)
260
+
261
+ expect(value).to eq([1, 2])
262
+ end
263
+
264
+ end
265
+
251
266
  end
252
267
 
253
268
  end