grape 1.3.0 → 1.5.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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/LICENSE +1 -1
  4. data/README.md +104 -21
  5. data/UPGRADING.md +243 -39
  6. data/lib/grape.rb +4 -5
  7. data/lib/grape/api.rb +4 -4
  8. data/lib/grape/api/instance.rb +32 -31
  9. data/lib/grape/content_types.rb +34 -0
  10. data/lib/grape/dsl/helpers.rb +2 -1
  11. data/lib/grape/dsl/inside_route.rb +76 -42
  12. data/lib/grape/dsl/parameters.rb +4 -4
  13. data/lib/grape/dsl/routing.rb +8 -8
  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_errors.rb +11 -12
  19. data/lib/grape/http/headers.rb +26 -0
  20. data/lib/grape/middleware/base.rb +3 -4
  21. data/lib/grape/middleware/error.rb +10 -12
  22. data/lib/grape/middleware/formatter.rb +3 -3
  23. data/lib/grape/middleware/stack.rb +19 -5
  24. data/lib/grape/middleware/versioner/header.rb +4 -4
  25. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  26. data/lib/grape/middleware/versioner/path.rb +1 -1
  27. data/lib/grape/namespace.rb +12 -2
  28. data/lib/grape/path.rb +13 -3
  29. data/lib/grape/request.rb +13 -8
  30. data/lib/grape/router.rb +26 -30
  31. data/lib/grape/router/attribute_translator.rb +25 -4
  32. data/lib/grape/router/pattern.rb +17 -16
  33. data/lib/grape/router/route.rb +5 -24
  34. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  35. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  36. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  37. data/lib/grape/util/base_inheritable.rb +15 -8
  38. data/lib/grape/util/cache.rb +20 -0
  39. data/lib/grape/util/lazy_object.rb +43 -0
  40. data/lib/grape/util/lazy_value.rb +1 -0
  41. data/lib/grape/util/reverse_stackable_values.rb +2 -0
  42. data/lib/grape/util/stackable_values.rb +7 -20
  43. data/lib/grape/validations/params_scope.rb +6 -5
  44. data/lib/grape/validations/types.rb +6 -5
  45. data/lib/grape/validations/types/array_coercer.rb +14 -5
  46. data/lib/grape/validations/types/build_coercer.rb +5 -8
  47. data/lib/grape/validations/types/custom_type_coercer.rb +14 -2
  48. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  49. data/lib/grape/validations/types/file.rb +15 -12
  50. data/lib/grape/validations/types/json.rb +40 -36
  51. data/lib/grape/validations/types/primitive_coercer.rb +15 -6
  52. data/lib/grape/validations/types/set_coercer.rb +6 -4
  53. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  54. data/lib/grape/validations/validators/as.rb +1 -1
  55. data/lib/grape/validations/validators/base.rb +2 -4
  56. data/lib/grape/validations/validators/coerce.rb +4 -11
  57. data/lib/grape/validations/validators/default.rb +3 -5
  58. data/lib/grape/validations/validators/exactly_one_of.rb +4 -2
  59. data/lib/grape/validations/validators/except_values.rb +1 -1
  60. data/lib/grape/validations/validators/regexp.rb +1 -1
  61. data/lib/grape/validations/validators/values.rb +1 -1
  62. data/lib/grape/version.rb +1 -1
  63. data/spec/grape/api/instance_spec.rb +50 -0
  64. data/spec/grape/api_spec.rb +82 -6
  65. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  66. data/spec/grape/endpoint/declared_spec.rb +590 -0
  67. data/spec/grape/endpoint_spec.rb +0 -521
  68. data/spec/grape/entity_spec.rb +6 -0
  69. data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
  70. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  71. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  72. data/spec/grape/middleware/error_spec.rb +1 -1
  73. data/spec/grape/middleware/formatter_spec.rb +3 -3
  74. data/spec/grape/middleware/stack_spec.rb +12 -1
  75. data/spec/grape/path_spec.rb +4 -4
  76. data/spec/grape/validations/instance_behaivour_spec.rb +1 -1
  77. data/spec/grape/validations/params_scope_spec.rb +26 -0
  78. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  79. data/spec/grape/validations/types/primitive_coercer_spec.rb +135 -0
  80. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  81. data/spec/grape/validations/types_spec.rb +1 -1
  82. data/spec/grape/validations/validators/coerce_spec.rb +329 -77
  83. data/spec/grape/validations/validators/default_spec.rb +170 -0
  84. data/spec/grape/validations/validators/exactly_one_of_spec.rb +12 -12
  85. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  86. data/spec/grape/validations/validators/values_spec.rb +1 -1
  87. data/spec/grape/validations_spec.rb +30 -30
  88. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  89. data/spec/spec_helper.rb +3 -10
  90. data/spec/support/chunks.rb +14 -0
  91. data/spec/support/eager_load.rb +19 -0
  92. data/spec/support/versioned_helpers.rb +3 -5
  93. metadata +121 -105
  94. data/lib/grape/util/content_types.rb +0 -28
@@ -17,7 +17,7 @@ describe Grape::Validations::Types do
17
17
  [
18
18
  Integer, Float, Numeric, BigDecimal,
19
19
  Grape::API::Boolean, String, Symbol,
20
- Date, DateTime, Time, Rack::Multipart::UploadedFile
20
+ Date, DateTime, Time
21
21
  ].each do |type|
22
22
  it "recognizes #{type} as a primitive" do
23
23
  expect(described_class.primitive?(type)).to be_truthy
@@ -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,6 +210,23 @@ describe Grape::Validations::CoerceValidator do
167
210
  expect(last_response.body).to eq(integer_class_name)
168
211
  end
169
212
 
213
+ it 'String' do
214
+ subject.params do
215
+ requires :string, coerce: String
216
+ end
217
+ subject.get '/string' do
218
+ params[:string].class
219
+ end
220
+
221
+ get '/string', string: 45
222
+ expect(last_response.status).to eq(200)
223
+ expect(last_response.body).to eq('String')
224
+
225
+ get '/string', string: nil
226
+ expect(last_response.status).to eq(200)
227
+ expect(last_response.body).to eq('NilClass')
228
+ end
229
+
170
230
  it 'is a custom type' do
171
231
  subject.params do
172
232
  requires :uri, coerce: SecureURIOnly
@@ -281,119 +341,247 @@ describe Grape::Validations::CoerceValidator do
281
341
  end
282
342
  end
283
343
 
284
- it 'Bool' do
344
+ it 'Boolean' do
285
345
  subject.params do
286
- requires :bool, coerce: Grape::API::Boolean
346
+ requires :boolean, type: Boolean
287
347
  end
288
- subject.get '/bool' do
289
- params[:bool].class
348
+ subject.get '/boolean' do
349
+ params[:boolean].class
290
350
  end
291
351
 
292
- get '/bool', bool: 1
352
+ get '/boolean', boolean: 1
293
353
  expect(last_response.status).to eq(200)
294
354
  expect(last_response.body).to eq('TrueClass')
355
+ end
295
356
 
296
- get '/bool', bool: 0
297
- expect(last_response.status).to eq(200)
298
- expect(last_response.body).to eq('FalseClass')
357
+ context 'File' do
358
+ let(:file) { Rack::Test::UploadedFile.new(__FILE__) }
359
+ let(:filename) { File.basename(__FILE__).to_s }
299
360
 
300
- get '/bool', bool: 'false'
301
- expect(last_response.status).to eq(200)
302
- expect(last_response.body).to eq('FalseClass')
361
+ it 'Rack::Multipart::UploadedFile' do
362
+ subject.params do
363
+ requires :file, type: Rack::Multipart::UploadedFile
364
+ end
365
+ subject.post '/upload' do
366
+ params[:file][:filename]
367
+ end
303
368
 
304
- get '/bool', bool: 'true'
305
- expect(last_response.status).to eq(200)
306
- expect(last_response.body).to eq('TrueClass')
307
- end
369
+ post '/upload', file: file
370
+ expect(last_response.status).to eq(201)
371
+ expect(last_response.body).to eq(filename)
308
372
 
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
373
+ post '/upload', file: 'not a file'
374
+ expect(last_response.status).to eq(400)
375
+ expect(last_response.body).to eq('file is invalid')
315
376
  end
316
377
 
317
- get '/boolean'
318
- expect(last_response.status).to eq(200)
319
- expect(last_response.body).to eq('TrueClass')
378
+ it 'File' do
379
+ subject.params do
380
+ requires :file, coerce: File
381
+ end
382
+ subject.post '/upload' do
383
+ params[:file][:filename]
384
+ end
320
385
 
321
- get '/boolean', boolean: true
322
- expect(last_response.status).to eq(200)
323
- expect(last_response.body).to eq('TrueClass')
386
+ post '/upload', file: file
387
+ expect(last_response.status).to eq(201)
388
+ expect(last_response.body).to eq(filename)
324
389
 
325
- get '/boolean', boolean: false
326
- expect(last_response.status).to eq(200)
327
- expect(last_response.body).to eq('FalseClass')
328
-
329
- get '/boolean', boolean: 'true'
330
- expect(last_response.status).to eq(200)
331
- expect(last_response.body).to eq('TrueClass')
390
+ post '/upload', file: 'not a file'
391
+ expect(last_response.status).to eq(400)
392
+ expect(last_response.body).to eq('file is invalid')
332
393
 
333
- get '/boolean', boolean: 'false'
334
- expect(last_response.status).to eq(200)
335
- expect(last_response.body).to eq('FalseClass')
394
+ post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' }
395
+ expect(last_response.status).to eq(400)
396
+ expect(last_response.body).to eq('file is invalid')
397
+ end
336
398
 
337
- get '/boolean', boolean: 123
338
- expect(last_response.status).to eq(400)
339
- expect(last_response.body).to eq('boolean is invalid')
399
+ it 'collection' do
400
+ subject.params do
401
+ requires :files, type: Array[File]
402
+ end
403
+ subject.post '/upload' do
404
+ params[:files].first[:filename]
405
+ end
340
406
 
341
- get '/boolean', boolean: '123'
342
- expect(last_response.status).to eq(400)
343
- expect(last_response.body).to eq('boolean is invalid')
407
+ post '/upload', files: [file]
408
+ expect(last_response.status).to eq(201)
409
+ expect(last_response.body).to eq(filename)
410
+ end
344
411
  end
345
412
 
346
- it 'Rack::Multipart::UploadedFile' do
413
+ it 'Nests integers' do
347
414
  subject.params do
348
- requires :file, type: Rack::Multipart::UploadedFile
415
+ requires :integers, type: Hash do
416
+ requires :int, coerce: Integer
417
+ end
349
418
  end
350
- subject.post '/upload' do
351
- params[:file][:filename]
419
+ subject.get '/int' do
420
+ params[:integers][:int].class
352
421
  end
353
422
 
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')
423
+ get '/int', integers: { int: '45' }
424
+ expect(last_response.status).to eq(200)
425
+ expect(last_response.body).to eq(integer_class_name)
361
426
  end
362
427
 
363
- it 'File' do
364
- subject.params do
365
- requires :file, coerce: File
428
+ context 'nil values' do
429
+ context 'primitive types' do
430
+ Grape::Validations::Types::PRIMITIVES.each do |type|
431
+ it 'respects the nil value' do
432
+ subject.params do
433
+ requires :param, type: type
434
+ end
435
+ subject.get '/nil_value' do
436
+ params[:param].class
437
+ end
438
+
439
+ get '/nil_value', param: nil
440
+ expect(last_response.status).to eq(200)
441
+ expect(last_response.body).to eq('NilClass')
442
+ end
443
+ end
366
444
  end
367
- subject.post '/upload' do
368
- params[:file][:filename]
445
+
446
+ context 'structures types' do
447
+ Grape::Validations::Types::STRUCTURES.each do |type|
448
+ it 'respects the nil value' do
449
+ subject.params do
450
+ requires :param, type: type
451
+ end
452
+ subject.get '/nil_value' do
453
+ params[:param].class
454
+ end
455
+
456
+ get '/nil_value', param: nil
457
+ expect(last_response.status).to eq(200)
458
+ expect(last_response.body).to eq('NilClass')
459
+ end
460
+ end
369
461
  end
370
462
 
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)
463
+ context 'special types' do
464
+ Grape::Validations::Types::SPECIAL.each_key do |type|
465
+ it 'respects the nil value' do
466
+ subject.params do
467
+ requires :param, type: type
468
+ end
469
+ subject.get '/nil_value' do
470
+ params[:param].class
471
+ end
374
472
 
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')
473
+ get '/nil_value', param: nil
474
+ expect(last_response.status).to eq(200)
475
+ expect(last_response.body).to eq('NilClass')
476
+ end
477
+ end
378
478
 
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')
479
+ context 'variant-member-type collections' do
480
+ [
481
+ Array[Integer, String],
482
+ [Integer, String, Array[Integer, String]]
483
+ ].each do |type|
484
+ it 'respects the nil value' do
485
+ subject.params do
486
+ requires :param, type: type
487
+ end
488
+ subject.get '/nil_value' do
489
+ params[:param].class
490
+ end
491
+
492
+ get '/nil_value', param: nil
493
+ expect(last_response.status).to eq(200)
494
+ expect(last_response.body).to eq('NilClass')
495
+ end
496
+ end
497
+ end
498
+ end
382
499
  end
383
500
 
384
- it 'Nests integers' do
385
- subject.params do
386
- requires :integers, type: Hash do
387
- requires :int, coerce: Integer
501
+ context 'empty string' do
502
+ context 'primitive types' do
503
+ (Grape::Validations::Types::PRIMITIVES - [String]).each do |type|
504
+ it "is coerced to nil for type #{type}" do
505
+ subject.params do
506
+ requires :param, type: type
507
+ end
508
+ subject.get '/empty_string' do
509
+ params[:param].class
510
+ end
511
+
512
+ get '/empty_string', param: ''
513
+ expect(last_response.status).to eq(200)
514
+ expect(last_response.body).to eq('NilClass')
515
+ end
516
+ end
517
+
518
+ it 'is not coerced to nil for type String' do
519
+ subject.params do
520
+ requires :param, type: String
521
+ end
522
+ subject.get '/empty_string' do
523
+ params[:param].class
524
+ end
525
+
526
+ get '/empty_string', param: ''
527
+ expect(last_response.status).to eq(200)
528
+ expect(last_response.body).to eq('String')
388
529
  end
389
530
  end
390
- subject.get '/int' do
391
- params[:integers][:int].class
531
+
532
+ context 'structures types' do
533
+ (Grape::Validations::Types::STRUCTURES - [Hash]).each do |type|
534
+ it "is coerced to nil for type #{type}" do
535
+ subject.params do
536
+ requires :param, type: type
537
+ end
538
+ subject.get '/empty_string' do
539
+ params[:param].class
540
+ end
541
+
542
+ get '/empty_string', param: ''
543
+ expect(last_response.status).to eq(200)
544
+ expect(last_response.body).to eq('NilClass')
545
+ end
546
+ end
392
547
  end
393
548
 
394
- get '/int', integers: { int: '45' }
395
- expect(last_response.status).to eq(200)
396
- expect(last_response.body).to eq(integer_class_name)
549
+ context 'special types' do
550
+ (Grape::Validations::Types::SPECIAL.keys - [File, Rack::Multipart::UploadedFile]).each do |type|
551
+ it "is coerced to nil for type #{type}" do
552
+ subject.params do
553
+ requires :param, type: type
554
+ end
555
+ subject.get '/empty_string' do
556
+ params[:param].class
557
+ end
558
+
559
+ get '/empty_string', param: ''
560
+ expect(last_response.status).to eq(200)
561
+ expect(last_response.body).to eq('NilClass')
562
+ end
563
+ end
564
+
565
+ context 'variant-member-type collections' do
566
+ [
567
+ Array[Integer, String],
568
+ [Integer, String, Array[Integer, String]]
569
+ ].each do |type|
570
+ it "is coerced to nil for type #{type}" do
571
+ subject.params do
572
+ requires :param, type: type
573
+ end
574
+ subject.get '/empty_string' do
575
+ params[:param].class
576
+ end
577
+
578
+ get '/empty_string', param: ''
579
+ expect(last_response.status).to eq(200)
580
+ expect(last_response.body).to eq('NilClass')
581
+ end
582
+ end
583
+ end
584
+ end
397
585
  end
398
586
  end
399
587
 
@@ -432,6 +620,30 @@ describe Grape::Validations::CoerceValidator do
432
620
  expect(JSON.parse(last_response.body)).to eq(%w[a b c d])
433
621
  end
434
622
 
623
+ it 'parses parameters with Array[Array[String]] type and coerce_with' do
624
+ subject.params do
625
+ requires :values, type: Array[Array[String]], coerce_with: ->(val) { val.is_a?(String) ? [val.split(/,/).map(&:strip)] : val }
626
+ end
627
+ subject.post '/coerce_nested_strings' do
628
+ params[:values]
629
+ end
630
+
631
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: 'a,b,c,d'), 'CONTENT_TYPE' => 'application/json'
632
+ expect(last_response.status).to eq(201)
633
+ expect(JSON.parse(last_response.body)).to eq([%w[a b c d]])
634
+
635
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [%w[a c], %w[b]]), 'CONTENT_TYPE' => 'application/json'
636
+ expect(last_response.status).to eq(201)
637
+ expect(JSON.parse(last_response.body)).to eq([%w[a c], %w[b]])
638
+
639
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [[]]), 'CONTENT_TYPE' => 'application/json'
640
+ expect(last_response.status).to eq(201)
641
+ expect(JSON.parse(last_response.body)).to eq([[]])
642
+
643
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [['a', { bar: 0 }], ['b']]), 'CONTENT_TYPE' => 'application/json'
644
+ expect(last_response.status).to eq(400)
645
+ end
646
+
435
647
  it 'parses parameters with Array[Integer] type' do
436
648
  subject.params do
437
649
  requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) }
@@ -514,6 +726,46 @@ describe Grape::Validations::CoerceValidator do
514
726
  expect(last_response.body).to eq('3')
515
727
  end
516
728
 
729
+ context 'Integer type and coerce_with potentially returning nil' do
730
+ before do
731
+ subject.params do
732
+ requires :int, type: Integer, coerce_with: (lambda do |val|
733
+ if val == '0'
734
+ nil
735
+ elsif val.match?(/^-?\d+$/)
736
+ val.to_i
737
+ else
738
+ val
739
+ end
740
+ end)
741
+ end
742
+ subject.get '/' do
743
+ params[:int].class.to_s
744
+ end
745
+ end
746
+
747
+ it 'accepts value that coerces to nil' do
748
+ get '/', int: '0'
749
+
750
+ expect(last_response.status).to eq(200)
751
+ expect(last_response.body).to eq('NilClass')
752
+ end
753
+
754
+ it 'coerces to Integer' do
755
+ get '/', int: '1'
756
+
757
+ expect(last_response.status).to eq(200)
758
+ expect(last_response.body).to eq('Integer')
759
+ end
760
+
761
+ it 'returns invalid value if coercion returns a wrong type' do
762
+ get '/', int: 'lol'
763
+
764
+ expect(last_response.status).to eq(400)
765
+ expect(last_response.body).to eq('int is invalid')
766
+ end
767
+ end
768
+
517
769
  it 'must be supplied with :type or :coerce' do
518
770
  expect do
519
771
  subject.params do