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
@@ -9,7 +9,7 @@ describe Grape::Endpoint do
9
9
 
10
10
  describe '#initialize' do
11
11
  it 'takes a settings stack, options, and a block' do
12
- p = proc { }
12
+ p = proc {}
13
13
  expect {
14
14
  Grape::Endpoint.new(Grape::Util::HashStack.new, {
15
15
  path: '/',
@@ -64,7 +64,7 @@ describe Grape::Endpoint do
64
64
  }
65
65
  end
66
66
  it 'includes additional request headers' do
67
- get '/headers', nil, { "HTTP_X_GRAPE_CLIENT" => "1" }
67
+ get '/headers', nil, "HTTP_X_GRAPE_CLIENT" => "1"
68
68
  JSON.parse(last_response.body)["X-Grape-Client"].should == "1"
69
69
  end
70
70
  it 'includes headers passed as symbols' do
@@ -141,7 +141,7 @@ describe Grape::Endpoint do
141
141
  cookie = cookies[cookie_name]
142
142
  cookie.should_not be_nil
143
143
  cookie.value.should == "deleted"
144
- cookie.expired?.should be_true
144
+ cookie.expired?.should be true
145
145
  end
146
146
  end
147
147
 
@@ -150,7 +150,7 @@ describe Grape::Endpoint do
150
150
  sum = 0
151
151
  cookies.each do |name, val|
152
152
  sum += val.to_i
153
- cookies.delete name, { path: '/test' }
153
+ cookies.delete name, path: '/test'
154
154
  end
155
155
  sum
156
156
  end
@@ -166,7 +166,7 @@ describe Grape::Endpoint do
166
166
  cookie.should_not be_nil
167
167
  cookie.value.should == "deleted"
168
168
  cookie.path.should == "/test"
169
- cookie.expired?.should be_true
169
+ cookie.expired?.should be true
170
170
  end
171
171
  end
172
172
  end
@@ -177,7 +177,7 @@ describe Grape::Endpoint do
177
177
  requires :first
178
178
  optional :second
179
179
  optional :third, default: 'third-default'
180
- group :nested do
180
+ optional :nested, type: Hash do
181
181
  optional :fourth
182
182
  end
183
183
  end
@@ -214,6 +214,16 @@ describe Grape::Endpoint do
214
214
  end
215
215
 
216
216
  it 'builds nested params when given array' do
217
+ subject.get '/dummy' do
218
+ end
219
+ subject.params do
220
+ requires :first
221
+ optional :second
222
+ optional :third, default: 'third-default'
223
+ optional :nested, type: Array do
224
+ optional :fourth
225
+ end
226
+ end
217
227
  subject.get '/declared' do
218
228
  declared(params)[:nested].size.should == 2
219
229
  ""
@@ -254,6 +264,55 @@ describe Grape::Endpoint do
254
264
  end
255
265
  end
256
266
 
267
+ describe '#declared; call from child namespace' do
268
+ before do
269
+ subject.format :json
270
+ subject.namespace :something do
271
+ params do
272
+ requires :id, type: Integer
273
+ end
274
+ resource ':id' do
275
+ params do
276
+ requires :foo
277
+ optional :bar
278
+ end
279
+ get do
280
+ {
281
+ params: params,
282
+ declared_params: declared(params)
283
+ }
284
+ end
285
+ params do
286
+ requires :happy
287
+ optional :days
288
+ end
289
+ get '/test' do
290
+ {
291
+ params: params,
292
+ declared_params: declared(params, include_parent_namespaces: false)
293
+ }
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ it 'should include params defined in the parent namespace' do
300
+ get '/something/123', foo: 'test', extra: 'hello'
301
+ expect(last_response.status).to eq 200
302
+ json = JSON.parse(last_response.body, symbolize_names: true)
303
+ expect(json[:params][:id]).to eq 123
304
+ expect(json[:declared_params].keys).to match_array [:foo, :bar, :id]
305
+ end
306
+
307
+ it 'does not include params defined in the parent namespace with include_parent_namespaces: false' do
308
+ get '/something/123/test', happy: 'test', extra: 'hello'
309
+ expect(last_response.status).to eq 200
310
+ json = JSON.parse(last_response.body, symbolize_names: true)
311
+ expect(json[:params][:id]).to eq 123
312
+ expect(json[:declared_params].keys).to match_array [:happy, :days]
313
+ end
314
+ end
315
+
257
316
  describe '#params' do
258
317
  it 'is available to the caller' do
259
318
  subject.get('/hey') do
@@ -354,22 +413,22 @@ describe Grape::Endpoint do
354
413
  end
355
414
 
356
415
  it 'converts JSON bodies to params' do
357
- post '/request_body', MultiJson.dump(user: 'Bobby T.'), { 'CONTENT_TYPE' => 'application/json' }
416
+ post '/request_body', MultiJson.dump(user: 'Bobby T.'), 'CONTENT_TYPE' => 'application/json'
358
417
  last_response.body.should == 'Bobby T.'
359
418
  end
360
419
 
361
420
  it 'does not convert empty JSON bodies to params' do
362
- put '/request_body', '', { 'CONTENT_TYPE' => 'application/json' }
421
+ put '/request_body', '', 'CONTENT_TYPE' => 'application/json'
363
422
  last_response.body.should == ''
364
423
  end
365
424
 
366
425
  it 'converts XML bodies to params' do
367
- post '/request_body', '<user>Bobby T.</user>', { 'CONTENT_TYPE' => 'application/xml' }
426
+ post '/request_body', '<user>Bobby T.</user>', 'CONTENT_TYPE' => 'application/xml'
368
427
  last_response.body.should == 'Bobby T.'
369
428
  end
370
429
 
371
430
  it 'converts XML bodies to params' do
372
- put '/request_body', '<user>Bobby T.</user>', { 'CONTENT_TYPE' => 'application/xml' }
431
+ put '/request_body', '<user>Bobby T.</user>', 'CONTENT_TYPE' => 'application/xml'
373
432
  last_response.body.should == 'Bobby T.'
374
433
  end
375
434
 
@@ -378,7 +437,7 @@ describe Grape::Endpoint do
378
437
  error! 400, "expected nil" if params[:version]
379
438
  params[:user]
380
439
  end
381
- post '/omitted_params', MultiJson.dump(user: 'Bob'), { 'CONTENT_TYPE' => 'application/json' }
440
+ post '/omitted_params', MultiJson.dump(user: 'Bob'), 'CONTENT_TYPE' => 'application/json'
382
441
  last_response.status.should == 201
383
442
  last_response.body.should == "Bob"
384
443
  end
@@ -390,11 +449,70 @@ describe Grape::Endpoint do
390
449
  subject.put '/request_body' do
391
450
  params[:user]
392
451
  end
393
- put '/request_body', '<user>Bobby T.</user>', { 'CONTENT_TYPE' => 'application/xml' }
452
+ put '/request_body', '<user>Bobby T.</user>', 'CONTENT_TYPE' => 'application/xml'
394
453
  last_response.status.should == 406
395
454
  last_response.body.should == '{"error":"The requested content-type \'application/xml\' is not supported."}'
396
455
  end
397
456
 
457
+ context 'content type with params' do
458
+ before do
459
+ subject.format :json
460
+ subject.content_type :json, 'application/json; charset=utf-8'
461
+
462
+ subject.post do
463
+ params[:data]
464
+ end
465
+ post '/', MultiJson.dump(data: { some: 'payload' }), 'CONTENT_TYPE' => 'application/json'
466
+ end
467
+
468
+ it "should not response with 406 for same type without params" do
469
+ last_response.status.should_not be 406
470
+ end
471
+
472
+ it "should response with given content type in headers" do
473
+ last_response.headers['Content-Type'].should eq 'application/json; charset=utf-8'
474
+ end
475
+
476
+ end
477
+
478
+ context 'precedence' do
479
+
480
+ before do
481
+ subject.format :json
482
+ subject.namespace '/:id' do
483
+ get do
484
+ {
485
+ params: params[:id]
486
+ }
487
+ end
488
+ post do
489
+ {
490
+ params: params[:id]
491
+ }
492
+ end
493
+ put do
494
+ {
495
+ params: params[:id]
496
+ }
497
+ end
498
+ end
499
+ end
500
+
501
+ it 'route string params have higher precedence than body params' do
502
+ post '/123', { id: 456 }.to_json
503
+ expect(JSON.parse(last_response.body)['params']).to eq '123'
504
+ put '/123', { id: 456 }.to_json
505
+ expect(JSON.parse(last_response.body)['params']).to eq '123'
506
+ end
507
+
508
+ it 'route string params have higher precedence than URL params' do
509
+ get '/123?id=456'
510
+ expect(JSON.parse(last_response.body)['params']).to eq '123'
511
+ post '/123?id=456'
512
+ expect(JSON.parse(last_response.body)['params']).to eq '123'
513
+ end
514
+ end
515
+
398
516
  end
399
517
 
400
518
  describe '#error!' do
@@ -405,7 +523,7 @@ describe Grape::Endpoint do
405
523
  end
406
524
 
407
525
  get '/hey'
408
- last_response.status.should == 403
526
+ last_response.status.should == 500
409
527
  last_response.body.should == "This is not valid."
410
528
  end
411
529
 
@@ -428,6 +546,16 @@ describe Grape::Endpoint do
428
546
  last_response.status.should == 403
429
547
  last_response.body.should == '{"dude":"rad"}'
430
548
  end
549
+
550
+ it 'can specifiy headers' do
551
+ subject.get '/hey' do
552
+ error!({ 'dude' => 'rad' }, 403, 'X-Custom' => 'value')
553
+ end
554
+
555
+ get '/hey.json'
556
+ last_response.status.should == 403
557
+ last_response.headers['X-Custom'].should == 'value'
558
+ end
431
559
  end
432
560
 
433
561
  describe '#redirect' do
@@ -503,7 +631,7 @@ describe Grape::Endpoint do
503
631
  describe '.generate_api_method' do
504
632
  it 'raises NameError if the method name is already in use' do
505
633
  expect {
506
- Grape::Endpoint.generate_api_method("version", &proc { })
634
+ Grape::Endpoint.generate_api_method("version", &proc {})
507
635
  }.to raise_error(NameError)
508
636
  end
509
637
  it 'raises ArgumentError if a block is not given' do
@@ -512,7 +640,7 @@ describe Grape::Endpoint do
512
640
  }.to raise_error(ArgumentError)
513
641
  end
514
642
  it 'returns a Proc' do
515
- Grape::Endpoint.generate_api_method("GET test for a proc", &proc { }).should be_a Proc
643
+ Grape::Endpoint.generate_api_method("GET test for a proc", &proc {}).should be_a Proc
516
644
  end
517
645
  end
518
646
 
@@ -604,4 +732,23 @@ describe Grape::Endpoint do
604
732
  end
605
733
  end
606
734
 
735
+ context 'version headers' do
736
+ before do
737
+ # NOTE: a 404 is returned instead of the 406 if cascade: false is not set.
738
+ subject.version 'v1', using: :header, vendor: 'ohanapi', cascade: false
739
+ subject.get '/test' do
740
+ "Hello!"
741
+ end
742
+ end
743
+
744
+ it 'result in a 406 response if they are invalid' do
745
+ get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json'
746
+ last_response.status.should == 406
747
+ end
748
+
749
+ it 'result in a 406 response if they cannot be parsed by rack-accept' do
750
+ get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json; version=1'
751
+ last_response.status.should == 406
752
+ end
753
+ end
607
754
  end
@@ -109,6 +109,31 @@ describe Grape::Entity do
109
109
  last_response.body.should == 'Auto-detect!'
110
110
  end
111
111
 
112
+ it 'does not run autodetection for Entity when explicitely provided' do
113
+ entity = Class.new(Grape::Entity)
114
+ some_array = Array.new
115
+
116
+ subject.get '/example' do
117
+ present some_array, with: entity
118
+ end
119
+
120
+ some_array.should_not_receive(:first)
121
+ get '/example'
122
+ end
123
+
124
+ it 'autodetection does not use Entity if it is not a presenter' do
125
+ some_model = Class.new
126
+ entity = Class.new
127
+
128
+ some_model.class.const_set :Entity, entity
129
+
130
+ subject.get '/example' do
131
+ present some_model
132
+ end
133
+ get '/example'
134
+ entity.should_not_receive(:represent)
135
+ end
136
+
112
137
  it 'adds a root key to the output if one is given' do
113
138
  subject.get '/example' do
114
139
  present({ abc: 'def' }, root: :root)
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe Grape::Exceptions::ValidationErrors do
5
+ let(:validation_message) { "FooBar is invalid" }
6
+ let(:validation_error) { OpenStruct.new(param: validation_message) }
7
+
8
+ context "message" do
9
+ context "is not repeated" do
10
+ let(:error) do
11
+ described_class.new(errors: [validation_error, validation_error])
12
+ end
13
+ subject(:message) { error.message.split(',').map(&:strip) }
14
+
15
+ it { expect(message).to include validation_message }
16
+ it { expect(message.size).to eq 1 }
17
+ end
18
+ end
19
+ end
@@ -5,7 +5,7 @@ describe Grape::Middleware::Auth::OAuth2 do
5
5
  attr_accessor :token
6
6
 
7
7
  def self.verify(token)
8
- FakeToken.new(token) if %w(g e).include?(token[0..0])
8
+ FakeToken.new(token) if !!token && %w(g e).include?(token[0..0])
9
9
  end
10
10
 
11
11
  def initialize(token)
@@ -30,7 +30,7 @@ describe Grape::Middleware::Auth::OAuth2 do
30
30
 
31
31
  context 'with the token in the query string' do
32
32
  context 'and a valid token' do
33
- before { get '/awesome?oauth_token=g123' }
33
+ before { get '/awesome?access_token=g123' }
34
34
 
35
35
  it 'sets env["api.token"]' do
36
36
  last_response.body.should == 'g123'
@@ -40,7 +40,7 @@ describe Grape::Middleware::Auth::OAuth2 do
40
40
  context 'and an invalid token' do
41
41
  before do
42
42
  @err = catch :error do
43
- get '/awesome?oauth_token=b123'
43
+ get '/awesome?access_token=b123'
44
44
  end
45
45
  end
46
46
 
@@ -49,7 +49,7 @@ describe Grape::Middleware::Auth::OAuth2 do
49
49
  end
50
50
 
51
51
  it 'sets the WWW-Authenticate header in the response' do
52
- @err[:headers]['WWW-Authenticate'].should == "OAuth realm='OAuth API', error='invalid_token'"
52
+ @err[:headers]['WWW-Authenticate'].should == "OAuth realm='OAuth API', error='invalid_grant'"
53
53
  end
54
54
  end
55
55
  end
@@ -57,34 +57,79 @@ describe Grape::Middleware::Auth::OAuth2 do
57
57
  context 'with an expired token' do
58
58
  before do
59
59
  @err = catch :error do
60
- get '/awesome?oauth_token=e123'
60
+ get '/awesome?access_token=e123'
61
61
  end
62
62
  end
63
63
 
64
- it { @err[:status].should == 401 }
65
- it { @err[:headers]['WWW-Authenticate'].should == "OAuth realm='OAuth API', error='expired_token'" }
64
+ it 'throws an error' do
65
+ @err[:status].should == 401
66
+ end
67
+
68
+ it 'sets the WWW-Authenticate header in the response to error' do
69
+ @err[:headers]['WWW-Authenticate'].should == "OAuth realm='OAuth API', error='invalid_grant'"
70
+ end
66
71
  end
67
72
 
68
73
  %w(HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION REDIRECT_X_HTTP_AUTHORIZATION).each do |head|
69
- context 'with the token in the #{head} header' do
70
- before { get '/awesome', {}, head => 'OAuth g123' }
71
- it { last_response.body.should == 'g123' }
74
+ context "with the token in the #{head} header" do
75
+ before do
76
+ get '/awesome', {}, head => 'OAuth g123'
77
+ end
78
+
79
+ it 'sets env["api.token"]' do
80
+ last_response.body.should == 'g123'
81
+ end
72
82
  end
73
83
  end
74
84
 
75
85
  context 'with the token in the POST body' do
76
- before { post '/awesome', { 'oauth_token' => 'g123' } }
77
- it { last_response.body.should == 'g123' }
86
+ before do
87
+ post '/awesome', 'access_token' => 'g123'
88
+ end
89
+
90
+ it 'sets env["api.token"]' do
91
+ last_response.body.should == 'g123'
92
+ end
78
93
  end
79
94
 
80
95
  context 'when accessing something outside its scope' do
81
96
  before do
82
97
  @err = catch :error do
83
- get '/forbidden?oauth_token=g123'
98
+ get '/forbidden?access_token=g123'
99
+ end
100
+ end
101
+
102
+ it 'throws an error' do
103
+ @err[:status].should == 403
104
+ end
105
+
106
+ it 'sets the WWW-Authenticate header in the response to error' do
107
+ @err[:headers]['WWW-Authenticate'].should == "OAuth realm='OAuth API', error='insufficient_scope'"
108
+ end
109
+ end
110
+
111
+ context 'when authorization is not required' do
112
+ def app
113
+ Rack::Builder.app do
114
+ use Grape::Middleware::Auth::OAuth2, token_class: 'FakeToken', required: false
115
+ run lambda { |env| [200, {}, [(env['api.token'].token if env['api.token'])]] }
84
116
  end
85
117
  end
86
118
 
87
- it { @err[:headers]['WWW-Authenticate'].should == "OAuth realm='OAuth API', error='insufficient_scope'" }
88
- it { @err[:status].should == 403 }
119
+ context 'with no token' do
120
+ before { post '/awesome' }
121
+
122
+ it 'succeeds anyway' do
123
+ last_response.status.should == 200
124
+ end
125
+ end
126
+
127
+ context 'with a valid token' do
128
+ before { get '/awesome?access_token=g123' }
129
+
130
+ it 'sets env["api.token"]' do
131
+ last_response.body.should == 'g123'
132
+ end
133
+ end
89
134
  end
90
135
  end