grape 1.3.0 → 1.5.2

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +119 -1
  3. data/LICENSE +1 -1
  4. data/README.md +123 -29
  5. data/UPGRADING.md +265 -39
  6. data/lib/grape/api/instance.rb +32 -31
  7. data/lib/grape/api.rb +5 -5
  8. data/lib/grape/content_types.rb +34 -0
  9. data/lib/grape/dsl/callbacks.rb +1 -1
  10. data/lib/grape/dsl/helpers.rb +2 -1
  11. data/lib/grape/dsl/inside_route.rb +77 -43
  12. data/lib/grape/dsl/parameters.rb +12 -8
  13. data/lib/grape/dsl/routing.rb +12 -11
  14. data/lib/grape/dsl/validations.rb +18 -1
  15. data/lib/grape/eager_load.rb +1 -1
  16. data/lib/grape/endpoint.rb +8 -6
  17. data/lib/grape/exceptions/base.rb +0 -4
  18. data/lib/grape/exceptions/validation.rb +1 -1
  19. data/lib/grape/exceptions/validation_errors.rb +12 -13
  20. data/lib/grape/http/headers.rb +26 -0
  21. data/lib/grape/middleware/auth/base.rb +3 -3
  22. data/lib/grape/middleware/base.rb +4 -5
  23. data/lib/grape/middleware/error.rb +11 -13
  24. data/lib/grape/middleware/formatter.rb +3 -3
  25. data/lib/grape/middleware/stack.rb +10 -2
  26. data/lib/grape/middleware/versioner/header.rb +4 -4
  27. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  28. data/lib/grape/middleware/versioner/path.rb +1 -1
  29. data/lib/grape/namespace.rb +12 -2
  30. data/lib/grape/path.rb +13 -3
  31. data/lib/grape/request.rb +13 -8
  32. data/lib/grape/router/attribute_translator.rb +26 -5
  33. data/lib/grape/router/pattern.rb +17 -16
  34. data/lib/grape/router/route.rb +5 -24
  35. data/lib/grape/router.rb +26 -30
  36. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  37. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  38. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  39. data/lib/grape/util/base_inheritable.rb +15 -8
  40. data/lib/grape/util/cache.rb +20 -0
  41. data/lib/grape/util/lazy_object.rb +43 -0
  42. data/lib/grape/util/lazy_value.rb +1 -0
  43. data/lib/grape/util/reverse_stackable_values.rb +2 -0
  44. data/lib/grape/util/stackable_values.rb +7 -20
  45. data/lib/grape/validations/attributes_iterator.rb +8 -0
  46. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  47. data/lib/grape/validations/params_scope.rb +10 -8
  48. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  49. data/lib/grape/validations/types/array_coercer.rb +14 -5
  50. data/lib/grape/validations/types/build_coercer.rb +5 -8
  51. data/lib/grape/validations/types/custom_type_coercer.rb +16 -2
  52. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  53. data/lib/grape/validations/types/file.rb +15 -12
  54. data/lib/grape/validations/types/invalid_value.rb +24 -0
  55. data/lib/grape/validations/types/json.rb +40 -36
  56. data/lib/grape/validations/types/primitive_coercer.rb +15 -6
  57. data/lib/grape/validations/types/set_coercer.rb +6 -4
  58. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  59. data/lib/grape/validations/types.rb +7 -9
  60. data/lib/grape/validations/validator_factory.rb +1 -1
  61. data/lib/grape/validations/validators/as.rb +1 -1
  62. data/lib/grape/validations/validators/base.rb +8 -8
  63. data/lib/grape/validations/validators/coerce.rb +11 -15
  64. data/lib/grape/validations/validators/default.rb +3 -5
  65. data/lib/grape/validations/validators/exactly_one_of.rb +4 -2
  66. data/lib/grape/validations/validators/except_values.rb +1 -1
  67. data/lib/grape/validations/validators/multiple_params_base.rb +2 -1
  68. data/lib/grape/validations/validators/regexp.rb +1 -1
  69. data/lib/grape/validations/validators/values.rb +1 -1
  70. data/lib/grape/version.rb +1 -1
  71. data/lib/grape.rb +5 -5
  72. data/spec/grape/api/instance_spec.rb +50 -0
  73. data/spec/grape/api_remount_spec.rb +9 -4
  74. data/spec/grape/api_spec.rb +82 -6
  75. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  76. data/spec/grape/endpoint/declared_spec.rb +601 -0
  77. data/spec/grape/endpoint_spec.rb +0 -521
  78. data/spec/grape/entity_spec.rb +7 -1
  79. data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
  80. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  81. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  82. data/spec/grape/middleware/error_spec.rb +1 -1
  83. data/spec/grape/middleware/formatter_spec.rb +3 -3
  84. data/spec/grape/middleware/stack_spec.rb +10 -0
  85. data/spec/grape/path_spec.rb +4 -4
  86. data/spec/grape/request_spec.rb +1 -1
  87. data/spec/grape/validations/instance_behaivour_spec.rb +1 -1
  88. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  89. data/spec/grape/validations/params_scope_spec.rb +26 -0
  90. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  91. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  92. data/spec/grape/validations/types/primitive_coercer_spec.rb +135 -0
  93. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  94. data/spec/grape/validations/types_spec.rb +1 -1
  95. data/spec/grape/validations/validators/coerce_spec.rb +366 -86
  96. data/spec/grape/validations/validators/default_spec.rb +170 -0
  97. data/spec/grape/validations/validators/exactly_one_of_spec.rb +12 -12
  98. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  99. data/spec/grape/validations/validators/values_spec.rb +1 -1
  100. data/spec/grape/validations_spec.rb +298 -30
  101. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  102. data/spec/shared/versioning_examples.rb +20 -20
  103. data/spec/spec_helper.rb +3 -10
  104. data/spec/support/chunks.rb +14 -0
  105. data/spec/support/eager_load.rb +19 -0
  106. data/spec/support/versioned_helpers.rb +4 -6
  107. metadata +27 -10
  108. data/lib/grape/util/content_types.rb +0 -28
@@ -154,6 +154,49 @@ describe Grape::Validations::CoerceValidator do
154
154
  end
155
155
 
156
156
  context 'coerces' do
157
+ context 'json' do
158
+ let(:headers) { { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' } }
159
+
160
+ it 'BigDecimal' do
161
+ subject.params do
162
+ requires :bigdecimal, type: BigDecimal
163
+ end
164
+ subject.post '/bigdecimal' do
165
+ "#{params[:bigdecimal].class} #{params[:bigdecimal].to_f}"
166
+ end
167
+
168
+ post '/bigdecimal', { bigdecimal: 45.1 }.to_json, headers
169
+ expect(last_response.status).to eq(201)
170
+ expect(last_response.body).to eq('BigDecimal 45.1')
171
+ end
172
+
173
+ it 'Boolean' do
174
+ subject.params do
175
+ requires :boolean, type: Boolean
176
+ end
177
+ subject.post '/boolean' do
178
+ params[:boolean]
179
+ end
180
+
181
+ post '/boolean', { boolean: 'true' }.to_json, headers
182
+ expect(last_response.status).to eq(201)
183
+ expect(last_response.body).to eq('true')
184
+ end
185
+ end
186
+
187
+ it 'BigDecimal' do
188
+ subject.params do
189
+ requires :bigdecimal, coerce: BigDecimal
190
+ end
191
+ subject.get '/bigdecimal' do
192
+ params[:bigdecimal].class
193
+ end
194
+
195
+ get '/bigdecimal', bigdecimal: '45'
196
+ expect(last_response.status).to eq(200)
197
+ expect(last_response.body).to eq('BigDecimal')
198
+ end
199
+
157
200
  it 'Integer' do
158
201
  subject.params do
159
202
  requires :int, coerce: Integer
@@ -167,23 +210,68 @@ describe Grape::Validations::CoerceValidator do
167
210
  expect(last_response.body).to eq(integer_class_name)
168
211
  end
169
212
 
170
- it 'is a custom type' do
213
+ it 'String' do
171
214
  subject.params do
172
- requires :uri, coerce: SecureURIOnly
215
+ requires :string, coerce: String
173
216
  end
174
- subject.get '/secure_uri' do
175
- params[:uri].class
217
+ subject.get '/string' do
218
+ params[:string].class
176
219
  end
177
220
 
178
- get 'secure_uri', uri: 'https://www.example.com'
221
+ get '/string', string: 45
222
+ expect(last_response.status).to eq(200)
223
+ expect(last_response.body).to eq('String')
179
224
 
225
+ get '/string', string: nil
180
226
  expect(last_response.status).to eq(200)
181
- expect(last_response.body).to eq('URI::HTTPS')
227
+ expect(last_response.body).to eq('NilClass')
228
+ end
229
+
230
+ context 'a custom type' do
231
+ it 'coerces the given value' do
232
+ subject.params do
233
+ requires :uri, coerce: SecureURIOnly
234
+ end
235
+ subject.get '/secure_uri' do
236
+ params[:uri].class
237
+ end
182
238
 
183
- get 'secure_uri', uri: 'http://www.example.com'
239
+ get 'secure_uri', uri: 'https://www.example.com'
184
240
 
185
- expect(last_response.status).to eq(400)
186
- expect(last_response.body).to eq('uri is invalid')
241
+ expect(last_response.status).to eq(200)
242
+ expect(last_response.body).to eq('URI::HTTPS')
243
+
244
+ get 'secure_uri', uri: 'http://www.example.com'
245
+
246
+ expect(last_response.status).to eq(400)
247
+ expect(last_response.body).to eq('uri is invalid')
248
+ end
249
+
250
+ context 'returning the InvalidValue instance when invalid' do
251
+ let(:custom_type) do
252
+ Class.new do
253
+ def self.parse(_val)
254
+ Grape::Types::InvalidValue.new('must be unique')
255
+ end
256
+ end
257
+ end
258
+
259
+ it 'uses a custom message added to the invalid value' do
260
+ type = custom_type
261
+
262
+ subject.params do
263
+ requires :name, type: type
264
+ end
265
+ subject.get '/whatever' do
266
+ params[:name].class
267
+ end
268
+
269
+ get 'whatever', name: 'Bob'
270
+
271
+ expect(last_response.status).to eq(400)
272
+ expect(last_response.body).to eq('name must be unique')
273
+ end
274
+ end
187
275
  end
188
276
 
189
277
  context 'Array' do
@@ -281,119 +369,247 @@ describe Grape::Validations::CoerceValidator do
281
369
  end
282
370
  end
283
371
 
284
- it 'Bool' do
372
+ it 'Boolean' do
285
373
  subject.params do
286
- requires :bool, coerce: Grape::API::Boolean
374
+ requires :boolean, type: Boolean
287
375
  end
288
- subject.get '/bool' do
289
- params[:bool].class
376
+ subject.get '/boolean' do
377
+ params[:boolean].class
290
378
  end
291
379
 
292
- get '/bool', bool: 1
380
+ get '/boolean', boolean: 1
293
381
  expect(last_response.status).to eq(200)
294
382
  expect(last_response.body).to eq('TrueClass')
383
+ end
295
384
 
296
- get '/bool', bool: 0
297
- expect(last_response.status).to eq(200)
298
- expect(last_response.body).to eq('FalseClass')
385
+ context 'File' do
386
+ let(:file) { Rack::Test::UploadedFile.new(__FILE__) }
387
+ let(:filename) { File.basename(__FILE__).to_s }
299
388
 
300
- get '/bool', bool: 'false'
301
- expect(last_response.status).to eq(200)
302
- expect(last_response.body).to eq('FalseClass')
389
+ it 'Rack::Multipart::UploadedFile' do
390
+ subject.params do
391
+ requires :file, type: Rack::Multipart::UploadedFile
392
+ end
393
+ subject.post '/upload' do
394
+ params[:file][:filename]
395
+ end
303
396
 
304
- get '/bool', bool: 'true'
305
- expect(last_response.status).to eq(200)
306
- expect(last_response.body).to eq('TrueClass')
307
- end
397
+ post '/upload', file: file
398
+ expect(last_response.status).to eq(201)
399
+ expect(last_response.body).to eq(filename)
308
400
 
309
- it 'Boolean' do
310
- subject.params do
311
- optional :boolean, type: Boolean, default: true
312
- end
313
- subject.get '/boolean' do
314
- params[:boolean].class
401
+ post '/upload', file: 'not a file'
402
+ expect(last_response.status).to eq(400)
403
+ expect(last_response.body).to eq('file is invalid')
315
404
  end
316
405
 
317
- get '/boolean'
318
- expect(last_response.status).to eq(200)
319
- expect(last_response.body).to eq('TrueClass')
320
-
321
- get '/boolean', boolean: true
322
- expect(last_response.status).to eq(200)
323
- expect(last_response.body).to eq('TrueClass')
406
+ it 'File' do
407
+ subject.params do
408
+ requires :file, coerce: File
409
+ end
410
+ subject.post '/upload' do
411
+ params[:file][:filename]
412
+ end
324
413
 
325
- get '/boolean', boolean: false
326
- expect(last_response.status).to eq(200)
327
- expect(last_response.body).to eq('FalseClass')
414
+ post '/upload', file: file
415
+ expect(last_response.status).to eq(201)
416
+ expect(last_response.body).to eq(filename)
328
417
 
329
- get '/boolean', boolean: 'true'
330
- expect(last_response.status).to eq(200)
331
- expect(last_response.body).to eq('TrueClass')
418
+ post '/upload', file: 'not a file'
419
+ expect(last_response.status).to eq(400)
420
+ expect(last_response.body).to eq('file is invalid')
332
421
 
333
- get '/boolean', boolean: 'false'
334
- expect(last_response.status).to eq(200)
335
- expect(last_response.body).to eq('FalseClass')
422
+ post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' }
423
+ expect(last_response.status).to eq(400)
424
+ expect(last_response.body).to eq('file is invalid')
425
+ end
336
426
 
337
- get '/boolean', boolean: 123
338
- expect(last_response.status).to eq(400)
339
- expect(last_response.body).to eq('boolean is invalid')
427
+ it 'collection' do
428
+ subject.params do
429
+ requires :files, type: Array[File]
430
+ end
431
+ subject.post '/upload' do
432
+ params[:files].first[:filename]
433
+ end
340
434
 
341
- get '/boolean', boolean: '123'
342
- expect(last_response.status).to eq(400)
343
- expect(last_response.body).to eq('boolean is invalid')
435
+ post '/upload', files: [file]
436
+ expect(last_response.status).to eq(201)
437
+ expect(last_response.body).to eq(filename)
438
+ end
344
439
  end
345
440
 
346
- it 'Rack::Multipart::UploadedFile' do
441
+ it 'Nests integers' do
347
442
  subject.params do
348
- requires :file, type: Rack::Multipart::UploadedFile
443
+ requires :integers, type: Hash do
444
+ requires :int, coerce: Integer
445
+ end
349
446
  end
350
- subject.post '/upload' do
351
- params[:file][:filename]
447
+ subject.get '/int' do
448
+ params[:integers][:int].class
352
449
  end
353
450
 
354
- post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
355
- expect(last_response.status).to eq(201)
356
- expect(last_response.body).to eq(File.basename(__FILE__).to_s)
357
-
358
- post '/upload', file: 'not a file'
359
- expect(last_response.status).to eq(400)
360
- expect(last_response.body).to eq('file is invalid')
451
+ get '/int', integers: { int: '45' }
452
+ expect(last_response.status).to eq(200)
453
+ expect(last_response.body).to eq(integer_class_name)
361
454
  end
362
455
 
363
- it 'File' do
364
- subject.params do
365
- requires :file, coerce: File
456
+ context 'nil values' do
457
+ context 'primitive types' do
458
+ Grape::Validations::Types::PRIMITIVES.each do |type|
459
+ it 'respects the nil value' do
460
+ subject.params do
461
+ requires :param, type: type
462
+ end
463
+ subject.get '/nil_value' do
464
+ params[:param].class
465
+ end
466
+
467
+ get '/nil_value', param: nil
468
+ expect(last_response.status).to eq(200)
469
+ expect(last_response.body).to eq('NilClass')
470
+ end
471
+ end
366
472
  end
367
- subject.post '/upload' do
368
- params[:file][:filename]
473
+
474
+ context 'structures types' do
475
+ Grape::Validations::Types::STRUCTURES.each do |type|
476
+ it 'respects the nil value' do
477
+ subject.params do
478
+ requires :param, type: type
479
+ end
480
+ subject.get '/nil_value' do
481
+ params[:param].class
482
+ end
483
+
484
+ get '/nil_value', param: nil
485
+ expect(last_response.status).to eq(200)
486
+ expect(last_response.body).to eq('NilClass')
487
+ end
488
+ end
369
489
  end
370
490
 
371
- post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
372
- expect(last_response.status).to eq(201)
373
- expect(last_response.body).to eq(File.basename(__FILE__).to_s)
491
+ context 'special types' do
492
+ Grape::Validations::Types::SPECIAL.each_key do |type|
493
+ it 'respects the nil value' do
494
+ subject.params do
495
+ requires :param, type: type
496
+ end
497
+ subject.get '/nil_value' do
498
+ params[:param].class
499
+ end
374
500
 
375
- post '/upload', file: 'not a file'
376
- expect(last_response.status).to eq(400)
377
- expect(last_response.body).to eq('file is invalid')
501
+ get '/nil_value', param: nil
502
+ expect(last_response.status).to eq(200)
503
+ expect(last_response.body).to eq('NilClass')
504
+ end
505
+ end
378
506
 
379
- post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' }
380
- expect(last_response.status).to eq(400)
381
- expect(last_response.body).to eq('file is invalid')
507
+ context 'variant-member-type collections' do
508
+ [
509
+ Array[Integer, String],
510
+ [Integer, String, Array[Integer, String]]
511
+ ].each do |type|
512
+ it 'respects the nil value' do
513
+ subject.params do
514
+ requires :param, type: type
515
+ end
516
+ subject.get '/nil_value' do
517
+ params[:param].class
518
+ end
519
+
520
+ get '/nil_value', param: nil
521
+ expect(last_response.status).to eq(200)
522
+ expect(last_response.body).to eq('NilClass')
523
+ end
524
+ end
525
+ end
526
+ end
382
527
  end
383
528
 
384
- it 'Nests integers' do
385
- subject.params do
386
- requires :integers, type: Hash do
387
- requires :int, coerce: Integer
529
+ context 'empty string' do
530
+ context 'primitive types' do
531
+ (Grape::Validations::Types::PRIMITIVES - [String]).each do |type|
532
+ it "is coerced to nil for type #{type}" do
533
+ subject.params do
534
+ requires :param, type: type
535
+ end
536
+ subject.get '/empty_string' do
537
+ params[:param].class
538
+ end
539
+
540
+ get '/empty_string', param: ''
541
+ expect(last_response.status).to eq(200)
542
+ expect(last_response.body).to eq('NilClass')
543
+ end
544
+ end
545
+
546
+ it 'is not coerced to nil for type String' do
547
+ subject.params do
548
+ requires :param, type: String
549
+ end
550
+ subject.get '/empty_string' do
551
+ params[:param].class
552
+ end
553
+
554
+ get '/empty_string', param: ''
555
+ expect(last_response.status).to eq(200)
556
+ expect(last_response.body).to eq('String')
388
557
  end
389
558
  end
390
- subject.get '/int' do
391
- params[:integers][:int].class
559
+
560
+ context 'structures types' do
561
+ (Grape::Validations::Types::STRUCTURES - [Hash]).each do |type|
562
+ it "is coerced to nil for type #{type}" do
563
+ subject.params do
564
+ requires :param, type: type
565
+ end
566
+ subject.get '/empty_string' do
567
+ params[:param].class
568
+ end
569
+
570
+ get '/empty_string', param: ''
571
+ expect(last_response.status).to eq(200)
572
+ expect(last_response.body).to eq('NilClass')
573
+ end
574
+ end
392
575
  end
393
576
 
394
- get '/int', integers: { int: '45' }
395
- expect(last_response.status).to eq(200)
396
- expect(last_response.body).to eq(integer_class_name)
577
+ context 'special types' do
578
+ (Grape::Validations::Types::SPECIAL.keys - [File, Rack::Multipart::UploadedFile]).each do |type|
579
+ it "is coerced to nil for type #{type}" do
580
+ subject.params do
581
+ requires :param, type: type
582
+ end
583
+ subject.get '/empty_string' do
584
+ params[:param].class
585
+ end
586
+
587
+ get '/empty_string', param: ''
588
+ expect(last_response.status).to eq(200)
589
+ expect(last_response.body).to eq('NilClass')
590
+ end
591
+ end
592
+
593
+ context 'variant-member-type collections' do
594
+ [
595
+ Array[Integer, String],
596
+ [Integer, String, Array[Integer, String]]
597
+ ].each do |type|
598
+ it "is coerced to nil for type #{type}" do
599
+ subject.params do
600
+ requires :param, type: type
601
+ end
602
+ subject.get '/empty_string' do
603
+ params[:param].class
604
+ end
605
+
606
+ get '/empty_string', param: ''
607
+ expect(last_response.status).to eq(200)
608
+ expect(last_response.body).to eq('NilClass')
609
+ end
610
+ end
611
+ end
612
+ end
397
613
  end
398
614
  end
399
615
 
@@ -432,6 +648,30 @@ describe Grape::Validations::CoerceValidator do
432
648
  expect(JSON.parse(last_response.body)).to eq(%w[a b c d])
433
649
  end
434
650
 
651
+ it 'parses parameters with Array[Array[String]] type and coerce_with' do
652
+ subject.params do
653
+ requires :values, type: Array[Array[String]], coerce_with: ->(val) { val.is_a?(String) ? [val.split(/,/).map(&:strip)] : val }
654
+ end
655
+ subject.post '/coerce_nested_strings' do
656
+ params[:values]
657
+ end
658
+
659
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: 'a,b,c,d'), 'CONTENT_TYPE' => 'application/json'
660
+ expect(last_response.status).to eq(201)
661
+ expect(JSON.parse(last_response.body)).to eq([%w[a b c d]])
662
+
663
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [%w[a c], %w[b]]), 'CONTENT_TYPE' => 'application/json'
664
+ expect(last_response.status).to eq(201)
665
+ expect(JSON.parse(last_response.body)).to eq([%w[a c], %w[b]])
666
+
667
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [[]]), 'CONTENT_TYPE' => 'application/json'
668
+ expect(last_response.status).to eq(201)
669
+ expect(JSON.parse(last_response.body)).to eq([[]])
670
+
671
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [['a', { bar: 0 }], ['b']]), 'CONTENT_TYPE' => 'application/json'
672
+ expect(last_response.status).to eq(400)
673
+ end
674
+
435
675
  it 'parses parameters with Array[Integer] type' do
436
676
  subject.params do
437
677
  requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) }
@@ -514,6 +754,46 @@ describe Grape::Validations::CoerceValidator do
514
754
  expect(last_response.body).to eq('3')
515
755
  end
516
756
 
757
+ context 'Integer type and coerce_with potentially returning nil' do
758
+ before do
759
+ subject.params do
760
+ requires :int, type: Integer, coerce_with: (lambda do |val|
761
+ if val == '0'
762
+ nil
763
+ elsif val.match?(/^-?\d+$/)
764
+ val.to_i
765
+ else
766
+ val
767
+ end
768
+ end)
769
+ end
770
+ subject.get '/' do
771
+ params[:int].class.to_s
772
+ end
773
+ end
774
+
775
+ it 'accepts value that coerces to nil' do
776
+ get '/', int: '0'
777
+
778
+ expect(last_response.status).to eq(200)
779
+ expect(last_response.body).to eq('NilClass')
780
+ end
781
+
782
+ it 'coerces to Integer' do
783
+ get '/', int: '1'
784
+
785
+ expect(last_response.status).to eq(200)
786
+ expect(last_response.body).to eq('Integer')
787
+ end
788
+
789
+ it 'returns invalid value if coercion returns a wrong type' do
790
+ get '/', int: 'lol'
791
+
792
+ expect(last_response.status).to eq(400)
793
+ expect(last_response.body).to eq('int is invalid')
794
+ end
795
+ end
796
+
517
797
  it 'must be supplied with :type or :coerce' do
518
798
  expect do
519
799
  subject.params do