grape 0.3.0 → 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.
- checksums.yaml +15 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +70 -0
- data/.travis.yml +7 -6
- data/CHANGELOG.md +134 -4
- data/CONTRIBUTING.md +118 -0
- data/Gemfile +5 -2
- data/README.md +551 -116
- data/RELEASING.md +105 -0
- data/Rakefile +29 -8
- data/UPGRADING.md +124 -0
- data/grape.gemspec +3 -3
- data/lib/grape/api.rb +207 -88
- data/lib/grape/cookies.rb +4 -8
- data/lib/grape/endpoint.rb +198 -144
- data/lib/grape/error_formatter/base.rb +5 -7
- data/lib/grape/error_formatter/json.rb +3 -5
- data/lib/grape/error_formatter/txt.rb +1 -3
- data/lib/grape/error_formatter/xml.rb +4 -6
- data/lib/grape/exceptions/base.rb +9 -9
- data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
- data/lib/grape/exceptions/invalid_formatter.rb +1 -4
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -5
- data/lib/grape/exceptions/invalid_with_option_for_represent.rb +1 -6
- data/lib/grape/exceptions/missing_mime_type.rb +1 -5
- data/lib/grape/exceptions/missing_option.rb +1 -4
- data/lib/grape/exceptions/missing_vendor_option.rb +1 -4
- data/lib/grape/exceptions/unknown_options.rb +1 -5
- data/lib/grape/exceptions/unknown_validator.rb +1 -3
- data/lib/grape/exceptions/validation.rb +13 -3
- data/lib/grape/exceptions/validation_errors.rb +43 -0
- data/lib/grape/formatter/base.rb +5 -7
- data/lib/grape/formatter/json.rb +0 -3
- data/lib/grape/formatter/serializable_hash.rb +15 -15
- data/lib/grape/formatter/txt.rb +0 -2
- data/lib/grape/formatter/xml.rb +0 -2
- data/lib/grape/http/request.rb +26 -0
- data/lib/grape/locale/en.yml +8 -5
- data/lib/grape/middleware/auth/base.rb +30 -0
- data/lib/grape/middleware/auth/basic.rb +3 -20
- data/lib/grape/middleware/auth/digest.rb +2 -19
- data/lib/grape/middleware/auth/oauth2.rb +31 -24
- data/lib/grape/middleware/base.rb +7 -7
- data/lib/grape/middleware/error.rb +36 -22
- data/lib/grape/middleware/filter.rb +3 -3
- data/lib/grape/middleware/formatter.rb +99 -61
- data/lib/grape/middleware/globals.rb +13 -0
- data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
- data/lib/grape/middleware/versioner/header.rb +22 -16
- data/lib/grape/middleware/versioner/param.rb +9 -11
- data/lib/grape/middleware/versioner/path.rb +10 -13
- data/lib/grape/middleware/versioner.rb +3 -1
- data/lib/grape/namespace.rb +23 -0
- data/lib/grape/parser/base.rb +3 -5
- data/lib/grape/parser/json.rb +0 -2
- data/lib/grape/parser/xml.rb +0 -2
- data/lib/grape/path.rb +70 -0
- data/lib/grape/route.rb +10 -6
- data/lib/grape/util/content_types.rb +2 -1
- data/lib/grape/util/deep_merge.rb +5 -5
- data/lib/grape/util/hash_stack.rb +13 -2
- data/lib/grape/validations/coerce.rb +11 -10
- data/lib/grape/validations/default.rb +25 -0
- data/lib/grape/validations/presence.rb +7 -3
- data/lib/grape/validations/regexp.rb +2 -5
- data/lib/grape/validations/values.rb +17 -0
- data/lib/grape/validations.rb +161 -54
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +19 -4
- data/spec/grape/api_spec.rb +897 -268
- data/spec/grape/endpoint_spec.rb +283 -66
- data/spec/grape/entity_spec.rb +132 -29
- data/spec/grape/exceptions/missing_mime_type_spec.rb +3 -9
- data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
- data/spec/grape/middleware/auth/basic_spec.rb +8 -8
- data/spec/grape/middleware/auth/digest_spec.rb +5 -5
- data/spec/grape/middleware/auth/oauth2_spec.rb +81 -36
- data/spec/grape/middleware/base_spec.rb +8 -13
- data/spec/grape/middleware/error_spec.rb +13 -17
- data/spec/grape/middleware/exception_spec.rb +47 -27
- data/spec/grape/middleware/formatter_spec.rb +103 -41
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
- data/spec/grape/middleware/versioner/header_spec.rb +76 -51
- data/spec/grape/middleware/versioner/param_spec.rb +18 -18
- data/spec/grape/middleware/versioner/path_spec.rb +6 -6
- data/spec/grape/middleware/versioner_spec.rb +5 -2
- data/spec/grape/path_spec.rb +229 -0
- data/spec/grape/util/hash_stack_spec.rb +31 -32
- data/spec/grape/validations/coerce_spec.rb +116 -51
- data/spec/grape/validations/default_spec.rb +123 -0
- data/spec/grape/validations/presence_spec.rb +42 -44
- data/spec/grape/validations/regexp_spec.rb +9 -9
- data/spec/grape/validations/values_spec.rb +138 -0
- data/spec/grape/validations/zh-CN.yml +4 -3
- data/spec/grape/validations_spec.rb +681 -48
- data/spec/shared/versioning_examples.rb +22 -6
- data/spec/spec_helper.rb +3 -2
- data/spec/support/basic_auth_encode_helpers.rb +0 -1
- data/spec/support/content_type_helpers.rb +11 -0
- data/spec/support/versioned_helpers.rb +13 -5
- metadata +34 -84
data/spec/grape/api_spec.rb
CHANGED
@@ -4,7 +4,9 @@ require 'shared/versioning_examples'
|
|
4
4
|
describe Grape::API do
|
5
5
|
subject { Class.new(Grape::API) }
|
6
6
|
|
7
|
-
def app
|
7
|
+
def app
|
8
|
+
subject
|
9
|
+
end
|
8
10
|
|
9
11
|
describe '.prefix' do
|
10
12
|
|
@@ -52,17 +54,18 @@ describe Grape::API do
|
|
52
54
|
it_should_behave_like 'versioning' do
|
53
55
|
let(:macro_options) do
|
54
56
|
{
|
55
|
-
:
|
57
|
+
using: :path
|
56
58
|
}
|
57
59
|
end
|
58
60
|
end
|
59
61
|
end
|
62
|
+
|
60
63
|
describe '.version using param' do
|
61
64
|
it_should_behave_like 'versioning' do
|
62
65
|
let(:macro_options) do
|
63
66
|
{
|
64
|
-
:
|
65
|
-
:
|
67
|
+
using: :param,
|
68
|
+
parameter: "apiver"
|
66
69
|
}
|
67
70
|
end
|
68
71
|
end
|
@@ -72,9 +75,9 @@ describe Grape::API do
|
|
72
75
|
it_should_behave_like 'versioning' do
|
73
76
|
let(:macro_options) do
|
74
77
|
{
|
75
|
-
:
|
76
|
-
:
|
77
|
-
:
|
78
|
+
using: :header,
|
79
|
+
vendor: 'mycompany',
|
80
|
+
format: 'json'
|
78
81
|
}
|
79
82
|
end
|
80
83
|
end
|
@@ -82,7 +85,7 @@ describe Grape::API do
|
|
82
85
|
# Behavior as defined by rfc2616 when no header is defined
|
83
86
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
84
87
|
describe 'no specified accept header' do
|
85
|
-
# subject.version 'v1', :
|
88
|
+
# subject.version 'v1', using: :header
|
86
89
|
# subject.get '/hello' do
|
87
90
|
# 'hello'
|
88
91
|
# end
|
@@ -93,22 +96,29 @@ describe Grape::API do
|
|
93
96
|
# end
|
94
97
|
end
|
95
98
|
|
96
|
-
|
99
|
+
# pending 'routes if any media type is allowed'
|
100
|
+
end
|
97
101
|
|
102
|
+
describe '.version using accept_version_header' do
|
103
|
+
it_should_behave_like 'versioning' do
|
104
|
+
let(:macro_options) do
|
105
|
+
{
|
106
|
+
using: :accept_version_header
|
107
|
+
}
|
108
|
+
end
|
98
109
|
end
|
99
110
|
end
|
100
111
|
|
101
112
|
describe '.represent' do
|
102
113
|
it 'requires a :with option' do
|
103
|
-
expect{ subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent)
|
114
|
+
expect { subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent)
|
104
115
|
end
|
105
116
|
|
106
117
|
it 'adds the association to the :representations setting' do
|
107
118
|
klass = Class.new
|
108
|
-
subject.represent Object, :
|
119
|
+
subject.represent Object, with: klass
|
109
120
|
subject.settings[:representations][Object].should == klass
|
110
121
|
end
|
111
|
-
|
112
122
|
end
|
113
123
|
|
114
124
|
describe '.namespace' do
|
@@ -120,10 +130,10 @@ describe Grape::API do
|
|
120
130
|
|
121
131
|
it 'comes after the prefix and version' do
|
122
132
|
subject.prefix :rad
|
123
|
-
subject.version 'v1', :
|
133
|
+
subject.version 'v1', using: :path
|
124
134
|
|
125
135
|
subject.namespace :awesome do
|
126
|
-
get('/hello'){ "worked" }
|
136
|
+
get('/hello') { "worked" }
|
127
137
|
end
|
128
138
|
|
129
139
|
get "/rad/v1/awesome/hello"
|
@@ -163,10 +173,10 @@ describe Grape::API do
|
|
163
173
|
|
164
174
|
it 'is callable with nil just to push onto the stack' do
|
165
175
|
subject.namespace do
|
166
|
-
version 'v2', :
|
167
|
-
get('/hello'){ "inner" }
|
176
|
+
version 'v2', using: :path
|
177
|
+
get('/hello') { "inner" }
|
168
178
|
end
|
169
|
-
subject.get('/hello'){ "outer" }
|
179
|
+
subject.get('/hello') { "outer" }
|
170
180
|
|
171
181
|
get '/v2/hello'
|
172
182
|
last_response.body.should == "inner"
|
@@ -183,13 +193,42 @@ describe Grape::API do
|
|
183
193
|
end
|
184
194
|
end
|
185
195
|
|
196
|
+
describe '.route_param' do
|
197
|
+
it 'adds a parameterized route segment namespace' do
|
198
|
+
subject.namespace :users do
|
199
|
+
route_param :id do
|
200
|
+
get do
|
201
|
+
params[:id]
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
get '/users/23'
|
207
|
+
last_response.body.should == '23'
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'should be able to define requirements with a single hash' do
|
211
|
+
subject.namespace :users do
|
212
|
+
route_param :id, requirements: /[0-9]+/ do
|
213
|
+
get do
|
214
|
+
params[:id]
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
get '/users/michael'
|
220
|
+
last_response.status.should == 404
|
221
|
+
get '/users/23'
|
222
|
+
last_response.status.should == 200
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
186
226
|
describe '.route' do
|
187
227
|
it 'allows for no path' do
|
188
228
|
subject.namespace :votes do
|
189
229
|
get do
|
190
230
|
"Votes"
|
191
231
|
end
|
192
|
-
|
193
232
|
post do
|
194
233
|
"Created a Vote"
|
195
234
|
end
|
@@ -201,25 +240,32 @@ describe Grape::API do
|
|
201
240
|
last_response.body.should eql 'Created a Vote'
|
202
241
|
end
|
203
242
|
|
243
|
+
it 'handles empty calls' do
|
244
|
+
subject.get "/"
|
245
|
+
get "/"
|
246
|
+
last_response.body.should eql ""
|
247
|
+
end
|
248
|
+
|
204
249
|
describe 'root routes should work with' do
|
205
250
|
before do
|
251
|
+
subject.format :txt
|
206
252
|
def subject.enable_root_route!
|
207
|
-
|
253
|
+
get("/") { "root" }
|
208
254
|
end
|
209
255
|
end
|
210
256
|
|
211
257
|
after do
|
212
|
-
last_response.body.should eql
|
258
|
+
last_response.body.should eql "root"
|
213
259
|
end
|
214
260
|
|
215
261
|
describe 'path versioned APIs' do
|
216
262
|
before do
|
217
|
-
subject.version 'v1', :
|
263
|
+
subject.version 'v1', using: :path
|
218
264
|
subject.enable_root_route!
|
219
265
|
end
|
220
266
|
|
221
267
|
it 'without a format' do
|
222
|
-
versioned_get "/", "v1", :
|
268
|
+
versioned_get "/", "v1", using: :path
|
223
269
|
end
|
224
270
|
|
225
271
|
it 'with a format' do
|
@@ -228,17 +274,32 @@ describe Grape::API do
|
|
228
274
|
end
|
229
275
|
|
230
276
|
it 'header versioned APIs' do
|
231
|
-
subject.version 'v1', :
|
277
|
+
subject.version 'v1', using: :header, vendor: 'test'
|
232
278
|
subject.enable_root_route!
|
233
279
|
|
234
|
-
versioned_get "/", "v1", :
|
280
|
+
versioned_get "/", "v1", using: :header, vendor: 'test'
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'header versioned APIs with multiple headers' do
|
284
|
+
subject.version ['v1', 'v2'], using: :header, vendor: 'test'
|
285
|
+
subject.enable_root_route!
|
286
|
+
|
287
|
+
versioned_get "/", "v1", using: :header, vendor: 'test'
|
288
|
+
versioned_get "/", "v2", using: :header, vendor: 'test'
|
235
289
|
end
|
236
290
|
|
237
291
|
it 'param versioned APIs' do
|
238
|
-
subject.version 'v1', :
|
292
|
+
subject.version 'v1', using: :param
|
239
293
|
subject.enable_root_route!
|
240
294
|
|
241
|
-
versioned_get "/", "v1", :
|
295
|
+
versioned_get "/", "v1", using: :param
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'Accept-Version header versioned APIs' do
|
299
|
+
subject.version 'v1', using: :accept_version_header
|
300
|
+
subject.enable_root_route!
|
301
|
+
|
302
|
+
versioned_get "/", "v1", using: :accept_version_header
|
242
303
|
end
|
243
304
|
|
244
305
|
it 'unversioned APIs' do
|
@@ -262,7 +323,7 @@ describe Grape::API do
|
|
262
323
|
context 'format' do
|
263
324
|
before(:each) do
|
264
325
|
subject.get("/abc") do
|
265
|
-
RSpec::Mocks::Mock.new(:
|
326
|
+
RSpec::Mocks::Mock.new(to_json: 'abc', to_txt: 'def')
|
266
327
|
end
|
267
328
|
end
|
268
329
|
|
@@ -281,7 +342,7 @@ describe Grape::API do
|
|
281
342
|
|
282
343
|
it 'allows for format without corrupting a param' do
|
283
344
|
subject.get('/:id') do
|
284
|
-
{"id" => params[:id]}
|
345
|
+
{ "id" => params[:id] }
|
285
346
|
end
|
286
347
|
|
287
348
|
get '/awesome.json'
|
@@ -314,6 +375,43 @@ describe Grape::API do
|
|
314
375
|
last_response.body.should eql 'hiya'
|
315
376
|
end
|
316
377
|
|
378
|
+
[:put, :post].each do |verb|
|
379
|
+
context verb do
|
380
|
+
['string', :symbol, 1, -1.1, {}, [], true, false, nil].each do |object|
|
381
|
+
it "allows a(n) #{object.class} json object in params" do
|
382
|
+
subject.format :json
|
383
|
+
subject.send(verb) do
|
384
|
+
env['api.request.body']
|
385
|
+
end
|
386
|
+
send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json'
|
387
|
+
last_response.status.should == (verb == :post ? 201 : 200)
|
388
|
+
last_response.body.should eql MultiJson.dump(object)
|
389
|
+
last_request.params.should eql Hash.new
|
390
|
+
end
|
391
|
+
it "stores input in api.request.input" do
|
392
|
+
subject.format :json
|
393
|
+
subject.send(verb) do
|
394
|
+
env['api.request.input']
|
395
|
+
end
|
396
|
+
send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json'
|
397
|
+
last_response.status.should == (verb == :post ? 201 : 200)
|
398
|
+
last_response.body.should eql MultiJson.dump(object).to_json
|
399
|
+
end
|
400
|
+
context "chunked transfer encoding" do
|
401
|
+
it "stores input in api.request.input" do
|
402
|
+
subject.format :json
|
403
|
+
subject.send(verb) do
|
404
|
+
env['api.request.input']
|
405
|
+
end
|
406
|
+
send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked', 'CONTENT_LENGTH' => nil
|
407
|
+
last_response.status.should == (verb == :post ? 201 : 200)
|
408
|
+
last_response.body.should eql MultiJson.dump(object).to_json
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
317
415
|
it 'allows for multipart paths' do
|
318
416
|
|
319
417
|
subject.route([:get, :post], '/:id/first') do
|
@@ -361,7 +459,7 @@ describe Grape::API do
|
|
361
459
|
last_response.body.should eql verb == 'head' ? '' : verb
|
362
460
|
# Call it with a method other than the properly constrained one.
|
363
461
|
send(used_verb = verbs[(verbs.index(verb) + 2) % verbs.size], '/example')
|
364
|
-
last_response.status.should eql used_verb == 'options' ? 204 :405
|
462
|
+
last_response.status.should eql used_verb == 'options' ? 204 : 405
|
365
463
|
end
|
366
464
|
end
|
367
465
|
|
@@ -375,13 +473,15 @@ describe Grape::API do
|
|
375
473
|
last_response.body.should eql 'Created'
|
376
474
|
end
|
377
475
|
|
378
|
-
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' }
|
379
478
|
subject.get 'example' do
|
380
479
|
"example"
|
381
480
|
end
|
382
481
|
put '/example'
|
383
482
|
last_response.status.should eql 405
|
384
483
|
last_response.body.should eql ''
|
484
|
+
last_response.headers['X-Custom-Header'].should eql 'foo'
|
385
485
|
end
|
386
486
|
|
387
487
|
specify '405 responses includes an Allow header specifying supported methods' do
|
@@ -395,7 +495,19 @@ describe Grape::API do
|
|
395
495
|
last_response.headers['Allow'].should eql 'OPTIONS, GET, POST, HEAD'
|
396
496
|
end
|
397
497
|
|
398
|
-
|
498
|
+
specify '405 responses includes an Content-Type header' do
|
499
|
+
subject.get 'example' do
|
500
|
+
"example"
|
501
|
+
end
|
502
|
+
subject.post 'example' do
|
503
|
+
"example"
|
504
|
+
end
|
505
|
+
put '/example'
|
506
|
+
last_response.headers['Content-Type'].should eql 'text/plain'
|
507
|
+
end
|
508
|
+
|
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' }
|
399
511
|
subject.get 'example' do
|
400
512
|
"example"
|
401
513
|
end
|
@@ -403,6 +515,7 @@ describe Grape::API do
|
|
403
515
|
last_response.status.should eql 204
|
404
516
|
last_response.body.should eql ''
|
405
517
|
last_response.headers['Allow'].should eql 'OPTIONS, GET, HEAD'
|
518
|
+
last_response.headers['X-Custom-Header'].should eql 'foo'
|
406
519
|
end
|
407
520
|
|
408
521
|
it 'allows HEAD on a GET request' do
|
@@ -470,17 +583,42 @@ describe Grape::API do
|
|
470
583
|
last_response.body.should eql 'first second'
|
471
584
|
end
|
472
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
|
+
|
473
611
|
it 'adds a after_validation filter' do
|
474
|
-
subject.after_validation { @foo = "first #{params[:id]}:#{params[:id].class}" }
|
612
|
+
subject.after_validation { @foo = "first #{params[:id] }:#{params[:id].class}" }
|
475
613
|
subject.after_validation { @bar = 'second' }
|
476
614
|
subject.params do
|
477
|
-
requires :id, :
|
615
|
+
requires :id, type: Integer
|
478
616
|
end
|
479
617
|
subject.get '/' do
|
480
618
|
"#{@foo} #{@bar}"
|
481
619
|
end
|
482
620
|
|
483
|
-
get '/', :
|
621
|
+
get '/', id: "32"
|
484
622
|
last_response.body.should eql 'first 32:Fixnum second'
|
485
623
|
end
|
486
624
|
|
@@ -496,6 +634,94 @@ describe Grape::API do
|
|
496
634
|
get '/'
|
497
635
|
last_response.body.should eql 'default'
|
498
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
|
499
725
|
end
|
500
726
|
|
501
727
|
context 'format' do
|
@@ -532,6 +758,26 @@ describe Grape::API do
|
|
532
758
|
get '/error.xml'
|
533
759
|
last_response.headers['Content-Type'].should eql 'application/xml'
|
534
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
|
535
781
|
end
|
536
782
|
|
537
783
|
context 'custom middleware' do
|
@@ -555,18 +801,19 @@ describe Grape::API do
|
|
555
801
|
describe '.middleware' do
|
556
802
|
it 'includes middleware arguments from settings' do
|
557
803
|
settings = Grape::Util::HashStack.new
|
558
|
-
settings.stub
|
559
|
-
subject.stub
|
804
|
+
settings.stub(:stack).and_return([{ middleware: [[ApiSpec::PhonyMiddleware, 'abc', 123]] }])
|
805
|
+
subject.stub(:settings).and_return(settings)
|
560
806
|
subject.middleware.should eql [[ApiSpec::PhonyMiddleware, 'abc', 123]]
|
561
807
|
end
|
562
808
|
|
563
809
|
it 'includes all middleware from stacked settings' do
|
564
810
|
settings = Grape::Util::HashStack.new
|
565
|
-
settings.stub
|
566
|
-
{:
|
567
|
-
{:
|
811
|
+
settings.stub(:stack).and_return [
|
812
|
+
{ middleware: [[ApiSpec::PhonyMiddleware, 123], [ApiSpec::PhonyMiddleware, 'abc']] },
|
813
|
+
{ middleware: [[ApiSpec::PhonyMiddleware, 'foo']] }
|
568
814
|
]
|
569
|
-
|
815
|
+
|
816
|
+
subject.stub(:settings).and_return(settings)
|
570
817
|
|
571
818
|
subject.middleware.should eql [
|
572
819
|
[ApiSpec::PhonyMiddleware, 123],
|
@@ -586,7 +833,7 @@ describe Grape::API do
|
|
586
833
|
subject.use ApiSpec::PhonyMiddleware, 123
|
587
834
|
subject.namespace :awesome do
|
588
835
|
use ApiSpec::PhonyMiddleware, 'abc'
|
589
|
-
middleware.should == [[ApiSpec::PhonyMiddleware, 123],[ApiSpec::PhonyMiddleware, 'abc']]
|
836
|
+
middleware.should == [[ApiSpec::PhonyMiddleware, 123], [ApiSpec::PhonyMiddleware, 'abc']]
|
590
837
|
end
|
591
838
|
|
592
839
|
subject.middleware.should eql [[ApiSpec::PhonyMiddleware, 123]]
|
@@ -603,13 +850,13 @@ describe Grape::API do
|
|
603
850
|
end
|
604
851
|
|
605
852
|
it 'adds a block if one is given' do
|
606
|
-
block = lambda{
|
853
|
+
block = lambda {}
|
607
854
|
subject.use ApiSpec::PhonyMiddleware, &block
|
608
855
|
subject.middleware.should eql [[ApiSpec::PhonyMiddleware, block]]
|
609
856
|
end
|
610
857
|
|
611
858
|
it 'uses a block if one is given' do
|
612
|
-
block = lambda{
|
859
|
+
block = lambda {}
|
613
860
|
subject.use ApiSpec::PhonyMiddleware, &block
|
614
861
|
subject.get '/' do
|
615
862
|
env['phony.block'].inspect
|
@@ -620,7 +867,7 @@ describe Grape::API do
|
|
620
867
|
end
|
621
868
|
|
622
869
|
it 'does not destroy the middleware settings on multiple runs' do
|
623
|
-
block = lambda{
|
870
|
+
block = lambda {}
|
624
871
|
subject.use ApiSpec::PhonyMiddleware, &block
|
625
872
|
subject.get '/' do
|
626
873
|
env['phony.block'].inspect
|
@@ -631,28 +878,42 @@ describe Grape::API do
|
|
631
878
|
last_response.body.should == 'true'
|
632
879
|
end
|
633
880
|
end
|
881
|
+
|
882
|
+
it 'mounts behind error middleware' do
|
883
|
+
m = Class.new(Grape::Middleware::Base) do
|
884
|
+
def before
|
885
|
+
throw :error, message: "Caught in the Net", status: 400
|
886
|
+
end
|
887
|
+
end
|
888
|
+
subject.use m
|
889
|
+
subject.get "/" do
|
890
|
+
end
|
891
|
+
get "/"
|
892
|
+
last_response.status.should == 400
|
893
|
+
last_response.body.should == "Caught in the Net"
|
894
|
+
end
|
634
895
|
end
|
635
896
|
end
|
636
|
-
describe '.
|
897
|
+
describe '.http_basic' do
|
637
898
|
it 'protects any resources on the same scope' do
|
638
|
-
subject.http_basic do |u,p|
|
899
|
+
subject.http_basic do |u, p|
|
639
900
|
u == 'allow'
|
640
901
|
end
|
641
|
-
subject.get(:hello){ "Hello, world."}
|
902
|
+
subject.get(:hello) { "Hello, world." }
|
642
903
|
get '/hello'
|
643
904
|
last_response.status.should eql 401
|
644
|
-
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow','whatever')
|
905
|
+
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
|
645
906
|
last_response.status.should eql 200
|
646
907
|
end
|
647
908
|
|
648
909
|
it 'is scopable' do
|
649
|
-
subject.get(:hello){ "Hello, world."}
|
910
|
+
subject.get(:hello) { "Hello, world." }
|
650
911
|
subject.namespace :admin do
|
651
|
-
http_basic do |u,p|
|
912
|
+
http_basic do |u, p|
|
652
913
|
u == 'allow'
|
653
914
|
end
|
654
915
|
|
655
|
-
get(:hello){ "Hello, world." }
|
916
|
+
get(:hello) { "Hello, world." }
|
656
917
|
end
|
657
918
|
|
658
919
|
get '/hello'
|
@@ -662,15 +923,60 @@ describe Grape::API do
|
|
662
923
|
end
|
663
924
|
|
664
925
|
it 'is callable via .auth as well' do
|
665
|
-
subject.auth :http_basic do |u,p|
|
926
|
+
subject.auth :http_basic do |u, p|
|
666
927
|
u == 'allow'
|
667
928
|
end
|
668
929
|
|
669
|
-
subject.get(:hello){ "Hello, world."}
|
930
|
+
subject.get(:hello) { "Hello, world." }
|
670
931
|
get '/hello'
|
671
932
|
last_response.status.should eql 401
|
672
|
-
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow','whatever')
|
933
|
+
get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
|
934
|
+
last_response.status.should eql 200
|
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')
|
673
978
|
last_response.status.should eql 200
|
979
|
+
last_response.body.should eql "Hello, world."
|
674
980
|
end
|
675
981
|
end
|
676
982
|
|
@@ -685,6 +991,13 @@ describe Grape::API do
|
|
685
991
|
mylogger.should_receive(:info).exactly(1).times
|
686
992
|
subject.logger.info "this will be logged"
|
687
993
|
end
|
994
|
+
|
995
|
+
it "defaults to a standard logger log format" do
|
996
|
+
t = Time.at(100)
|
997
|
+
Time.stub(:now).and_return(t)
|
998
|
+
STDOUT.should_receive(:write).with("I, [#{Logger::Formatter.new.send(:format_datetime, t)}\##{Process.pid}] INFO -- : this will be logged\n")
|
999
|
+
subject.logger.info "this will be logged"
|
1000
|
+
end
|
688
1001
|
end
|
689
1002
|
|
690
1003
|
describe '.helpers' do
|
@@ -749,7 +1062,7 @@ describe Grape::API do
|
|
749
1062
|
[one, two]
|
750
1063
|
end
|
751
1064
|
|
752
|
-
lambda{get '/howdy'}.should_not raise_error
|
1065
|
+
lambda { get '/howdy' }.should_not raise_error
|
753
1066
|
end
|
754
1067
|
|
755
1068
|
it 'allows for modules' do
|
@@ -787,7 +1100,7 @@ describe Grape::API do
|
|
787
1100
|
subject.get 'howdy' do
|
788
1101
|
[one, two, three]
|
789
1102
|
end
|
790
|
-
lambda{get '/howdy'}.should_not raise_error
|
1103
|
+
lambda { get '/howdy' }.should_not raise_error
|
791
1104
|
end
|
792
1105
|
end
|
793
1106
|
|
@@ -833,39 +1146,58 @@ describe Grape::API do
|
|
833
1146
|
raise "rain!"
|
834
1147
|
end
|
835
1148
|
get '/exception'
|
836
|
-
last_response.status.should eql
|
1149
|
+
last_response.status.should eql 500
|
837
1150
|
end
|
838
1151
|
|
839
1152
|
it 'rescues only certain errors if rescue_from is called with specific errors' do
|
840
1153
|
subject.rescue_from ArgumentError
|
841
|
-
subject.get('/rescued'){ raise ArgumentError }
|
842
|
-
subject.get('/unrescued'){ raise "beefcake" }
|
1154
|
+
subject.get('/rescued') { raise ArgumentError }
|
1155
|
+
subject.get('/unrescued') { raise "beefcake" }
|
843
1156
|
|
844
1157
|
get '/rescued'
|
845
|
-
last_response.status.should eql
|
1158
|
+
last_response.status.should eql 500
|
846
1159
|
|
847
|
-
lambda{ get '/unrescued' }.should raise_error
|
1160
|
+
lambda { get '/unrescued' }.should raise_error
|
848
1161
|
end
|
849
1162
|
|
850
|
-
|
851
|
-
|
852
|
-
|
1163
|
+
context 'CustomError subclass of Grape::Exceptions::Base' do
|
1164
|
+
before do
|
1165
|
+
class CustomError < Grape::Exceptions::Base; end
|
1166
|
+
end
|
853
1167
|
|
854
|
-
|
855
|
-
|
1168
|
+
it 'does not re-raise exceptions of type Grape::Exceptions::Base' do
|
1169
|
+
subject.get('/custom_exception') { raise CustomError }
|
856
1170
|
|
857
|
-
|
858
|
-
class CustomError < Grape::Exceptions::Base; end
|
859
|
-
subject.rescue_from CustomError do |e|
|
860
|
-
rack_response('New Error', e.status)
|
1171
|
+
lambda { get '/custom_exception' }.should_not raise_error
|
861
1172
|
end
|
862
|
-
|
863
|
-
|
1173
|
+
|
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'
|
864
1185
|
end
|
1186
|
+
end
|
865
1187
|
|
866
|
-
|
867
|
-
|
868
|
-
|
1188
|
+
it 'can rescue exceptions raised in the formatter' do
|
1189
|
+
formatter = double(:formatter)
|
1190
|
+
formatter.stub(:call) { raise StandardError }
|
1191
|
+
Grape::Formatter::Base.stub(:formatter_for) { formatter }
|
1192
|
+
|
1193
|
+
subject.rescue_from :all do |e|
|
1194
|
+
rack_response('Formatter Error', 500)
|
1195
|
+
end
|
1196
|
+
subject.get('/formatter_exception') { 'Hello world' }
|
1197
|
+
|
1198
|
+
get '/formatter_exception'
|
1199
|
+
last_response.status.should eql 500
|
1200
|
+
last_response.body.should == 'Formatter Error'
|
869
1201
|
end
|
870
1202
|
end
|
871
1203
|
|
@@ -881,61 +1213,144 @@ describe Grape::API do
|
|
881
1213
|
last_response.status.should eql 202
|
882
1214
|
last_response.body.should == 'rescued from rain!'
|
883
1215
|
end
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
1216
|
+
|
1217
|
+
context 'custom errors' do
|
1218
|
+
before do
|
1219
|
+
class ConnectionError < RuntimeError; end
|
1220
|
+
class DatabaseError < RuntimeError; end
|
1221
|
+
class CommunicationError < StandardError; end
|
888
1222
|
end
|
889
|
-
|
890
|
-
|
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'
|
1234
|
+
end
|
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'
|
1245
|
+
end
|
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'
|
1265
|
+
end
|
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)
|
891
1274
|
end
|
892
|
-
get '/exception'
|
893
|
-
last_response.status.should eql 500
|
894
|
-
last_response.body.should == 'rescued from ConnectionError'
|
895
1275
|
end
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
describe '.rescue_from klass, lambda' do
|
1279
|
+
it 'rescues an error with the lambda' do
|
1280
|
+
subject.rescue_from ArgumentError, lambda {
|
1281
|
+
rack_response("rescued with a lambda", 400)
|
1282
|
+
}
|
1283
|
+
subject.get('/rescue_lambda') { raise ArgumentError }
|
1284
|
+
|
1285
|
+
get '/rescue_lambda'
|
1286
|
+
last_response.status.should == 400
|
1287
|
+
last_response.body.should == "rescued with a lambda"
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
it 'can execute the lambda with an argument' do
|
1291
|
+
subject.rescue_from ArgumentError, lambda { |e|
|
1292
|
+
rack_response(e.message, 400)
|
1293
|
+
}
|
1294
|
+
subject.get('/rescue_lambda') { raise ArgumentError, 'lambda takes an argument' }
|
1295
|
+
|
1296
|
+
get '/rescue_lambda'
|
1297
|
+
last_response.status.should == 400
|
1298
|
+
last_response.body.should == 'lambda takes an argument'
|
1299
|
+
end
|
1300
|
+
end
|
1301
|
+
|
1302
|
+
describe '.rescue_from klass, with: method' do
|
1303
|
+
it 'rescues an error with the specified message' do
|
1304
|
+
def rescue_arg_error
|
1305
|
+
Rack::Response.new('rescued with a method', 400)
|
900
1306
|
end
|
901
|
-
|
902
|
-
|
1307
|
+
|
1308
|
+
subject.rescue_from ArgumentError, with: rescue_arg_error
|
1309
|
+
subject.get('/rescue_method') { raise ArgumentError }
|
1310
|
+
|
1311
|
+
get '/rescue_method'
|
1312
|
+
last_response.status.should == 400
|
1313
|
+
last_response.body.should == 'rescued with a method'
|
1314
|
+
end
|
1315
|
+
end
|
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
|
903
1322
|
end
|
904
|
-
get '/exception'
|
905
|
-
last_response.status.should eql 500
|
906
|
-
last_response.body.should == 'rescued from ConnectionError'
|
907
1323
|
end
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
subject.rescue_from ConnectionError do |e|
|
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|
|
912
1327
|
rack_response("rescued from #{e.class.name}", 500)
|
913
1328
|
end
|
914
|
-
subject.
|
915
|
-
|
1329
|
+
subject.get '/caught_child' do
|
1330
|
+
raise APIErrors::ChildError
|
916
1331
|
end
|
917
|
-
subject.get '/
|
918
|
-
raise
|
1332
|
+
subject.get '/caught_parent' do
|
1333
|
+
raise APIErrors::ParentError
|
919
1334
|
end
|
920
|
-
subject.get '/
|
921
|
-
raise
|
1335
|
+
subject.get '/uncaught_parent' do
|
1336
|
+
raise StandardError
|
922
1337
|
end
|
923
|
-
|
1338
|
+
|
1339
|
+
get '/caught_child'
|
924
1340
|
last_response.status.should eql 500
|
925
|
-
|
926
|
-
get '/database'
|
1341
|
+
get '/caught_parent'
|
927
1342
|
last_response.status.should eql 500
|
928
|
-
|
1343
|
+
lambda { get '/uncaught_parent' }.should raise_error(StandardError)
|
929
1344
|
end
|
930
|
-
|
931
|
-
|
932
|
-
subject.rescue_from
|
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|
|
933
1348
|
rack_response("rescued from #{e.class.name}", 500)
|
934
1349
|
end
|
935
1350
|
subject.get '/uncaught' do
|
936
|
-
raise
|
1351
|
+
raise APIErrors::ChildError
|
937
1352
|
end
|
938
|
-
lambda { get '/uncaught' }.should raise_error(
|
1353
|
+
lambda { get '/uncaught' }.should raise_error(APIErrors::ChildError)
|
939
1354
|
end
|
940
1355
|
end
|
941
1356
|
|
@@ -951,13 +1366,13 @@ describe Grape::API do
|
|
951
1366
|
end
|
952
1367
|
|
953
1368
|
it 'rescues all errors and return :txt with backtrace' do
|
954
|
-
subject.rescue_from :all, :
|
1369
|
+
subject.rescue_from :all, backtrace: true
|
955
1370
|
subject.format :txt
|
956
1371
|
subject.get '/exception' do
|
957
1372
|
raise "rain!"
|
958
1373
|
end
|
959
1374
|
get '/exception'
|
960
|
-
last_response.body.start_with?("rain!\r\n").should
|
1375
|
+
last_response.body.start_with?("rain!\r\n").should be true
|
961
1376
|
end
|
962
1377
|
|
963
1378
|
it 'rescues all errors with a default formatter' do
|
@@ -994,7 +1409,7 @@ describe Grape::API do
|
|
994
1409
|
end
|
995
1410
|
end
|
996
1411
|
it 'returns a custom error format' do
|
997
|
-
subject.rescue_from :all, :
|
1412
|
+
subject.rescue_from :all, backtrace: true
|
998
1413
|
subject.error_formatter :txt, CustomErrorFormatter
|
999
1414
|
subject.get '/exception' do
|
1000
1415
|
raise "rain!"
|
@@ -1004,6 +1419,27 @@ describe Grape::API do
|
|
1004
1419
|
end
|
1005
1420
|
end
|
1006
1421
|
|
1422
|
+
describe 'with' do
|
1423
|
+
context 'class' do
|
1424
|
+
before :each do
|
1425
|
+
class CustomErrorFormatter
|
1426
|
+
def self.call(message, backtrace, option, env)
|
1427
|
+
"message: #{message} @backtrace"
|
1428
|
+
end
|
1429
|
+
end
|
1430
|
+
end
|
1431
|
+
|
1432
|
+
it 'returns a custom error format' do
|
1433
|
+
subject.rescue_from :all, backtrace: true
|
1434
|
+
subject.error_formatter :txt, with: CustomErrorFormatter
|
1435
|
+
subject.get('/exception') { raise "rain!" }
|
1436
|
+
|
1437
|
+
get '/exception'
|
1438
|
+
last_response.body.should == 'message: rain! @backtrace'
|
1439
|
+
end
|
1440
|
+
end
|
1441
|
+
end
|
1442
|
+
|
1007
1443
|
it 'rescues all errors and return :json' do
|
1008
1444
|
subject.rescue_from :all
|
1009
1445
|
subject.format :json
|
@@ -1014,7 +1450,7 @@ describe Grape::API do
|
|
1014
1450
|
last_response.body.should eql '{"error":"rain!"}'
|
1015
1451
|
end
|
1016
1452
|
it 'rescues all errors and return :json with backtrace' do
|
1017
|
-
subject.rescue_from :all, :
|
1453
|
+
subject.rescue_from :all, backtrace: true
|
1018
1454
|
subject.format :json
|
1019
1455
|
subject.get '/exception' do
|
1020
1456
|
raise "rain!"
|
@@ -1073,10 +1509,10 @@ describe Grape::API do
|
|
1073
1509
|
describe '.formatter' do
|
1074
1510
|
context 'multiple formatters' do
|
1075
1511
|
before :each do
|
1076
|
-
subject.formatter :json, lambda { |object, env| "{\"custom_formatter\":\"#{object[:some]}\"}" }
|
1077
|
-
subject.formatter :txt, lambda { |object, env| "custom_formatter: #{object[:some]}" }
|
1512
|
+
subject.formatter :json, lambda { |object, env| "{\"custom_formatter\":\"#{object[:some] }\"}" }
|
1513
|
+
subject.formatter :txt, lambda { |object, env| "custom_formatter: #{object[:some] }" }
|
1078
1514
|
subject.get :simple do
|
1079
|
-
{:
|
1515
|
+
{ some: 'hash' }
|
1080
1516
|
end
|
1081
1517
|
end
|
1082
1518
|
it 'sets one formatter' do
|
@@ -1092,9 +1528,9 @@ describe Grape::API do
|
|
1092
1528
|
before :each do
|
1093
1529
|
subject.content_type :json, 'application/json'
|
1094
1530
|
subject.content_type :custom, 'application/custom'
|
1095
|
-
subject.formatter :custom, lambda { |object, env| "{\"custom_formatter\":\"#{object[:some]}\"}" }
|
1531
|
+
subject.formatter :custom, lambda { |object, env| "{\"custom_formatter\":\"#{object[:some] }\"}" }
|
1096
1532
|
subject.get :simple do
|
1097
|
-
{:
|
1533
|
+
{ some: 'hash' }
|
1098
1534
|
end
|
1099
1535
|
end
|
1100
1536
|
it 'uses json' do
|
@@ -1102,14 +1538,14 @@ describe Grape::API do
|
|
1102
1538
|
last_response.body.should eql '{"some":"hash"}'
|
1103
1539
|
end
|
1104
1540
|
it 'uses custom formatter' do
|
1105
|
-
get '/simple.custom',
|
1541
|
+
get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom'
|
1106
1542
|
last_response.body.should eql '{"custom_formatter":"hash"}'
|
1107
1543
|
end
|
1108
1544
|
end
|
1109
1545
|
context 'custom formatter class' do
|
1110
1546
|
module CustomFormatter
|
1111
1547
|
def self.call(object, env)
|
1112
|
-
"{\"custom_formatter\":\"#{object[:some]}\"}"
|
1548
|
+
"{\"custom_formatter\":\"#{object[:some] }\"}"
|
1113
1549
|
end
|
1114
1550
|
end
|
1115
1551
|
before :each do
|
@@ -1117,7 +1553,7 @@ describe Grape::API do
|
|
1117
1553
|
subject.content_type :custom, 'application/custom'
|
1118
1554
|
subject.formatter :custom, CustomFormatter
|
1119
1555
|
subject.get :simple do
|
1120
|
-
{:
|
1556
|
+
{ some: 'hash' }
|
1121
1557
|
end
|
1122
1558
|
end
|
1123
1559
|
it 'uses json' do
|
@@ -1125,13 +1561,22 @@ describe Grape::API do
|
|
1125
1561
|
last_response.body.should eql '{"some":"hash"}'
|
1126
1562
|
end
|
1127
1563
|
it 'uses custom formatter' do
|
1128
|
-
get '/simple.custom',
|
1564
|
+
get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom'
|
1129
1565
|
last_response.body.should eql '{"custom_formatter":"hash"}'
|
1130
1566
|
end
|
1131
1567
|
end
|
1132
1568
|
end
|
1133
1569
|
|
1134
1570
|
describe '.parser' do
|
1571
|
+
it 'parses data in format requested by content-type' do
|
1572
|
+
subject.format :json
|
1573
|
+
subject.post '/data' do
|
1574
|
+
{ x: params[:x] }
|
1575
|
+
end
|
1576
|
+
post "/data", '{"x":42}', 'CONTENT_TYPE' => 'application/json'
|
1577
|
+
last_response.status.should == 201
|
1578
|
+
last_response.body.should == '{"x":42}'
|
1579
|
+
end
|
1135
1580
|
context 'lambda parser' do
|
1136
1581
|
before :each do
|
1137
1582
|
subject.content_type :txt, "text/plain"
|
@@ -1141,7 +1586,7 @@ describe Grape::API do
|
|
1141
1586
|
params[:simple]
|
1142
1587
|
end
|
1143
1588
|
end
|
1144
|
-
[
|
1589
|
+
["text/custom", "text/custom; charset=UTF-8"].each do |content_type|
|
1145
1590
|
it "uses parser for #{content_type}" do
|
1146
1591
|
put '/simple', "simple", "CONTENT_TYPE" => content_type
|
1147
1592
|
last_response.status.should == 200
|
@@ -1179,6 +1624,42 @@ describe Grape::API do
|
|
1179
1624
|
last_response.body.should eql 'Disallowed type attribute: "symbol"'
|
1180
1625
|
end
|
1181
1626
|
end
|
1627
|
+
context "none parser class" do
|
1628
|
+
before :each do
|
1629
|
+
subject.parser :json, nil
|
1630
|
+
subject.put "data" do
|
1631
|
+
"body: #{env['api.request.body'] }"
|
1632
|
+
end
|
1633
|
+
end
|
1634
|
+
it "does not parse data" do
|
1635
|
+
put '/data', 'not valid json', "CONTENT_TYPE" => "application/json"
|
1636
|
+
last_response.status.should == 200
|
1637
|
+
last_response.body.should == "body: not valid json"
|
1638
|
+
end
|
1639
|
+
end
|
1640
|
+
end
|
1641
|
+
|
1642
|
+
describe '.default_format' do
|
1643
|
+
before :each do
|
1644
|
+
subject.format :json
|
1645
|
+
subject.default_format :json
|
1646
|
+
end
|
1647
|
+
it 'returns data in default format' do
|
1648
|
+
subject.get '/data' do
|
1649
|
+
{ x: 42 }
|
1650
|
+
end
|
1651
|
+
get "/data"
|
1652
|
+
last_response.status.should == 200
|
1653
|
+
last_response.body.should == '{"x":42}'
|
1654
|
+
end
|
1655
|
+
it 'parses data in default format' do
|
1656
|
+
subject.post '/data' do
|
1657
|
+
{ x: params[:x] }
|
1658
|
+
end
|
1659
|
+
post "/data", '{"x":42}', "CONTENT_TYPE" => ""
|
1660
|
+
last_response.status.should == 201
|
1661
|
+
last_response.body.should == '{"x":42}'
|
1662
|
+
end
|
1182
1663
|
end
|
1183
1664
|
|
1184
1665
|
describe '.default_error_status' do
|
@@ -1197,7 +1678,16 @@ describe Grape::API do
|
|
1197
1678
|
raise "rain!"
|
1198
1679
|
end
|
1199
1680
|
get '/exception'
|
1200
|
-
last_response.status.should eql
|
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
|
1201
1691
|
end
|
1202
1692
|
end
|
1203
1693
|
|
@@ -1210,76 +1700,76 @@ describe Grape::API do
|
|
1210
1700
|
describe 'single method api structure' do
|
1211
1701
|
before(:each) do
|
1212
1702
|
subject.get :ping do
|
1213
|
-
|
1703
|
+
'pong'
|
1214
1704
|
end
|
1215
1705
|
end
|
1216
1706
|
it 'returns one route' do
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1707
|
+
subject.routes.size.should == 1
|
1708
|
+
route = subject.routes[0]
|
1709
|
+
route.route_version.should be_nil
|
1710
|
+
route.route_path.should == "/ping(.:format)"
|
1711
|
+
route.route_method.should == "GET"
|
1222
1712
|
end
|
1223
1713
|
end
|
1224
1714
|
describe 'api structure with two versions and a namespace' do
|
1225
1715
|
before :each do
|
1226
|
-
subject.version 'v1', :
|
1716
|
+
subject.version 'v1', using: :path
|
1227
1717
|
subject.get 'version' do
|
1228
|
-
|
1718
|
+
api.version
|
1229
1719
|
end
|
1230
1720
|
# version v2
|
1231
|
-
subject.version 'v2', :
|
1721
|
+
subject.version 'v2', using: :path
|
1232
1722
|
subject.prefix 'p'
|
1233
1723
|
subject.namespace 'n1' do
|
1234
1724
|
namespace 'n2' do
|
1235
1725
|
get 'version' do
|
1236
|
-
|
1726
|
+
api.version
|
1237
1727
|
end
|
1238
1728
|
end
|
1239
1729
|
end
|
1240
1730
|
end
|
1241
1731
|
it 'returns the latest version set' do
|
1242
|
-
|
1732
|
+
subject.version.should == 'v2'
|
1243
1733
|
end
|
1244
1734
|
it 'returns versions' do
|
1245
|
-
|
1735
|
+
subject.versions.should == ['v1', 'v2']
|
1246
1736
|
end
|
1247
1737
|
it 'sets route paths' do
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1738
|
+
subject.routes.size.should >= 2
|
1739
|
+
subject.routes[0].route_path.should == "/:version/version(.:format)"
|
1740
|
+
subject.routes[1].route_path.should == "/p/:version/n1/n2/version(.:format)"
|
1251
1741
|
end
|
1252
1742
|
it 'sets route versions' do
|
1253
|
-
|
1254
|
-
|
1743
|
+
subject.routes[0].route_version.should == 'v1'
|
1744
|
+
subject.routes[1].route_version.should == 'v2'
|
1255
1745
|
end
|
1256
1746
|
it 'sets a nested namespace' do
|
1257
|
-
|
1747
|
+
subject.routes[1].route_namespace.should == "/n1/n2"
|
1258
1748
|
end
|
1259
1749
|
it 'sets prefix' do
|
1260
|
-
|
1750
|
+
subject.routes[1].route_prefix.should == 'p'
|
1261
1751
|
end
|
1262
1752
|
end
|
1263
1753
|
describe 'api structure with additional parameters' do
|
1264
1754
|
before(:each) do
|
1265
|
-
subject.get 'split/:string',
|
1755
|
+
subject.get 'split/:string', params: { "token" => "a token" }, optional_params: { "limit" => "the limit" } do
|
1266
1756
|
params[:string].split(params[:token], (params[:limit] || 0).to_i)
|
1267
1757
|
end
|
1268
1758
|
end
|
1269
1759
|
it 'splits a string' do
|
1270
|
-
get "/split/a,b,c.json", :
|
1760
|
+
get "/split/a,b,c.json", token: ','
|
1271
1761
|
last_response.body.should == '["a","b","c"]'
|
1272
1762
|
end
|
1273
1763
|
it 'splits a string with limit' do
|
1274
|
-
get "/split/a,b,c.json", :
|
1764
|
+
get "/split/a,b,c.json", token: ',', limit: '2'
|
1275
1765
|
last_response.body.should == '["a","b,c"]'
|
1276
1766
|
end
|
1277
1767
|
it 'sets route_params' do
|
1278
1768
|
subject.routes.map { |route|
|
1279
|
-
{ :
|
1769
|
+
{ params: route.route_params, optional_params: route.route_optional_params }
|
1280
1770
|
}.should eq [
|
1281
|
-
{ :
|
1282
|
-
|
1771
|
+
{ params: { "string" => "", "token" => "a token" }, optional_params: { "limit" => "the limit" } }
|
1772
|
+
]
|
1283
1773
|
end
|
1284
1774
|
end
|
1285
1775
|
end
|
@@ -1299,7 +1789,7 @@ describe Grape::API do
|
|
1299
1789
|
route = subject.routes.first
|
1300
1790
|
route.route_description.should == "first method"
|
1301
1791
|
route.route_foo.should be_nil
|
1302
|
-
route.route_params.should == {
|
1792
|
+
route.route_params.should == {}
|
1303
1793
|
end
|
1304
1794
|
it 'describes methods separately' do
|
1305
1795
|
subject.desc "first method"
|
@@ -1308,122 +1798,120 @@ describe Grape::API do
|
|
1308
1798
|
subject.get :second do ; end
|
1309
1799
|
subject.routes.count.should == 2
|
1310
1800
|
subject.routes.map { |route|
|
1311
|
-
{ :
|
1801
|
+
{ description: route.route_description, params: route.route_params }
|
1312
1802
|
}.should eq [
|
1313
|
-
{ :
|
1314
|
-
{ :
|
1315
|
-
|
1803
|
+
{ description: "first method", params: {} },
|
1804
|
+
{ description: "second method", params: {} }
|
1805
|
+
]
|
1316
1806
|
end
|
1317
1807
|
it 'resets desc' do
|
1318
1808
|
subject.desc "first method"
|
1319
1809
|
subject.get :first do ; end
|
1320
1810
|
subject.get :second do ; end
|
1321
1811
|
subject.routes.map { |route|
|
1322
|
-
{ :
|
1812
|
+
{ description: route.route_description, params: route.route_params }
|
1323
1813
|
}.should eq [
|
1324
|
-
{ :
|
1325
|
-
{ :
|
1326
|
-
|
1814
|
+
{ description: "first method", params: {} },
|
1815
|
+
{ description: nil, params: {} }
|
1816
|
+
]
|
1327
1817
|
end
|
1328
1818
|
it 'namespaces and describe arbitrary parameters' do
|
1329
1819
|
subject.namespace 'ns' do
|
1330
|
-
desc "ns second", :
|
1820
|
+
desc "ns second", foo: "bar"
|
1331
1821
|
get 'second' do ; end
|
1332
1822
|
end
|
1333
1823
|
subject.routes.map { |route|
|
1334
|
-
{ :
|
1824
|
+
{ description: route.route_description, foo: route.route_foo, params: route.route_params }
|
1335
1825
|
}.should eq [
|
1336
|
-
{ :
|
1337
|
-
|
1826
|
+
{ description: "ns second", foo: "bar", params: {} },
|
1827
|
+
]
|
1338
1828
|
end
|
1339
1829
|
it 'includes details' do
|
1340
|
-
subject.desc "method", :
|
1830
|
+
subject.desc "method", details: "method details"
|
1341
1831
|
subject.get 'method' do ; end
|
1342
1832
|
subject.routes.map { |route|
|
1343
|
-
{ :
|
1833
|
+
{ description: route.route_description, details: route.route_details, params: route.route_params }
|
1344
1834
|
}.should eq [
|
1345
|
-
{ :
|
1346
|
-
|
1835
|
+
{ description: "method", details: "method details", params: {} },
|
1836
|
+
]
|
1347
1837
|
end
|
1348
1838
|
it 'describes a method with parameters' do
|
1349
|
-
subject.desc "Reverses a string.", { :
|
1350
|
-
{ "s" => { :desc => "string to reverse", :type => "string" }}
|
1351
|
-
}
|
1839
|
+
subject.desc "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } }
|
1352
1840
|
subject.get 'reverse' do
|
1353
1841
|
params[:s].reverse
|
1354
1842
|
end
|
1355
1843
|
subject.routes.map { |route|
|
1356
|
-
{ :
|
1844
|
+
{ description: route.route_description, params: route.route_params }
|
1357
1845
|
}.should eq [
|
1358
|
-
{ :
|
1359
|
-
|
1846
|
+
{ description: "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } } }
|
1847
|
+
]
|
1360
1848
|
end
|
1361
1849
|
it 'merges the parameters of the namespace with the parameters of the method' do
|
1362
1850
|
subject.desc "namespace"
|
1363
1851
|
subject.params do
|
1364
|
-
requires :ns_param, :
|
1852
|
+
requires :ns_param, desc: "namespace parameter"
|
1365
1853
|
end
|
1366
1854
|
subject.namespace 'ns' do
|
1367
1855
|
desc "method"
|
1368
1856
|
params do
|
1369
|
-
optional :method_param, :
|
1857
|
+
optional :method_param, desc: "method parameter"
|
1370
1858
|
end
|
1371
1859
|
get 'method' do ; end
|
1372
1860
|
end
|
1373
1861
|
subject.routes.map { |route|
|
1374
|
-
{ :
|
1862
|
+
{ description: route.route_description, params: route.route_params }
|
1375
1863
|
}.should eq [
|
1376
|
-
{ :
|
1377
|
-
:
|
1378
|
-
"ns_param" => { :
|
1379
|
-
"method_param" => { :
|
1864
|
+
{ description: "method",
|
1865
|
+
params: {
|
1866
|
+
"ns_param" => { required: true, desc: "namespace parameter" },
|
1867
|
+
"method_param" => { required: false, desc: "method parameter" }
|
1380
1868
|
}
|
1381
1869
|
}
|
1382
|
-
|
1870
|
+
]
|
1383
1871
|
end
|
1384
1872
|
it 'merges the parameters of nested namespaces' do
|
1385
1873
|
subject.desc "ns1"
|
1386
1874
|
subject.params do
|
1387
|
-
optional :ns_param, :
|
1388
|
-
requires :ns1_param, :
|
1875
|
+
optional :ns_param, desc: "ns param 1"
|
1876
|
+
requires :ns1_param, desc: "ns1 param"
|
1389
1877
|
end
|
1390
1878
|
subject.namespace 'ns1' do
|
1391
1879
|
desc "ns2"
|
1392
1880
|
params do
|
1393
|
-
requires :ns_param, :
|
1394
|
-
requires :ns2_param, :
|
1881
|
+
requires :ns_param, desc: "ns param 2"
|
1882
|
+
requires :ns2_param, desc: "ns2 param"
|
1395
1883
|
end
|
1396
1884
|
namespace 'ns2' do
|
1397
1885
|
desc "method"
|
1398
1886
|
params do
|
1399
|
-
optional :method_param, :
|
1887
|
+
optional :method_param, desc: "method param"
|
1400
1888
|
end
|
1401
1889
|
get 'method' do ; end
|
1402
1890
|
end
|
1403
1891
|
end
|
1404
1892
|
subject.routes.map { |route|
|
1405
|
-
{ :
|
1893
|
+
{ description: route.route_description, params: route.route_params }
|
1406
1894
|
}.should eq [
|
1407
|
-
{ :
|
1408
|
-
:
|
1409
|
-
"ns_param" => { :
|
1410
|
-
"ns1_param" => { :
|
1411
|
-
"ns2_param" => { :
|
1412
|
-
"method_param" => { :
|
1895
|
+
{ description: "method",
|
1896
|
+
params: {
|
1897
|
+
"ns_param" => { required: true, desc: "ns param 2" },
|
1898
|
+
"ns1_param" => { required: true, desc: "ns1 param" },
|
1899
|
+
"ns2_param" => { required: true, desc: "ns2 param" },
|
1900
|
+
"method_param" => { required: false, desc: "method param" }
|
1413
1901
|
}
|
1414
1902
|
}
|
1415
|
-
|
1903
|
+
]
|
1416
1904
|
end
|
1417
1905
|
it "groups nested params and prevents overwriting of params with same name in different groups" do
|
1418
1906
|
subject.desc "method"
|
1419
1907
|
subject.params do
|
1420
1908
|
group :group1 do
|
1421
|
-
optional :param1, :
|
1422
|
-
requires :param2, :
|
1909
|
+
optional :param1, desc: "group1 param1 desc"
|
1910
|
+
requires :param2, desc: "group1 param2 desc"
|
1423
1911
|
end
|
1424
1912
|
group :group2 do
|
1425
|
-
optional :param1, :
|
1426
|
-
requires :param2, :
|
1913
|
+
optional :param1, desc: "group2 param1 desc"
|
1914
|
+
requires :param2, desc: "group2 param2 desc"
|
1427
1915
|
end
|
1428
1916
|
end
|
1429
1917
|
subject.get "method" do ; end
|
@@ -1431,60 +1919,68 @@ describe Grape::API do
|
|
1431
1919
|
subject.routes.map { |route|
|
1432
1920
|
route.route_params
|
1433
1921
|
}.should eq [{
|
1434
|
-
"group1
|
1435
|
-
"group1[
|
1436
|
-
"
|
1437
|
-
"group2
|
1922
|
+
"group1" => { required: true, type: "Array" },
|
1923
|
+
"group1[param1]" => { required: false, desc: "group1 param1 desc" },
|
1924
|
+
"group1[param2]" => { required: true, desc: "group1 param2 desc" },
|
1925
|
+
"group2" => { required: true, type: "Array" },
|
1926
|
+
"group2[param1]" => { required: false, desc: "group2 param1 desc" },
|
1927
|
+
"group2[param2]" => { required: true, desc: "group2 param2 desc" }
|
1438
1928
|
}]
|
1439
1929
|
end
|
1440
1930
|
it 'uses full name of parameters in nested groups' do
|
1441
1931
|
subject.desc "nesting"
|
1442
1932
|
subject.params do
|
1443
|
-
requires :root_param, :
|
1933
|
+
requires :root_param, desc: "root param"
|
1444
1934
|
group :nested do
|
1445
|
-
requires :nested_param, :
|
1935
|
+
requires :nested_param, desc: "nested param"
|
1446
1936
|
end
|
1447
1937
|
end
|
1448
1938
|
subject.get 'method' do ; end
|
1449
1939
|
subject.routes.map { |route|
|
1450
|
-
{ :
|
1940
|
+
{ description: route.route_description, params: route.route_params }
|
1451
1941
|
}.should eq [
|
1452
|
-
{ :
|
1453
|
-
:
|
1454
|
-
"root_param" => { :
|
1455
|
-
"nested
|
1942
|
+
{ description: "nesting",
|
1943
|
+
params: {
|
1944
|
+
"root_param" => { required: true, desc: "root param" },
|
1945
|
+
"nested" => { required: true, type: "Array" },
|
1946
|
+
"nested[nested_param]" => { required: true, desc: "nested param" }
|
1456
1947
|
}
|
1457
1948
|
}
|
1458
|
-
|
1949
|
+
]
|
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
|
1459
1957
|
end
|
1460
1958
|
it 'parses parameters when no description is given' do
|
1461
1959
|
subject.params do
|
1462
|
-
requires :one_param, :
|
1960
|
+
requires :one_param, desc: "one param"
|
1463
1961
|
end
|
1464
1962
|
subject.get 'method' do ; end
|
1465
1963
|
subject.routes.map { |route|
|
1466
|
-
{ :
|
1964
|
+
{ description: route.route_description, params: route.route_params }
|
1467
1965
|
}.should eq [
|
1468
|
-
{ :
|
1469
|
-
|
1966
|
+
{ description: nil, params: { "one_param" => { required: true, desc: "one param" } } }
|
1967
|
+
]
|
1470
1968
|
end
|
1471
1969
|
it 'does not symbolize params' do
|
1472
|
-
subject.desc "Reverses a string.", { :
|
1473
|
-
{ "s" => { :desc => "string to reverse", :type => "string" }}
|
1474
|
-
}
|
1970
|
+
subject.desc "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } }
|
1475
1971
|
subject.get 'reverse/:s' do
|
1476
1972
|
params[:s].reverse
|
1477
1973
|
end
|
1478
1974
|
subject.routes.map { |route|
|
1479
|
-
{ :
|
1975
|
+
{ description: route.route_description, params: route.route_params }
|
1480
1976
|
}.should eq [
|
1481
|
-
{ :
|
1482
|
-
|
1977
|
+
{ description: "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } } }
|
1978
|
+
]
|
1483
1979
|
end
|
1484
1980
|
end
|
1485
1981
|
|
1486
1982
|
describe '.mount' do
|
1487
|
-
let(:mounted_app){ lambda{|env| [200, {}, ["MOUNTED"]]} }
|
1983
|
+
let(:mounted_app) { lambda { |env| [200, {}, ["MOUNTED"]] } }
|
1488
1984
|
|
1489
1985
|
context 'with a bare rack app' do
|
1490
1986
|
before do
|
@@ -1502,7 +1998,7 @@ describe Grape::API do
|
|
1502
1998
|
end
|
1503
1999
|
|
1504
2000
|
it 'is able to cascade' do
|
1505
|
-
subject.mount lambda{ |env|
|
2001
|
+
subject.mount lambda { |env|
|
1506
2002
|
headers = {}
|
1507
2003
|
headers['X-Cascade'] == 'pass' unless env['PATH_INFO'].include?('boo')
|
1508
2004
|
[200, headers, ["Farfegnugen"]]
|
@@ -1525,7 +2021,7 @@ describe Grape::API do
|
|
1525
2021
|
|
1526
2022
|
context 'mounting an API' do
|
1527
2023
|
it 'applies the settings of the mounting api' do
|
1528
|
-
subject.version 'v1', :
|
2024
|
+
subject.version 'v1', using: :path
|
1529
2025
|
|
1530
2026
|
subject.namespace :cool do
|
1531
2027
|
app = Class.new(Grape::API)
|
@@ -1535,6 +2031,25 @@ describe Grape::API do
|
|
1535
2031
|
|
1536
2032
|
mount app
|
1537
2033
|
end
|
2034
|
+
|
2035
|
+
get '/v1/cool/awesome'
|
2036
|
+
last_response.body.should == 'yo'
|
2037
|
+
end
|
2038
|
+
|
2039
|
+
it 'applies the settings to nested mounted apis' do
|
2040
|
+
subject.version 'v1', using: :path
|
2041
|
+
|
2042
|
+
subject.namespace :cool do
|
2043
|
+
inner_app = Class.new(Grape::API)
|
2044
|
+
inner_app.get('/awesome') do
|
2045
|
+
"yo"
|
2046
|
+
end
|
2047
|
+
|
2048
|
+
app = Class.new(Grape::API)
|
2049
|
+
app.mount inner_app
|
2050
|
+
mount app
|
2051
|
+
end
|
2052
|
+
|
1538
2053
|
get '/v1/cool/awesome'
|
1539
2054
|
last_response.body.should == 'yo'
|
1540
2055
|
end
|
@@ -1562,8 +2077,8 @@ describe Grape::API do
|
|
1562
2077
|
mount app
|
1563
2078
|
end
|
1564
2079
|
subject.routes.size.should == 2
|
1565
|
-
subject.routes.first.route_path.should =~
|
1566
|
-
subject.routes.last.route_path.should =~
|
2080
|
+
subject.routes.first.route_path.should =~ %r{\/cool\/awesome}
|
2081
|
+
subject.routes.last.route_path.should =~ %r{\/cool\/sauce}
|
1567
2082
|
end
|
1568
2083
|
|
1569
2084
|
it 'mounts on a path' do
|
@@ -1591,6 +2106,50 @@ describe Grape::API do
|
|
1591
2106
|
get "/app1/app2/nice"
|
1592
2107
|
last_response.status.should == 200
|
1593
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
|
1594
2153
|
end
|
1595
2154
|
|
1596
2155
|
end
|
@@ -1625,8 +2184,8 @@ describe Grape::API do
|
|
1625
2184
|
subject.format :json
|
1626
2185
|
subject.get '/endpoint/options' do
|
1627
2186
|
{
|
1628
|
-
:
|
1629
|
-
:
|
2187
|
+
path: options[:path],
|
2188
|
+
source_location: source.source_location
|
1630
2189
|
}
|
1631
2190
|
end
|
1632
2191
|
end
|
@@ -1662,7 +2221,7 @@ describe Grape::API do
|
|
1662
2221
|
subject.get '/description' do
|
1663
2222
|
route.route_description
|
1664
2223
|
end
|
1665
|
-
subject.desc 'returns parameters',
|
2224
|
+
subject.desc 'returns parameters', params: { "x" => "y" }
|
1666
2225
|
subject.get '/params/:id' do
|
1667
2226
|
route.route_params[params[:id]]
|
1668
2227
|
end
|
@@ -1677,46 +2236,46 @@ describe Grape::API do
|
|
1677
2236
|
end
|
1678
2237
|
end
|
1679
2238
|
end
|
1680
|
-
|
2239
|
+
describe '.format' do
|
1681
2240
|
context ':txt' do
|
1682
2241
|
before(:each) do
|
1683
2242
|
subject.format :txt
|
1684
2243
|
subject.content_type :json, "application/json"
|
1685
2244
|
subject.get '/meaning_of_life' do
|
1686
|
-
{ :
|
2245
|
+
{ meaning_of_life: 42 }
|
1687
2246
|
end
|
1688
2247
|
end
|
1689
2248
|
it 'forces txt without an extension' do
|
1690
2249
|
get '/meaning_of_life'
|
1691
|
-
last_response.body.should == { :
|
2250
|
+
last_response.body.should == { meaning_of_life: 42 }.to_s
|
1692
2251
|
end
|
1693
2252
|
it 'does not force txt with an extension' do
|
1694
2253
|
get '/meaning_of_life.json'
|
1695
|
-
last_response.body.should == { :
|
2254
|
+
last_response.body.should == { meaning_of_life: 42 }.to_json
|
1696
2255
|
end
|
1697
2256
|
it 'forces txt from a non-accepting header' do
|
1698
|
-
get '/meaning_of_life', {},
|
1699
|
-
last_response.body.should == { :
|
2257
|
+
get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json'
|
2258
|
+
last_response.body.should == { meaning_of_life: 42 }.to_s
|
1700
2259
|
end
|
1701
2260
|
end
|
1702
2261
|
context ':txt only' do
|
1703
2262
|
before(:each) do
|
1704
2263
|
subject.format :txt
|
1705
2264
|
subject.get '/meaning_of_life' do
|
1706
|
-
{ :
|
2265
|
+
{ meaning_of_life: 42 }
|
1707
2266
|
end
|
1708
2267
|
end
|
1709
2268
|
it 'forces txt without an extension' do
|
1710
2269
|
get '/meaning_of_life'
|
1711
|
-
last_response.body.should == { :
|
2270
|
+
last_response.body.should == { meaning_of_life: 42 }.to_s
|
1712
2271
|
end
|
1713
2272
|
it 'forces txt with the wrong extension' do
|
1714
2273
|
get '/meaning_of_life.json'
|
1715
|
-
last_response.body.should == { :
|
2274
|
+
last_response.body.should == { meaning_of_life: 42 }.to_s
|
1716
2275
|
end
|
1717
2276
|
it 'forces txt from a non-accepting header' do
|
1718
|
-
get '/meaning_of_life', {},
|
1719
|
-
last_response.body.should == { :
|
2277
|
+
get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json'
|
2278
|
+
last_response.body.should == { meaning_of_life: 42 }.to_s
|
1720
2279
|
end
|
1721
2280
|
end
|
1722
2281
|
context ':json' do
|
@@ -1724,48 +2283,71 @@ describe Grape::API do
|
|
1724
2283
|
subject.format :json
|
1725
2284
|
subject.content_type :txt, "text/plain"
|
1726
2285
|
subject.get '/meaning_of_life' do
|
1727
|
-
{ :
|
2286
|
+
{ meaning_of_life: 42 }
|
1728
2287
|
end
|
1729
2288
|
end
|
1730
2289
|
it 'forces json without an extension' do
|
1731
2290
|
get '/meaning_of_life'
|
1732
|
-
last_response.body.should == { :
|
2291
|
+
last_response.body.should == { meaning_of_life: 42 }.to_json
|
1733
2292
|
end
|
1734
2293
|
it 'does not force json with an extension' do
|
1735
2294
|
get '/meaning_of_life.txt'
|
1736
|
-
last_response.body.should == { :
|
2295
|
+
last_response.body.should == { meaning_of_life: 42 }.to_s
|
1737
2296
|
end
|
1738
2297
|
it 'forces json from a non-accepting header' do
|
1739
|
-
get '/meaning_of_life', {},
|
1740
|
-
last_response.body.should == { :
|
2298
|
+
get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html'
|
2299
|
+
last_response.body.should == { meaning_of_life: 42 }.to_json
|
2300
|
+
end
|
2301
|
+
it 'can be overwritten with an explicit content type' do
|
2302
|
+
subject.get '/meaning_of_life_with_content_type' do
|
2303
|
+
content_type "text/plain"
|
2304
|
+
{ meaning_of_life: 42 }.to_s
|
2305
|
+
end
|
2306
|
+
get '/meaning_of_life_with_content_type'
|
2307
|
+
last_response.body.should == { meaning_of_life: 42 }.to_s
|
2308
|
+
end
|
2309
|
+
it 'raised :error from middleware' do
|
2310
|
+
middleware = Class.new(Grape::Middleware::Base) do
|
2311
|
+
def before
|
2312
|
+
throw :error, message: "Unauthorized", status: 42
|
2313
|
+
end
|
2314
|
+
end
|
2315
|
+
subject.use middleware
|
2316
|
+
subject.get do
|
2317
|
+
|
2318
|
+
end
|
2319
|
+
get "/"
|
2320
|
+
last_response.status.should == 42
|
2321
|
+
last_response.body.should == { error: "Unauthorized" }.to_json
|
1741
2322
|
end
|
2323
|
+
|
1742
2324
|
end
|
1743
2325
|
context ':serializable_hash' do
|
1744
2326
|
before(:each) do
|
1745
|
-
class
|
2327
|
+
class SerializableHashExample
|
1746
2328
|
def serializable_hash
|
1747
|
-
{:
|
2329
|
+
{ abc: 'def' }
|
1748
2330
|
end
|
1749
2331
|
end
|
1750
2332
|
subject.format :serializable_hash
|
1751
2333
|
end
|
1752
2334
|
it 'instance' do
|
1753
2335
|
subject.get '/example' do
|
1754
|
-
|
2336
|
+
SerializableHashExample.new
|
1755
2337
|
end
|
1756
2338
|
get '/example'
|
1757
2339
|
last_response.body.should == '{"abc":"def"}'
|
1758
2340
|
end
|
1759
2341
|
it 'root' do
|
1760
2342
|
subject.get '/example' do
|
1761
|
-
{ "root" =>
|
2343
|
+
{ "root" => SerializableHashExample.new }
|
1762
2344
|
end
|
1763
2345
|
get '/example'
|
1764
2346
|
last_response.body.should == '{"root":{"abc":"def"}}'
|
1765
2347
|
end
|
1766
2348
|
it 'array' do
|
1767
2349
|
subject.get '/examples' do
|
1768
|
-
[
|
2350
|
+
[SerializableHashExample.new, SerializableHashExample.new]
|
1769
2351
|
end
|
1770
2352
|
get '/examples'
|
1771
2353
|
last_response.body.should == '[{"abc":"def"},{"abc":"def"}]'
|
@@ -1793,7 +2375,7 @@ XML
|
|
1793
2375
|
ActiveSupport::OrderedHash[
|
1794
2376
|
:example1, "example1",
|
1795
2377
|
:example2, "example2"
|
1796
|
-
|
2378
|
+
]
|
1797
2379
|
end
|
1798
2380
|
get '/example'
|
1799
2381
|
last_response.status.should == 200
|
@@ -1807,7 +2389,7 @@ XML
|
|
1807
2389
|
end
|
1808
2390
|
it 'array' do
|
1809
2391
|
subject.get "/example" do
|
1810
|
-
[
|
2392
|
+
["example1", "example2"]
|
1811
2393
|
end
|
1812
2394
|
get '/example'
|
1813
2395
|
last_response.status.should == 200
|
@@ -1817,6 +2399,25 @@ XML
|
|
1817
2399
|
<string>example1</string>
|
1818
2400
|
<string>example2</string>
|
1819
2401
|
</strings>
|
2402
|
+
XML
|
2403
|
+
end
|
2404
|
+
it 'raised :error from middleware' do
|
2405
|
+
middleware = Class.new(Grape::Middleware::Base) do
|
2406
|
+
def before
|
2407
|
+
throw :error, message: "Unauthorized", status: 42
|
2408
|
+
end
|
2409
|
+
end
|
2410
|
+
subject.use middleware
|
2411
|
+
subject.get do
|
2412
|
+
|
2413
|
+
end
|
2414
|
+
get "/"
|
2415
|
+
last_response.status.should == 42
|
2416
|
+
last_response.body.should == <<-XML
|
2417
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2418
|
+
<error>
|
2419
|
+
<message>Unauthorized</message>
|
2420
|
+
</error>
|
1820
2421
|
XML
|
1821
2422
|
end
|
1822
2423
|
end
|
@@ -1825,22 +2426,22 @@ XML
|
|
1825
2426
|
context "catch-all" do
|
1826
2427
|
before do
|
1827
2428
|
api1 = Class.new(Grape::API)
|
1828
|
-
api1.version 'v1', :
|
2429
|
+
api1.version 'v1', using: :path
|
1829
2430
|
api1.get "hello" do
|
1830
2431
|
"v1"
|
1831
2432
|
end
|
1832
2433
|
api2 = Class.new(Grape::API)
|
1833
|
-
api2.version 'v2', :
|
2434
|
+
api2.version 'v2', using: :path
|
1834
2435
|
api2.get "hello" do
|
1835
2436
|
"v2"
|
1836
2437
|
end
|
1837
2438
|
subject.mount api1
|
1838
2439
|
subject.mount api2
|
1839
2440
|
end
|
1840
|
-
[
|
2441
|
+
[true, false].each do |anchor|
|
1841
2442
|
it "anchor=#{anchor}" do
|
1842
|
-
subject.route :any, '*path', :
|
1843
|
-
error!("Unrecognized request path: #{params[:path]} - #{env['PATH_INFO']}#{env['SCRIPT_NAME']}", 404)
|
2443
|
+
subject.route :any, '*path', anchor: anchor do
|
2444
|
+
error!("Unrecognized request path: #{params[:path] } - #{env['PATH_INFO'] }#{env['SCRIPT_NAME'] }", 404)
|
1844
2445
|
end
|
1845
2446
|
get "/v1/hello"
|
1846
2447
|
last_response.status.should == 200
|
@@ -1856,18 +2457,46 @@ XML
|
|
1856
2457
|
end
|
1857
2458
|
|
1858
2459
|
context "cascading" do
|
1859
|
-
|
1860
|
-
|
1861
|
-
|
1862
|
-
|
1863
|
-
|
2460
|
+
context "via version" do
|
2461
|
+
it "cascades" do
|
2462
|
+
subject.version 'v1', using: :path, cascade: true
|
2463
|
+
get "/v1/hello"
|
2464
|
+
last_response.status.should == 404
|
2465
|
+
last_response.headers["X-Cascade"].should == "pass"
|
2466
|
+
end
|
2467
|
+
it "does not cascade" do
|
2468
|
+
subject.version 'v2', using: :path, cascade: false
|
2469
|
+
get "/v2/hello"
|
2470
|
+
last_response.status.should == 404
|
2471
|
+
last_response.headers.keys.should_not include "X-Cascade"
|
2472
|
+
end
|
1864
2473
|
end
|
2474
|
+
context "via endpoint" do
|
2475
|
+
it "cascades" do
|
2476
|
+
subject.cascade true
|
2477
|
+
get "/hello"
|
2478
|
+
last_response.status.should == 404
|
2479
|
+
last_response.headers["X-Cascade"].should == "pass"
|
2480
|
+
end
|
2481
|
+
it "does not cascade" do
|
2482
|
+
subject.cascade false
|
2483
|
+
get "/hello"
|
2484
|
+
last_response.status.should == 404
|
2485
|
+
last_response.headers.keys.should_not include "X-Cascade"
|
2486
|
+
end
|
2487
|
+
end
|
2488
|
+
end
|
1865
2489
|
|
1866
|
-
|
1867
|
-
|
1868
|
-
|
1869
|
-
|
1870
|
-
|
2490
|
+
context 'with json default_error_formatter' do
|
2491
|
+
it 'returns json error' do
|
2492
|
+
subject.content_type :json, "application/json"
|
2493
|
+
subject.default_error_formatter :json
|
2494
|
+
subject.get '/something' do
|
2495
|
+
'foo'
|
2496
|
+
end
|
2497
|
+
get '/something'
|
2498
|
+
last_response.status.should == 406
|
2499
|
+
last_response.body.should == "{\"error\":\"The requested format 'txt' is not supported.\"}"
|
1871
2500
|
end
|
1872
2501
|
end
|
1873
2502
|
end
|