grape 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grape might be problematic. Click here for more details.

Files changed (79) hide show
  1. checksums.yaml +9 -9
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +5 -0
  4. data/.travis.yml +3 -3
  5. data/CHANGELOG.md +42 -3
  6. data/CONTRIBUTING.md +118 -0
  7. data/Gemfile +4 -4
  8. data/README.md +312 -52
  9. data/Rakefile +6 -1
  10. data/UPGRADING.md +124 -0
  11. data/lib/grape.rb +2 -0
  12. data/lib/grape/api.rb +95 -44
  13. data/lib/grape/cookies.rb +0 -2
  14. data/lib/grape/endpoint.rb +63 -39
  15. data/lib/grape/error_formatter/base.rb +0 -3
  16. data/lib/grape/error_formatter/json.rb +0 -2
  17. data/lib/grape/error_formatter/txt.rb +0 -2
  18. data/lib/grape/error_formatter/xml.rb +0 -2
  19. data/lib/grape/exceptions/base.rb +0 -2
  20. data/lib/grape/exceptions/incompatible_option_values.rb +0 -3
  21. data/lib/grape/exceptions/invalid_formatter.rb +0 -3
  22. data/lib/grape/exceptions/invalid_versioner_option.rb +0 -4
  23. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +0 -2
  24. data/lib/grape/exceptions/missing_mime_type.rb +0 -4
  25. data/lib/grape/exceptions/missing_option.rb +0 -3
  26. data/lib/grape/exceptions/missing_vendor_option.rb +0 -3
  27. data/lib/grape/exceptions/unknown_options.rb +0 -4
  28. data/lib/grape/exceptions/unknown_validator.rb +0 -2
  29. data/lib/grape/exceptions/validation_errors.rb +6 -5
  30. data/lib/grape/formatter/base.rb +0 -3
  31. data/lib/grape/formatter/json.rb +0 -2
  32. data/lib/grape/formatter/serializable_hash.rb +15 -16
  33. data/lib/grape/formatter/txt.rb +0 -2
  34. data/lib/grape/formatter/xml.rb +0 -2
  35. data/lib/grape/http/request.rb +2 -4
  36. data/lib/grape/locale/en.yml +1 -1
  37. data/lib/grape/middleware/auth/oauth2.rb +15 -6
  38. data/lib/grape/middleware/base.rb +7 -7
  39. data/lib/grape/middleware/error.rb +11 -6
  40. data/lib/grape/middleware/formatter.rb +80 -78
  41. data/lib/grape/middleware/globals.rb +13 -0
  42. data/lib/grape/middleware/versioner/accept_version_header.rb +0 -2
  43. data/lib/grape/middleware/versioner/header.rb +5 -3
  44. data/lib/grape/middleware/versioner/param.rb +2 -4
  45. data/lib/grape/middleware/versioner/path.rb +3 -4
  46. data/lib/grape/namespace.rb +0 -1
  47. data/lib/grape/parser/base.rb +0 -3
  48. data/lib/grape/parser/json.rb +0 -2
  49. data/lib/grape/parser/xml.rb +0 -2
  50. data/lib/grape/path.rb +1 -3
  51. data/lib/grape/route.rb +0 -3
  52. data/lib/grape/util/hash_stack.rb +1 -1
  53. data/lib/grape/validations.rb +72 -22
  54. data/lib/grape/validations/coerce.rb +5 -4
  55. data/lib/grape/validations/default.rb +5 -3
  56. data/lib/grape/validations/presence.rb +1 -1
  57. data/lib/grape/validations/regexp.rb +0 -2
  58. data/lib/grape/validations/values.rb +2 -1
  59. data/lib/grape/version.rb +1 -1
  60. data/spec/grape/api_spec.rb +385 -96
  61. data/spec/grape/endpoint_spec.rb +162 -15
  62. data/spec/grape/entity_spec.rb +25 -0
  63. data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
  64. data/spec/grape/middleware/auth/oauth2_spec.rb +60 -15
  65. data/spec/grape/middleware/base_spec.rb +3 -8
  66. data/spec/grape/middleware/error_spec.rb +2 -2
  67. data/spec/grape/middleware/exception_spec.rb +4 -4
  68. data/spec/grape/middleware/formatter_spec.rb +7 -4
  69. data/spec/grape/middleware/versioner/param_spec.rb +8 -7
  70. data/spec/grape/path_spec.rb +24 -14
  71. data/spec/grape/util/hash_stack_spec.rb +8 -8
  72. data/spec/grape/validations/coerce_spec.rb +75 -33
  73. data/spec/grape/validations/default_spec.rb +57 -0
  74. data/spec/grape/validations/presence_spec.rb +13 -11
  75. data/spec/grape/validations/values_spec.rb +76 -2
  76. data/spec/grape/validations_spec.rb +443 -20
  77. data/spec/spec_helper.rb +2 -2
  78. data/spec/support/content_type_helpers.rb +11 -0
  79. metadata +9 -38
@@ -1,13 +1,12 @@
1
1
  module Grape
2
-
3
2
  class API
4
3
  Boolean = Virtus::Attribute::Boolean # rubocop:disable ConstantName
5
4
  end
6
5
 
7
6
  module Validations
8
-
9
7
  class CoerceValidator < SingleOptionValidator
10
8
  def validate_param!(attr_name, params)
9
+ raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message_key: :coerce unless params.is_a? Hash
11
10
  new_value = coerce_value(@option, params[attr_name])
12
11
  if valid_type?(new_value)
13
12
  params[attr_name] = new_value
@@ -47,6 +46,10 @@ module Grape
47
46
  end
48
47
 
49
48
  def coerce_value(type, val)
49
+ # Don't coerce things other than nil to Arrays or Hashes
50
+ return val || [] if type == Array
51
+ return val || {} if type == Hash
52
+
50
53
  converter = Virtus::Attribute.build(type)
51
54
  converter.coerce(val)
52
55
 
@@ -55,8 +58,6 @@ module Grape
55
58
  rescue
56
59
  InvalidValue.new
57
60
  end
58
-
59
61
  end
60
-
61
62
  end
62
63
  end
@@ -7,14 +7,16 @@ module Grape
7
7
  end
8
8
 
9
9
  def validate_param!(attr_name, params)
10
- params[attr_name] = @default unless params.has_key?(attr_name)
10
+ params[attr_name] = @default.is_a?(Proc) ? @default.call : @default unless params.has_key?(attr_name)
11
11
  end
12
12
 
13
13
  def validate!(params)
14
- params = AttributesIterator.new(self, @scope, params)
15
- params.each do |resource_params, attr_name|
14
+ attrs = AttributesIterator.new(self, @scope, params)
15
+ parent_element = @scope.element
16
+ attrs.each do |resource_params, attr_name|
16
17
  if resource_params[attr_name].nil?
17
18
  validate_param!(attr_name, resource_params)
19
+ params[parent_element] = resource_params if parent_element && params[parent_element].nil?
18
20
  end
19
21
  end
20
22
  end
@@ -7,7 +7,7 @@ module Grape
7
7
  end
8
8
 
9
9
  def validate_param!(attr_name, params)
10
- unless params.has_key?(attr_name)
10
+ unless params.respond_to?(:has_key?) && params.has_key?(attr_name)
11
11
  raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message_key: :presence
12
12
  end
13
13
  end
@@ -1,6 +1,5 @@
1
1
  module Grape
2
2
  module Validations
3
-
4
3
  class RegexpValidator < SingleOptionValidator
5
4
  def validate_param!(attr_name, params)
6
5
  if params[attr_name] && !(params[attr_name].to_s =~ @option)
@@ -8,6 +7,5 @@ module Grape
8
7
  end
9
8
  end
10
9
  end
11
-
12
10
  end
13
11
  end
@@ -3,11 +3,12 @@ module Grape
3
3
  class ValuesValidator < Validator
4
4
  def initialize(attrs, options, required, scope)
5
5
  @values = options
6
+ @required = required
6
7
  super
7
8
  end
8
9
 
9
10
  def validate_param!(attr_name, params)
10
- if params[attr_name] && !@values.include?(params[attr_name])
11
+ if (params[attr_name] || @required) && !(@values.is_a?(Proc) ? @values.call : @values).include?(params[attr_name])
11
12
  raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message_key: :values
12
13
  end
13
14
  end
@@ -1,3 +1,3 @@
1
1
  module Grape
2
- VERSION = '0.6.1'
2
+ VERSION = '0.7.0'
3
3
  end
@@ -383,7 +383,7 @@ describe Grape::API do
383
383
  subject.send(verb) do
384
384
  env['api.request.body']
385
385
  end
386
- send verb, '/', MultiJson.dump(object), { 'CONTENT_TYPE' => 'application/json' }
386
+ send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json'
387
387
  last_response.status.should == (verb == :post ? 201 : 200)
388
388
  last_response.body.should eql MultiJson.dump(object)
389
389
  last_request.params.should eql Hash.new
@@ -393,7 +393,7 @@ describe Grape::API do
393
393
  subject.send(verb) do
394
394
  env['api.request.input']
395
395
  end
396
- send verb, '/', MultiJson.dump(object), { 'CONTENT_TYPE' => 'application/json' }
396
+ send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json'
397
397
  last_response.status.should == (verb == :post ? 201 : 200)
398
398
  last_response.body.should eql MultiJson.dump(object).to_json
399
399
  end
@@ -403,7 +403,7 @@ describe Grape::API do
403
403
  subject.send(verb) do
404
404
  env['api.request.input']
405
405
  end
406
- send verb, '/', MultiJson.dump(object), { 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked', 'CONTENT_LENGTH' => nil }
406
+ send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked', 'CONTENT_LENGTH' => nil
407
407
  last_response.status.should == (verb == :post ? 201 : 200)
408
408
  last_response.body.should eql MultiJson.dump(object).to_json
409
409
  end
@@ -473,13 +473,15 @@ describe Grape::API do
473
473
  last_response.body.should eql 'Created'
474
474
  end
475
475
 
476
- it 'returns a 405 for an unsupported method' do
476
+ it 'returns a 405 for an unsupported method with an X-Custom-Header' do
477
+ subject.before { header 'X-Custom-Header', 'foo' }
477
478
  subject.get 'example' do
478
479
  "example"
479
480
  end
480
481
  put '/example'
481
482
  last_response.status.should eql 405
482
483
  last_response.body.should eql ''
484
+ last_response.headers['X-Custom-Header'].should eql 'foo'
483
485
  end
484
486
 
485
487
  specify '405 responses includes an Allow header specifying supported methods' do
@@ -504,7 +506,8 @@ describe Grape::API do
504
506
  last_response.headers['Content-Type'].should eql 'text/plain'
505
507
  end
506
508
 
507
- it 'adds an OPTIONS route that returns a 204 and an Allow header' do
509
+ it 'adds an OPTIONS route that returns a 204, an Allow header and a X-Custom-Header' do
510
+ subject.before { header 'X-Custom-Header', 'foo' }
508
511
  subject.get 'example' do
509
512
  "example"
510
513
  end
@@ -512,6 +515,7 @@ describe Grape::API do
512
515
  last_response.status.should eql 204
513
516
  last_response.body.should eql ''
514
517
  last_response.headers['Allow'].should eql 'OPTIONS, GET, HEAD'
518
+ last_response.headers['X-Custom-Header'].should eql 'foo'
515
519
  end
516
520
 
517
521
  it 'allows HEAD on a GET request' do
@@ -579,6 +583,31 @@ describe Grape::API do
579
583
  last_response.body.should eql 'first second'
580
584
  end
581
585
 
586
+ it 'adds a before filter to current and child namespaces only' do
587
+ subject.get '/' do
588
+ "root - #{@foo}"
589
+ end
590
+ subject.namespace :blah do
591
+ before { @foo = 'foo' }
592
+ get '/' do
593
+ "blah - #{@foo}"
594
+ end
595
+
596
+ namespace :bar do
597
+ get '/' do
598
+ "blah - bar - #{@foo}"
599
+ end
600
+ end
601
+ end
602
+
603
+ get '/'
604
+ last_response.body.should eql 'root - '
605
+ get '/blah'
606
+ last_response.body.should eql 'blah - foo'
607
+ get '/blah/bar'
608
+ last_response.body.should eql 'blah - bar - foo'
609
+ end
610
+
582
611
  it 'adds a after_validation filter' do
583
612
  subject.after_validation { @foo = "first #{params[:id] }:#{params[:id].class}" }
584
613
  subject.after_validation { @bar = 'second' }
@@ -605,6 +634,94 @@ describe Grape::API do
605
634
  get '/'
606
635
  last_response.body.should eql 'default'
607
636
  end
637
+
638
+ it 'calls all filters when validation passes' do
639
+ a = double('before mock')
640
+ b = double('before_validation mock')
641
+ c = double('after_validation mock')
642
+ d = double('after mock')
643
+
644
+ subject.params do
645
+ requires :id, type: Integer
646
+ end
647
+ subject.resource ':id' do
648
+ before { a.do_something! }
649
+ before_validation { b.do_something! }
650
+ after_validation { c.do_something! }
651
+ after { d.do_something! }
652
+ get do
653
+ 'got it'
654
+ end
655
+ end
656
+
657
+ a.should_receive(:do_something!).exactly(1).times
658
+ b.should_receive(:do_something!).exactly(1).times
659
+ c.should_receive(:do_something!).exactly(1).times
660
+ d.should_receive(:do_something!).exactly(1).times
661
+
662
+ get '/123'
663
+ last_response.status.should eql 200
664
+ last_response.body.should eql 'got it'
665
+ end
666
+
667
+ it 'calls only before filters when validation fails' do
668
+ a = double('before mock')
669
+ b = double('before_validation mock')
670
+ c = double('after_validation mock')
671
+ d = double('after mock')
672
+
673
+ subject.params do
674
+ requires :id, type: Integer
675
+ end
676
+ subject.resource ':id' do
677
+ before { a.do_something! }
678
+ before_validation { b.do_something! }
679
+ after_validation { c.do_something! }
680
+ after { d.do_something! }
681
+ get do
682
+ 'got it'
683
+ end
684
+ end
685
+
686
+ a.should_receive(:do_something!).exactly(1).times
687
+ b.should_receive(:do_something!).exactly(1).times
688
+ c.should_receive(:do_something!).exactly(0).times
689
+ d.should_receive(:do_something!).exactly(0).times
690
+
691
+ get '/abc'
692
+ last_response.status.should eql 400
693
+ last_response.body.should eql 'id is invalid'
694
+ end
695
+
696
+ it 'calls filters in the correct order' do
697
+ i = 0
698
+ a = double('before mock')
699
+ b = double('before_validation mock')
700
+ c = double('after_validation mock')
701
+ d = double('after mock')
702
+
703
+ subject.params do
704
+ requires :id, type: Integer
705
+ end
706
+ subject.resource ':id' do
707
+ before { a.here(i += 1) }
708
+ before_validation { b.here(i += 1) }
709
+ after_validation { c.here(i += 1) }
710
+ after { d.here(i += 1) }
711
+ get do
712
+ 'got it'
713
+ end
714
+ end
715
+
716
+ a.should_receive(:here).with(1).exactly(1).times
717
+ b.should_receive(:here).with(2).exactly(1).times
718
+ c.should_receive(:here).with(3).exactly(1).times
719
+ d.should_receive(:here).with(4).exactly(1).times
720
+
721
+ get '/123'
722
+ last_response.status.should eql 200
723
+ last_response.body.should eql 'got it'
724
+ end
608
725
  end
609
726
 
610
727
  context 'format' do
@@ -641,6 +758,26 @@ describe Grape::API do
641
758
  get '/error.xml'
642
759
  last_response.headers['Content-Type'].should eql 'application/xml'
643
760
  end
761
+
762
+ context 'with a custom content_type' do
763
+ before do
764
+ subject.content_type :custom, 'application/custom'
765
+ subject.formatter :custom, lambda { |object, env| "custom" }
766
+
767
+ subject.get('/custom') { 'bar' }
768
+ subject.get('/error') { error!('error in custom', 500) }
769
+ end
770
+
771
+ it 'sets content type' do
772
+ get '/custom.custom'
773
+ last_response.headers['Content-Type'].should eql 'application/custom'
774
+ end
775
+
776
+ it 'sets content type for error' do
777
+ get '/error.custom'
778
+ last_response.headers['Content-Type'].should eql 'application/custom'
779
+ end
780
+ end
644
781
  end
645
782
 
646
783
  context 'custom middleware' do
@@ -713,13 +850,13 @@ describe Grape::API do
713
850
  end
714
851
 
715
852
  it 'adds a block if one is given' do
716
- block = lambda { }
853
+ block = lambda {}
717
854
  subject.use ApiSpec::PhonyMiddleware, &block
718
855
  subject.middleware.should eql [[ApiSpec::PhonyMiddleware, block]]
719
856
  end
720
857
 
721
858
  it 'uses a block if one is given' do
722
- block = lambda { }
859
+ block = lambda {}
723
860
  subject.use ApiSpec::PhonyMiddleware, &block
724
861
  subject.get '/' do
725
862
  env['phony.block'].inspect
@@ -730,7 +867,7 @@ describe Grape::API do
730
867
  end
731
868
 
732
869
  it 'does not destroy the middleware settings on multiple runs' do
733
- block = lambda { }
870
+ block = lambda {}
734
871
  subject.use ApiSpec::PhonyMiddleware, &block
735
872
  subject.get '/' do
736
873
  env['phony.block'].inspect
@@ -757,7 +894,7 @@ describe Grape::API do
757
894
  end
758
895
  end
759
896
  end
760
- describe '.basic' do
897
+ describe '.http_basic' do
761
898
  it 'protects any resources on the same scope' do
762
899
  subject.http_basic do |u, p|
763
900
  u == 'allow'
@@ -796,6 +933,51 @@ describe Grape::API do
796
933
  get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
797
934
  last_response.status.should eql 200
798
935
  end
936
+
937
+ it 'has access to the current endpoint' do
938
+ basic_auth_context = nil
939
+
940
+ subject.http_basic do |u, p|
941
+ basic_auth_context = self
942
+
943
+ u == 'allow'
944
+ end
945
+
946
+ subject.get(:hello) { "Hello, world." }
947
+ get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
948
+ basic_auth_context.should be_an_instance_of(Grape::Endpoint)
949
+ end
950
+
951
+ it 'has access to helper methods' do
952
+ subject.helpers do
953
+ def authorize(u, p)
954
+ u == 'allow' && p == 'whatever'
955
+ end
956
+ end
957
+
958
+ subject.http_basic do |u, p|
959
+ authorize(u, p)
960
+ end
961
+
962
+ subject.get(:hello) { "Hello, world." }
963
+ get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
964
+ last_response.status.should eql 200
965
+ get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('disallow', 'whatever')
966
+ last_response.status.should eql 401
967
+ end
968
+
969
+ it 'can set instance variables accessible to routes' do
970
+ subject.http_basic do |u, p|
971
+ @hello = "Hello, world."
972
+
973
+ u == 'allow'
974
+ end
975
+
976
+ subject.get(:hello) { @hello }
977
+ get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
978
+ last_response.status.should eql 200
979
+ last_response.body.should eql "Hello, world."
980
+ end
799
981
  end
800
982
 
801
983
  describe '.logger' do
@@ -964,7 +1146,7 @@ describe Grape::API do
964
1146
  raise "rain!"
965
1147
  end
966
1148
  get '/exception'
967
- last_response.status.should eql 403
1149
+ last_response.status.should eql 500
968
1150
  end
969
1151
 
970
1152
  it 'rescues only certain errors if rescue_from is called with specific errors' do
@@ -973,30 +1155,34 @@ describe Grape::API do
973
1155
  subject.get('/unrescued') { raise "beefcake" }
974
1156
 
975
1157
  get '/rescued'
976
- last_response.status.should eql 403
1158
+ last_response.status.should eql 500
977
1159
 
978
1160
  lambda { get '/unrescued' }.should raise_error
979
1161
  end
980
1162
 
981
- it 'does not re-raise exceptions of type Grape::Exception::Base' do
982
- class CustomError < Grape::Exceptions::Base; end
983
- subject.get('/custom_exception') { raise CustomError }
1163
+ context 'CustomError subclass of Grape::Exceptions::Base' do
1164
+ before do
1165
+ class CustomError < Grape::Exceptions::Base; end
1166
+ end
984
1167
 
985
- lambda { get '/custom_exception' }.should_not raise_error
986
- end
1168
+ it 'does not re-raise exceptions of type Grape::Exceptions::Base' do
1169
+ subject.get('/custom_exception') { raise CustomError }
987
1170
 
988
- it 'rescues custom grape exceptions' do
989
- class CustomError < Grape::Exceptions::Base; end
990
- subject.rescue_from CustomError do |e|
991
- rack_response('New Error', e.status)
992
- end
993
- subject.get '/custom_error' do
994
- raise CustomError, status: 400, message: 'Custom Error'
1171
+ lambda { get '/custom_exception' }.should_not raise_error
995
1172
  end
996
1173
 
997
- get '/custom_error'
998
- last_response.status.should == 400
999
- last_response.body.should == 'New Error'
1174
+ it 'rescues custom grape exceptions' do
1175
+ subject.rescue_from CustomError do |e|
1176
+ rack_response('New Error', e.status)
1177
+ end
1178
+ subject.get '/custom_error' do
1179
+ raise CustomError, status: 400, message: 'Custom Error'
1180
+ end
1181
+
1182
+ get '/custom_error'
1183
+ last_response.status.should == 400
1184
+ last_response.body.should == 'New Error'
1185
+ end
1000
1186
  end
1001
1187
 
1002
1188
  it 'can rescue exceptions raised in the formatter' do
@@ -1027,61 +1213,65 @@ describe Grape::API do
1027
1213
  last_response.status.should eql 202
1028
1214
  last_response.body.should == 'rescued from rain!'
1029
1215
  end
1030
- it 'rescues an error via rescue_from :all' do
1031
- class ConnectionError < RuntimeError; end
1032
- subject.rescue_from :all do |e|
1033
- rack_response("rescued from #{e.class.name}", 500)
1034
- end
1035
- subject.get '/exception' do
1036
- raise ConnectionError
1037
- end
1038
- get '/exception'
1039
- last_response.status.should eql 500
1040
- last_response.body.should == 'rescued from ConnectionError'
1041
- end
1042
- it 'rescues a specific error' do
1043
- class ConnectionError < RuntimeError; end
1044
- subject.rescue_from ConnectionError do |e|
1045
- rack_response("rescued from #{e.class.name}", 500)
1046
- end
1047
- subject.get '/exception' do
1048
- raise ConnectionError
1049
- end
1050
- get '/exception'
1051
- last_response.status.should eql 500
1052
- last_response.body.should == 'rescued from ConnectionError'
1053
- end
1054
- it 'rescues multiple specific errors' do
1055
- class ConnectionError < RuntimeError; end
1056
- class DatabaseError < RuntimeError; end
1057
- subject.rescue_from ConnectionError do |e|
1058
- rack_response("rescued from #{e.class.name}", 500)
1059
- end
1060
- subject.rescue_from DatabaseError do |e|
1061
- rack_response("rescued from #{e.class.name}", 500)
1216
+
1217
+ context 'custom errors' do
1218
+ before do
1219
+ class ConnectionError < RuntimeError; end
1220
+ class DatabaseError < RuntimeError; end
1221
+ class CommunicationError < StandardError; end
1062
1222
  end
1063
- subject.get '/connection' do
1064
- raise ConnectionError
1223
+
1224
+ it 'rescues an error via rescue_from :all' do
1225
+ subject.rescue_from :all do |e|
1226
+ rack_response("rescued from #{e.class.name}", 500)
1227
+ end
1228
+ subject.get '/exception' do
1229
+ raise ConnectionError
1230
+ end
1231
+ get '/exception'
1232
+ last_response.status.should eql 500
1233
+ last_response.body.should == 'rescued from ConnectionError'
1065
1234
  end
1066
- subject.get '/database' do
1067
- raise DatabaseError
1235
+ it 'rescues a specific error' do
1236
+ subject.rescue_from ConnectionError do |e|
1237
+ rack_response("rescued from #{e.class.name}", 500)
1238
+ end
1239
+ subject.get '/exception' do
1240
+ raise ConnectionError
1241
+ end
1242
+ get '/exception'
1243
+ last_response.status.should eql 500
1244
+ last_response.body.should == 'rescued from ConnectionError'
1068
1245
  end
1069
- get '/connection'
1070
- last_response.status.should eql 500
1071
- last_response.body.should == 'rescued from ConnectionError'
1072
- get '/database'
1073
- last_response.status.should eql 500
1074
- last_response.body.should == 'rescued from DatabaseError'
1075
- end
1076
- it 'does not rescue a different error' do
1077
- class CommunicationError < RuntimeError; end
1078
- subject.rescue_from RuntimeError do |e|
1079
- rack_response("rescued from #{e.class.name}", 500)
1246
+ it 'rescues multiple specific errors' do
1247
+ subject.rescue_from ConnectionError do |e|
1248
+ rack_response("rescued from #{e.class.name}", 500)
1249
+ end
1250
+ subject.rescue_from DatabaseError do |e|
1251
+ rack_response("rescued from #{e.class.name}", 500)
1252
+ end
1253
+ subject.get '/connection' do
1254
+ raise ConnectionError
1255
+ end
1256
+ subject.get '/database' do
1257
+ raise DatabaseError
1258
+ end
1259
+ get '/connection'
1260
+ last_response.status.should eql 500
1261
+ last_response.body.should == 'rescued from ConnectionError'
1262
+ get '/database'
1263
+ last_response.status.should eql 500
1264
+ last_response.body.should == 'rescued from DatabaseError'
1080
1265
  end
1081
- subject.get '/uncaught' do
1082
- raise CommunicationError
1266
+ it 'does not rescue a different error' do
1267
+ subject.rescue_from RuntimeError do |e|
1268
+ rack_response("rescued from #{e.class.name}", 500)
1269
+ end
1270
+ subject.get '/uncaught' do
1271
+ raise CommunicationError
1272
+ end
1273
+ lambda { get '/uncaught' }.should raise_error(CommunicationError)
1083
1274
  end
1084
- lambda { get '/uncaught' }.should raise_error(CommunicationError)
1085
1275
  end
1086
1276
  end
1087
1277
 
@@ -1124,6 +1314,46 @@ describe Grape::API do
1124
1314
  end
1125
1315
  end
1126
1316
 
1317
+ describe '.rescue_from klass, rescue_subclasses: boolean' do
1318
+ before do
1319
+ module APIErrors
1320
+ class ParentError < StandardError; end
1321
+ class ChildError < ParentError; end
1322
+ end
1323
+ end
1324
+
1325
+ it 'rescues error as well as subclass errors with rescue_subclasses option set' do
1326
+ subject.rescue_from APIErrors::ParentError, rescue_subclasses: true do |e|
1327
+ rack_response("rescued from #{e.class.name}", 500)
1328
+ end
1329
+ subject.get '/caught_child' do
1330
+ raise APIErrors::ChildError
1331
+ end
1332
+ subject.get '/caught_parent' do
1333
+ raise APIErrors::ParentError
1334
+ end
1335
+ subject.get '/uncaught_parent' do
1336
+ raise StandardError
1337
+ end
1338
+
1339
+ get '/caught_child'
1340
+ last_response.status.should eql 500
1341
+ get '/caught_parent'
1342
+ last_response.status.should eql 500
1343
+ lambda { get '/uncaught_parent' }.should raise_error(StandardError)
1344
+ end
1345
+
1346
+ it 'does not rescue child errors if rescue_subclasses is false' do
1347
+ subject.rescue_from APIErrors::ParentError, rescue_subclasses: false do |e|
1348
+ rack_response("rescued from #{e.class.name}", 500)
1349
+ end
1350
+ subject.get '/uncaught' do
1351
+ raise APIErrors::ChildError
1352
+ end
1353
+ lambda { get '/uncaught' }.should raise_error(APIErrors::ChildError)
1354
+ end
1355
+ end
1356
+
1127
1357
  describe '.error_format' do
1128
1358
  it 'rescues all errors and return :txt' do
1129
1359
  subject.rescue_from :all
@@ -1142,7 +1372,7 @@ describe Grape::API do
1142
1372
  raise "rain!"
1143
1373
  end
1144
1374
  get '/exception'
1145
- last_response.body.start_with?("rain!\r\n").should be_true
1375
+ last_response.body.start_with?("rain!\r\n").should be true
1146
1376
  end
1147
1377
 
1148
1378
  it 'rescues all errors with a default formatter' do
@@ -1308,7 +1538,7 @@ describe Grape::API do
1308
1538
  last_response.body.should eql '{"some":"hash"}'
1309
1539
  end
1310
1540
  it 'uses custom formatter' do
1311
- get '/simple.custom', { 'HTTP_ACCEPT' => 'application/custom' }
1541
+ get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom'
1312
1542
  last_response.body.should eql '{"custom_formatter":"hash"}'
1313
1543
  end
1314
1544
  end
@@ -1331,7 +1561,7 @@ describe Grape::API do
1331
1561
  last_response.body.should eql '{"some":"hash"}'
1332
1562
  end
1333
1563
  it 'uses custom formatter' do
1334
- get '/simple.custom', { 'HTTP_ACCEPT' => 'application/custom' }
1564
+ get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom'
1335
1565
  last_response.body.should eql '{"custom_formatter":"hash"}'
1336
1566
  end
1337
1567
  end
@@ -1343,7 +1573,7 @@ describe Grape::API do
1343
1573
  subject.post '/data' do
1344
1574
  { x: params[:x] }
1345
1575
  end
1346
- post "/data", '{"x":42}', { 'CONTENT_TYPE' => 'application/json' }
1576
+ post "/data", '{"x":42}', 'CONTENT_TYPE' => 'application/json'
1347
1577
  last_response.status.should == 201
1348
1578
  last_response.body.should == '{"x":42}'
1349
1579
  end
@@ -1448,7 +1678,16 @@ describe Grape::API do
1448
1678
  raise "rain!"
1449
1679
  end
1450
1680
  get '/exception'
1451
- last_response.status.should eql 403
1681
+ last_response.status.should eql 500
1682
+ end
1683
+ it 'uses the default error status in error!' do
1684
+ subject.rescue_from :all
1685
+ subject.default_error_status 400
1686
+ subject.get '/exception' do
1687
+ error! "rain!"
1688
+ end
1689
+ get '/exception'
1690
+ last_response.status.should eql 400
1452
1691
  end
1453
1692
  end
1454
1693
 
@@ -1513,7 +1752,7 @@ describe Grape::API do
1513
1752
  end
1514
1753
  describe 'api structure with additional parameters' do
1515
1754
  before(:each) do
1516
- subject.get 'split/:string', { params: { "token" => "a token" }, optional_params: { "limit" => "the limit" } } do
1755
+ subject.get 'split/:string', params: { "token" => "a token" }, optional_params: { "limit" => "the limit" } do
1517
1756
  params[:string].split(params[:token], (params[:limit] || 0).to_i)
1518
1757
  end
1519
1758
  end
@@ -1550,7 +1789,7 @@ describe Grape::API do
1550
1789
  route = subject.routes.first
1551
1790
  route.route_description.should == "first method"
1552
1791
  route.route_foo.should be_nil
1553
- route.route_params.should == { }
1792
+ route.route_params.should == {}
1554
1793
  end
1555
1794
  it 'describes methods separately' do
1556
1795
  subject.desc "first method"
@@ -1597,9 +1836,7 @@ describe Grape::API do
1597
1836
  ]
1598
1837
  end
1599
1838
  it 'describes a method with parameters' do
1600
- subject.desc "Reverses a string.", { params:
1601
- { "s" => { desc: "string to reverse", type: "string" } }
1602
- }
1839
+ subject.desc "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } }
1603
1840
  subject.get 'reverse' do
1604
1841
  params[:s].reverse
1605
1842
  end
@@ -1682,8 +1919,10 @@ describe Grape::API do
1682
1919
  subject.routes.map { |route|
1683
1920
  route.route_params
1684
1921
  }.should eq [{
1922
+ "group1" => { required: true, type: "Array" },
1685
1923
  "group1[param1]" => { required: false, desc: "group1 param1 desc" },
1686
1924
  "group1[param2]" => { required: true, desc: "group1 param2 desc" },
1925
+ "group2" => { required: true, type: "Array" },
1687
1926
  "group2[param1]" => { required: false, desc: "group2 param1 desc" },
1688
1927
  "group2[param2]" => { required: true, desc: "group2 param2 desc" }
1689
1928
  }]
@@ -1703,11 +1942,19 @@ describe Grape::API do
1703
1942
  { description: "nesting",
1704
1943
  params: {
1705
1944
  "root_param" => { required: true, desc: "root param" },
1945
+ "nested" => { required: true, type: "Array" },
1706
1946
  "nested[nested_param]" => { required: true, desc: "nested param" }
1707
1947
  }
1708
1948
  }
1709
1949
  ]
1710
1950
  end
1951
+ it 'allows to set the type attribute on :group element' do
1952
+ subject.params do
1953
+ group :foo, type: Array do
1954
+ optional :bar
1955
+ end
1956
+ end
1957
+ end
1711
1958
  it 'parses parameters when no description is given' do
1712
1959
  subject.params do
1713
1960
  requires :one_param, desc: "one param"
@@ -1720,9 +1967,7 @@ describe Grape::API do
1720
1967
  ]
1721
1968
  end
1722
1969
  it 'does not symbolize params' do
1723
- subject.desc "Reverses a string.", { params:
1724
- { "s" => { desc: "string to reverse", type: "string" } }
1725
- }
1970
+ subject.desc "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } }
1726
1971
  subject.get 'reverse/:s' do
1727
1972
  params[:s].reverse
1728
1973
  end
@@ -1735,7 +1980,7 @@ describe Grape::API do
1735
1980
  end
1736
1981
 
1737
1982
  describe '.mount' do
1738
- let(:mounted_app) { lambda { |env| [200, { }, ["MOUNTED"]] } }
1983
+ let(:mounted_app) { lambda { |env| [200, {}, ["MOUNTED"]] } }
1739
1984
 
1740
1985
  context 'with a bare rack app' do
1741
1986
  before do
@@ -1827,8 +2072,8 @@ describe Grape::API do
1827
2072
  it 'collects the routes of the mounted api' do
1828
2073
  subject.namespace :cool do
1829
2074
  app = Class.new(Grape::API)
1830
- app.get('/awesome') { }
1831
- app.post('/sauce') { }
2075
+ app.get('/awesome') {}
2076
+ app.post('/sauce') {}
1832
2077
  mount app
1833
2078
  end
1834
2079
  subject.routes.size.should == 2
@@ -1861,6 +2106,50 @@ describe Grape::API do
1861
2106
  get "/app1/app2/nice"
1862
2107
  last_response.status.should == 200
1863
2108
  last_response.body.should == "play"
2109
+ options "/app1/app2/nice"
2110
+ last_response.status.should == 204
2111
+ end
2112
+
2113
+ it 'responds to options' do
2114
+ app = Class.new(Grape::API)
2115
+ app.get '/colour' do
2116
+ 'red'
2117
+ end
2118
+ app.namespace :pears do
2119
+ get '/colour' do
2120
+ 'green'
2121
+ end
2122
+ end
2123
+ subject.namespace :apples do
2124
+ mount app
2125
+ end
2126
+ get '/apples/colour'
2127
+ last_response.status.should eql 200
2128
+ last_response.body.should == 'red'
2129
+ options '/apples/colour'
2130
+ last_response.status.should eql 204
2131
+ get '/apples/pears/colour'
2132
+ last_response.status.should eql 200
2133
+ last_response.body.should == 'green'
2134
+ options '/apples/pears/colour'
2135
+ last_response.status.should eql 204
2136
+ end
2137
+
2138
+ it 'responds to options with path versioning' do
2139
+ subject.version 'v1', using: :path
2140
+ subject.namespace :apples do
2141
+ app = Class.new(Grape::API)
2142
+ app.get('/colour') do
2143
+ "red"
2144
+ end
2145
+ mount app
2146
+ end
2147
+
2148
+ get '/v1/apples/colour'
2149
+ last_response.status.should eql 200
2150
+ last_response.body.should == 'red'
2151
+ options '/v1/apples/colour'
2152
+ last_response.status.should eql 204
1864
2153
  end
1865
2154
 
1866
2155
  end
@@ -1932,7 +2221,7 @@ describe Grape::API do
1932
2221
  subject.get '/description' do
1933
2222
  route.route_description
1934
2223
  end
1935
- subject.desc 'returns parameters', { params: { "x" => "y" } }
2224
+ subject.desc 'returns parameters', params: { "x" => "y" }
1936
2225
  subject.get '/params/:id' do
1937
2226
  route.route_params[params[:id]]
1938
2227
  end
@@ -1965,7 +2254,7 @@ describe Grape::API do
1965
2254
  last_response.body.should == { meaning_of_life: 42 }.to_json
1966
2255
  end
1967
2256
  it 'forces txt from a non-accepting header' do
1968
- get '/meaning_of_life', {}, { 'HTTP_ACCEPT' => 'application/json' }
2257
+ get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json'
1969
2258
  last_response.body.should == { meaning_of_life: 42 }.to_s
1970
2259
  end
1971
2260
  end
@@ -1985,7 +2274,7 @@ describe Grape::API do
1985
2274
  last_response.body.should == { meaning_of_life: 42 }.to_s
1986
2275
  end
1987
2276
  it 'forces txt from a non-accepting header' do
1988
- get '/meaning_of_life', {}, { 'HTTP_ACCEPT' => 'application/json' }
2277
+ get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json'
1989
2278
  last_response.body.should == { meaning_of_life: 42 }.to_s
1990
2279
  end
1991
2280
  end
@@ -2006,7 +2295,7 @@ describe Grape::API do
2006
2295
  last_response.body.should == { meaning_of_life: 42 }.to_s
2007
2296
  end
2008
2297
  it 'forces json from a non-accepting header' do
2009
- get '/meaning_of_life', {}, { 'HTTP_ACCEPT' => 'text/html' }
2298
+ get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html'
2010
2299
  last_response.body.should == { meaning_of_life: 42 }.to_json
2011
2300
  end
2012
2301
  it 'can be overwritten with an explicit content type' do