grape 0.2.1.1 → 0.2.2
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.
- data/.gitignore +1 -0
- data/CHANGELOG.markdown +23 -2
- data/Gemfile +2 -0
- data/README.markdown +402 -227
- data/grape.gemspec +5 -2
- data/lib/grape.rb +6 -0
- data/lib/grape/api.rb +59 -2
- data/lib/grape/endpoint.rb +49 -9
- data/lib/grape/entity.rb +75 -8
- data/lib/grape/exceptions/base.rb +17 -0
- data/lib/grape/exceptions/validation_error.rb +10 -0
- data/lib/grape/middleware/base.rb +28 -19
- data/lib/grape/middleware/error.rb +11 -3
- data/lib/grape/middleware/formatter.rb +11 -18
- data/lib/grape/middleware/versioner/header.rb +76 -17
- data/lib/grape/util/deep_merge.rb +23 -0
- data/lib/grape/util/hash_stack.rb +12 -3
- data/lib/grape/validations.rb +202 -0
- data/lib/grape/validations/coerce.rb +61 -0
- data/lib/grape/validations/presence.rb +11 -0
- data/lib/grape/validations/regexp.rb +13 -0
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +281 -123
- data/spec/grape/endpoint_spec.rb +69 -4
- data/spec/grape/entity_spec.rb +204 -16
- data/spec/grape/middleware/exception_spec.rb +21 -0
- data/spec/grape/middleware/formatter_spec.rb +19 -0
- data/spec/grape/middleware/versioner/header_spec.rb +159 -88
- data/spec/grape/validations/coerce_spec.rb +129 -0
- data/spec/grape/validations/presence_spec.rb +138 -0
- data/spec/grape/validations/regexp_spec.rb +33 -0
- data/spec/grape/validations_spec.rb +185 -0
- metadata +65 -74
- data/spec/grape_spec.rb +0 -1
data/spec/grape/endpoint_spec.rb
CHANGED
@@ -108,6 +108,57 @@ describe Grape::Endpoint do
|
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
111
|
+
describe '#declared' do
|
112
|
+
before do
|
113
|
+
subject.params do
|
114
|
+
requires :first
|
115
|
+
optional :second
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'should have as many keys as there are declared params' do
|
122
|
+
subject.get "/declared" do
|
123
|
+
declared(params).keys.size.should == 2
|
124
|
+
""
|
125
|
+
end
|
126
|
+
|
127
|
+
get '/declared?first=present'
|
128
|
+
last_response.status.should == 200
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should filter out any additional params that are given' do
|
132
|
+
subject.get "/declared" do
|
133
|
+
declared(params).key?(:other).should == false
|
134
|
+
""
|
135
|
+
end
|
136
|
+
|
137
|
+
get '/declared?first=one&other=two'
|
138
|
+
last_response.status.should == 200
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'should stringify if that option is passed' do
|
142
|
+
subject.get "/declared" do
|
143
|
+
declared(params, :stringify => true)["first"].should == "one"
|
144
|
+
""
|
145
|
+
end
|
146
|
+
|
147
|
+
get '/declared?first=one&other=two'
|
148
|
+
last_response.status.should == 200
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should not include missing attributes if that option is passed' do
|
152
|
+
subject.get "/declared" do
|
153
|
+
declared(params, :include_missing => false)[:second].should == nil
|
154
|
+
""
|
155
|
+
end
|
156
|
+
|
157
|
+
get '/declared?first=one&other=two'
|
158
|
+
last_response.status.should == 200
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
111
162
|
describe '#params' do
|
112
163
|
it 'should be available to the caller' do
|
113
164
|
subject.get('/hey') do
|
@@ -186,9 +237,9 @@ describe Grape::Endpoint do
|
|
186
237
|
last_response.body.should == 'Bobby T.'
|
187
238
|
end
|
188
239
|
|
189
|
-
it 'should convert JSON bodies to params' do
|
190
|
-
put '/request_body',
|
191
|
-
last_response.body.should == '
|
240
|
+
it 'should not convert empty JSON bodies to params' do
|
241
|
+
put '/request_body', '', {'CONTENT_TYPE' => 'application/json'}
|
242
|
+
last_response.body.should == ''
|
192
243
|
end
|
193
244
|
|
194
245
|
it 'should convert XML bodies to params' do
|
@@ -268,7 +319,7 @@ describe Grape::Endpoint do
|
|
268
319
|
redirect "/ha", :permanent => true
|
269
320
|
end
|
270
321
|
get '/hey'
|
271
|
-
last_response.status.should eq
|
322
|
+
last_response.status.should eq 301
|
272
323
|
last_response.headers['Location'].should eq "/ha"
|
273
324
|
last_response.body.should eq ""
|
274
325
|
end
|
@@ -347,6 +398,20 @@ describe Grape::Endpoint do
|
|
347
398
|
last_response.body.should == 'Hiya'
|
348
399
|
end
|
349
400
|
|
401
|
+
it 'should automatically use Klass::Entity if that exists' do
|
402
|
+
some_model = Class.new
|
403
|
+
entity = Class.new(Grape::Entity)
|
404
|
+
entity.stub!(:represent).and_return("Auto-detect!")
|
405
|
+
|
406
|
+
some_model.const_set :Entity, entity
|
407
|
+
|
408
|
+
subject.get '/example' do
|
409
|
+
present some_model.new
|
410
|
+
end
|
411
|
+
get '/example'
|
412
|
+
last_response.body.should == 'Auto-detect!'
|
413
|
+
end
|
414
|
+
|
350
415
|
it 'should add a root key to the output if one is given' do
|
351
416
|
subject.get '/example' do
|
352
417
|
present({:abc => 'def'}, :root => :root)
|
data/spec/grape/entity_spec.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Grape::Entity do
|
4
|
-
let(:fresh_class){ Class.new(Grape::Entity) }
|
4
|
+
let(:fresh_class) { Class.new(Grape::Entity) }
|
5
5
|
|
6
6
|
context 'class methods' do
|
7
|
-
subject{ fresh_class }
|
7
|
+
subject { fresh_class }
|
8
8
|
|
9
9
|
describe '.expose' do
|
10
10
|
context 'multiple attributes' do
|
@@ -235,20 +235,24 @@ describe Grape::Entity do
|
|
235
235
|
end
|
236
236
|
|
237
237
|
context 'instance methods' do
|
238
|
+
|
238
239
|
let(:model){ mock(attributes) }
|
239
|
-
|
240
|
-
|
240
|
+
|
241
|
+
let(:attributes) { {
|
242
|
+
:name => 'Bob Bobson',
|
241
243
|
:email => 'bob@example.com',
|
242
244
|
:birthday => Time.gm(2012, 2, 27),
|
243
245
|
:fantasies => ['Unicorns', 'Double Rainbows', 'Nessy'],
|
244
246
|
:friends => [
|
245
|
-
mock(:name => "Friend 1", :email => 'friend1@example.com', :fantasies => [], :birthday => Time.gm(2012, 2, 27), :friends => []),
|
247
|
+
mock(:name => "Friend 1", :email => 'friend1@example.com', :fantasies => [], :birthday => Time.gm(2012, 2, 27), :friends => []),
|
246
248
|
mock(:name => "Friend 2", :email => 'friend2@example.com', :fantasies => [], :birthday => Time.gm(2012, 2, 27), :friends => [])
|
247
249
|
]
|
248
250
|
} }
|
251
|
+
|
249
252
|
subject{ fresh_class.new(model) }
|
250
253
|
|
251
254
|
describe '#serializable_hash' do
|
255
|
+
|
252
256
|
it 'should not throw an exception if a nil options object is passed' do
|
253
257
|
expect{ fresh_class.new(model).serializable_hash(nil) }.not_to raise_error
|
254
258
|
end
|
@@ -257,6 +261,88 @@ describe Grape::Entity do
|
|
257
261
|
fresh_class.expose :name
|
258
262
|
expect{ fresh_class.new(nil).serializable_hash }.not_to raise_error
|
259
263
|
end
|
264
|
+
|
265
|
+
it 'should not throw an exception when an attribute is not found on the object' do
|
266
|
+
fresh_class.expose :name, :non_existant_attribute
|
267
|
+
expect{ fresh_class.new(model).serializable_hash }.not_to raise_error
|
268
|
+
end
|
269
|
+
|
270
|
+
it "should not expose attributes that don't exist on the object" do
|
271
|
+
fresh_class.expose :email, :non_existant_attribute, :name
|
272
|
+
|
273
|
+
res = fresh_class.new(model).serializable_hash
|
274
|
+
res.should have_key :email
|
275
|
+
res.should_not have_key :non_existant_attribute
|
276
|
+
res.should have_key :name
|
277
|
+
end
|
278
|
+
|
279
|
+
it "should not expose attributes that don't exist on the object, even with criteria" do
|
280
|
+
fresh_class.expose :email
|
281
|
+
fresh_class.expose :non_existant_attribute, :if => lambda { false }
|
282
|
+
fresh_class.expose :non_existant_attribute2, :if => lambda { true }
|
283
|
+
|
284
|
+
res = fresh_class.new(model).serializable_hash
|
285
|
+
res.should have_key :email
|
286
|
+
res.should_not have_key :non_existant_attribute
|
287
|
+
res.should_not have_key :non_existant_attribute2
|
288
|
+
end
|
289
|
+
|
290
|
+
it "should expose attributes that don't exist on the object only when they are generated by a block" do
|
291
|
+
fresh_class.expose :non_existant_attribute do |model, options|
|
292
|
+
"well, I do exist after all"
|
293
|
+
end
|
294
|
+
res = fresh_class.new(model).serializable_hash
|
295
|
+
res.should have_key :non_existant_attribute
|
296
|
+
end
|
297
|
+
|
298
|
+
it "should not expose attributes that are generated by a block but have not passed criteria" do
|
299
|
+
fresh_class.expose :non_existant_attribute, :proc => lambda {|model, options|
|
300
|
+
"I exist, but it is not yet my time to shine"
|
301
|
+
}, :if => lambda { |model, options| false }
|
302
|
+
res = fresh_class.new(model).serializable_hash
|
303
|
+
res.should_not have_key :non_existant_attribute
|
304
|
+
end
|
305
|
+
|
306
|
+
context "#serializable_hash" do
|
307
|
+
|
308
|
+
module EntitySpec
|
309
|
+
class EmbeddedExample
|
310
|
+
def serializable_hash(opts = {})
|
311
|
+
{ :abc => 'def' }
|
312
|
+
end
|
313
|
+
end
|
314
|
+
class EmbeddedExampleWithMany
|
315
|
+
def name
|
316
|
+
"abc"
|
317
|
+
end
|
318
|
+
def embedded
|
319
|
+
[ EmbeddedExample.new, EmbeddedExample.new ]
|
320
|
+
end
|
321
|
+
end
|
322
|
+
class EmbeddedExampleWithOne
|
323
|
+
def name
|
324
|
+
"abc"
|
325
|
+
end
|
326
|
+
def embedded
|
327
|
+
EmbeddedExample.new
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
it 'should serialize embedded objects which respond to #serializable_hash' do
|
333
|
+
fresh_class.expose :name, :embedded
|
334
|
+
presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithOne.new)
|
335
|
+
presenter.serializable_hash.should == {:name => "abc", :embedded => {:abc => "def"}}
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'should serialize embedded arrays of objects which respond to #serializable_hash' do
|
339
|
+
fresh_class.expose :name, :embedded
|
340
|
+
presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithMany.new)
|
341
|
+
presenter.serializable_hash.should == {:name => "abc", :embedded => [{:abc => "def"}, {:abc => "def"}]}
|
342
|
+
end
|
343
|
+
|
344
|
+
end
|
345
|
+
|
260
346
|
end
|
261
347
|
|
262
348
|
describe '#value_for' do
|
@@ -289,19 +375,73 @@ describe Grape::Entity do
|
|
289
375
|
rep.last.serializable_hash[:name].should == 'Friend 2'
|
290
376
|
end
|
291
377
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
378
|
+
context 'child representations' do
|
379
|
+
it 'should disable root key name for child representations' do
|
380
|
+
|
381
|
+
module EntitySpec
|
382
|
+
class FriendEntity < Grape::Entity
|
383
|
+
root 'friends', 'friend'
|
384
|
+
expose :name, :email
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
fresh_class.class_eval do
|
389
|
+
expose :friends, :using => EntitySpec::FriendEntity
|
390
|
+
end
|
391
|
+
|
392
|
+
rep = subject.send(:value_for, :friends)
|
393
|
+
rep.should be_kind_of(Array)
|
394
|
+
rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
|
395
|
+
rep.first.serializable_hash[:name].should == 'Friend 1'
|
396
|
+
rep.last.serializable_hash[:name].should == 'Friend 2'
|
296
397
|
end
|
297
|
-
|
298
|
-
|
398
|
+
|
399
|
+
it 'should pass through custom options' do
|
400
|
+
module EntitySpec
|
401
|
+
class FriendEntity < Grape::Entity
|
402
|
+
root 'friends', 'friend'
|
403
|
+
expose :name
|
404
|
+
expose :email, :if => { :user_type => :admin }
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
fresh_class.class_eval do
|
409
|
+
expose :friends, :using => EntitySpec::FriendEntity
|
410
|
+
end
|
411
|
+
|
412
|
+
rep = subject.send(:value_for, :friends)
|
413
|
+
rep.should be_kind_of(Array)
|
414
|
+
rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
|
415
|
+
rep.first.serializable_hash[:email].should be_nil
|
416
|
+
rep.last.serializable_hash[:email].should be_nil
|
417
|
+
|
418
|
+
rep = subject.send(:value_for, :friends, { :user_type => :admin })
|
419
|
+
rep.should be_kind_of(Array)
|
420
|
+
rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
|
421
|
+
rep.first.serializable_hash[:email].should == 'friend1@example.com'
|
422
|
+
rep.last.serializable_hash[:email].should == 'friend2@example.com'
|
299
423
|
end
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
424
|
+
|
425
|
+
it 'should ignore the :collection parameter in the source options' do
|
426
|
+
module EntitySpec
|
427
|
+
class FriendEntity < Grape::Entity
|
428
|
+
root 'friends', 'friend'
|
429
|
+
expose :name
|
430
|
+
expose :email, :if => { :collection => true }
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
fresh_class.class_eval do
|
435
|
+
expose :friends, :using => EntitySpec::FriendEntity
|
436
|
+
end
|
437
|
+
|
438
|
+
rep = subject.send(:value_for, :friends, { :collection => false })
|
439
|
+
rep.should be_kind_of(Array)
|
440
|
+
rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty
|
441
|
+
rep.first.serializable_hash[:email].should == 'friend1@example.com'
|
442
|
+
rep.last.serializable_hash[:email].should == 'friend2@example.com'
|
443
|
+
end
|
444
|
+
|
305
445
|
end
|
306
446
|
|
307
447
|
it 'should call through to the proc if there is one' do
|
@@ -387,5 +527,53 @@ describe Grape::Entity do
|
|
387
527
|
subject.send(:conditions_met?, exposure_options, :true => true).should be_false
|
388
528
|
end
|
389
529
|
end
|
530
|
+
|
531
|
+
describe "::DSL" do
|
532
|
+
subject{ Class.new }
|
533
|
+
|
534
|
+
it 'should create an Entity class when called' do
|
535
|
+
subject.should_not be_const_defined(:Entity)
|
536
|
+
subject.send(:include, Grape::Entity::DSL)
|
537
|
+
subject.should be_const_defined(:Entity)
|
538
|
+
end
|
539
|
+
|
540
|
+
context 'pre-mixed' do
|
541
|
+
before{ subject.send(:include, Grape::Entity::DSL) }
|
542
|
+
|
543
|
+
it 'should be able to define entity traits through DSL' do
|
544
|
+
subject.entity do
|
545
|
+
expose :name
|
546
|
+
end
|
547
|
+
|
548
|
+
subject.entity_class.exposures.should_not be_empty
|
549
|
+
end
|
550
|
+
|
551
|
+
it 'should be able to expose straight from the class' do
|
552
|
+
subject.entity :name, :email
|
553
|
+
subject.entity_class.exposures.size.should == 2
|
554
|
+
end
|
555
|
+
|
556
|
+
it 'should be able to mix field and advanced exposures' do
|
557
|
+
subject.entity :name, :email do
|
558
|
+
expose :third
|
559
|
+
end
|
560
|
+
subject.entity_class.exposures.size.should == 3
|
561
|
+
end
|
562
|
+
|
563
|
+
context 'instance' do
|
564
|
+
let(:instance){ subject.new }
|
565
|
+
|
566
|
+
describe '#entity' do
|
567
|
+
it 'should be an instance of the entity class' do
|
568
|
+
instance.entity.should be_kind_of(subject.entity_class)
|
569
|
+
end
|
570
|
+
|
571
|
+
it 'should have an object of itself' do
|
572
|
+
instance.entity.object.should == instance
|
573
|
+
end
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
end
|
390
578
|
end
|
391
579
|
end
|
@@ -34,6 +34,16 @@ describe Grape::Middleware::Error do
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
37
|
+
|
38
|
+
# raises a custom error
|
39
|
+
class CustomError < Grape::Exceptions::Base; end
|
40
|
+
class CustomErrorApp
|
41
|
+
class << self
|
42
|
+
def call(env)
|
43
|
+
raise CustomError, :status => 400, :message => 'failed validation'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
37
47
|
|
38
48
|
def app
|
39
49
|
@app
|
@@ -116,6 +126,17 @@ describe Grape::Middleware::Error do
|
|
116
126
|
get '/'
|
117
127
|
last_response.status.should == 401
|
118
128
|
end
|
129
|
+
|
130
|
+
it 'should respond to custom Grape exceptions appropriately' do
|
131
|
+
@app ||= Rack::Builder.app do
|
132
|
+
use Grape::Middleware::Error, :rescue_all => false
|
133
|
+
run CustomErrorApp
|
134
|
+
end
|
135
|
+
|
136
|
+
get '/'
|
137
|
+
last_response.status.should == 400
|
138
|
+
last_response.body.should == 'failed validation'
|
139
|
+
end
|
119
140
|
|
120
141
|
end
|
121
142
|
end
|
@@ -48,6 +48,18 @@ describe Grape::Middleware::Formatter do
|
|
48
48
|
subject.call({'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json'}).last.each{|b| b.should == '{"abc":"def"}'}
|
49
49
|
end
|
50
50
|
|
51
|
+
it 'should serialize objects that respond to #serializable_hash if there is a root element' do
|
52
|
+
class SimpleExample
|
53
|
+
def serializable_hash
|
54
|
+
{:abc => 'def'}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
@body = {"root" => SimpleExample.new}
|
59
|
+
|
60
|
+
subject.call({'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json'}).last.each{|b| b.should == '{"root":{"abc":"def"}}'}
|
61
|
+
end
|
62
|
+
|
51
63
|
it 'should call #to_xml if the content type is xml' do
|
52
64
|
@body = "string"
|
53
65
|
@body.instance_eval do
|
@@ -68,6 +80,13 @@ describe Grape::Middleware::Formatter do
|
|
68
80
|
subject.env['api.format'].should == :json
|
69
81
|
end
|
70
82
|
|
83
|
+
it 'should use the format parameter if one is provided' do
|
84
|
+
subject.call({'PATH_INFO' => '/somewhere','QUERY_STRING' => 'format=json'})
|
85
|
+
subject.env['api.format'].should == :json
|
86
|
+
subject.call({'PATH_INFO' => '/somewhere','QUERY_STRING' => 'format=xml'})
|
87
|
+
subject.env['api.format'].should == :xml
|
88
|
+
end
|
89
|
+
|
71
90
|
it 'should use the default format if none is provided' do
|
72
91
|
subject.call({'PATH_INFO' => '/info'})
|
73
92
|
subject.env['api.format'].should == :txt
|
@@ -2,147 +2,218 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Grape::Middleware::Versioner::Header do
|
4
4
|
let(:app) { lambda{|env| [200, env, env]} }
|
5
|
-
let(:accept) { 'application/vnd.vendor-v1+json' }
|
6
5
|
subject { Grape::Middleware::Versioner::Header.new(app, @options || {}) }
|
7
6
|
|
7
|
+
before do
|
8
|
+
@options = {
|
9
|
+
:version_options => {
|
10
|
+
:using => :header,
|
11
|
+
:vendor => 'vendor',
|
12
|
+
},
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
8
16
|
context 'api.type and api.subtype' do
|
9
|
-
it 'should set
|
10
|
-
env = subject.call('HTTP_ACCEPT' => '*/*')
|
11
|
-
env['api.type'].should eql '
|
12
|
-
env['api.subtype'].should eql '
|
17
|
+
it 'should set type and subtype to first choice of content type if no preference given' do
|
18
|
+
status, _, env = subject.call('HTTP_ACCEPT' => '*/*')
|
19
|
+
env['api.type'].should eql 'application'
|
20
|
+
env['api.subtype'].should eql 'vnd.vendor+xml'
|
21
|
+
status.should == 200
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should set preferred type' do
|
25
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/*')
|
26
|
+
env['api.type'].should eql 'application'
|
27
|
+
env['api.subtype'].should eql 'vnd.vendor+xml'
|
28
|
+
status.should == 200
|
13
29
|
end
|
14
30
|
|
15
31
|
it 'should set preferred type and subtype' do
|
16
|
-
env = subject.call('HTTP_ACCEPT' => 'text/
|
32
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'text/plain')
|
17
33
|
env['api.type'].should eql 'text'
|
18
|
-
env['api.subtype'].should eql '
|
34
|
+
env['api.subtype'].should eql 'plain'
|
35
|
+
status.should == 200
|
19
36
|
end
|
20
37
|
end
|
21
38
|
|
22
39
|
context 'api.format' do
|
23
40
|
it 'should be set' do
|
24
|
-
env = subject.call('HTTP_ACCEPT' =>
|
41
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json')
|
25
42
|
env['api.format'].should eql 'json'
|
43
|
+
status.should == 200
|
26
44
|
end
|
27
45
|
|
28
46
|
it 'should be nil if not provided' do
|
29
|
-
env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor
|
47
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor')
|
30
48
|
env['api.format'].should eql nil
|
49
|
+
status.should == 200
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'when version is set' do
|
53
|
+
before do
|
54
|
+
@options[:versions] = ['v1']
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should be set' do
|
58
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
|
59
|
+
env['api.format'].should eql 'json'
|
60
|
+
status.should == 200
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should be nil if not provided' do
|
64
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
|
65
|
+
env['api.format'].should eql nil
|
66
|
+
status.should == 200
|
67
|
+
end
|
31
68
|
end
|
32
69
|
end
|
33
70
|
|
34
|
-
context '
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
}
|
71
|
+
context 'api.vendor' do
|
72
|
+
it 'should be set' do
|
73
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor')
|
74
|
+
env['api.vendor'].should eql 'vendor'
|
75
|
+
status.should == 200
|
40
76
|
end
|
41
77
|
|
42
|
-
it 'should set
|
43
|
-
env =
|
78
|
+
it 'should be set if format provided' do
|
79
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json')
|
44
80
|
env['api.vendor'].should eql 'vendor'
|
81
|
+
status.should == 200
|
45
82
|
end
|
46
83
|
|
47
|
-
it 'should
|
48
|
-
|
49
|
-
|
84
|
+
it 'should fail with 406 Not Acceptable if vendor is invalid' do
|
85
|
+
expect {
|
86
|
+
env = subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor+json').last
|
87
|
+
}.to throw_symbol(
|
88
|
+
:error,
|
89
|
+
:status => 406,
|
90
|
+
:headers => {'X-Cascade' => 'pass'},
|
91
|
+
:message => 'API vendor or version not found'
|
92
|
+
)
|
50
93
|
end
|
51
|
-
end
|
52
94
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
subject.call('HTTP_ACCEPT' => '').first.should == 200
|
69
|
-
subject.call({}).first.should == 200
|
70
|
-
end
|
71
|
-
|
72
|
-
context 'when strict header versioning is used' do
|
73
|
-
it 'should return a 406 when no header' do
|
74
|
-
@options = {
|
75
|
-
:versions => ['v1'],
|
76
|
-
:version_options => {:using => :header, :strict => true}
|
77
|
-
}
|
78
|
-
expect {
|
79
|
-
env = subject.call('HTTP_ACCEPT' => '').last
|
80
|
-
}.to throw_symbol(
|
81
|
-
:error,
|
82
|
-
:status => 406,
|
83
|
-
:headers => {'X-Cascade' => 'pass'},
|
84
|
-
:message => "406 API Version Not Found"
|
85
|
-
)
|
95
|
+
context 'when version is set' do
|
96
|
+
before do
|
97
|
+
@options[:versions] = ['v1']
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should be set' do
|
101
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
|
102
|
+
env['api.vendor'].should eql 'vendor'
|
103
|
+
status.should == 200
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'should be set if format provided' do
|
107
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
|
108
|
+
env['api.vendor'].should eql 'vendor'
|
109
|
+
status.should == 200
|
86
110
|
end
|
87
111
|
|
88
|
-
it 'should
|
89
|
-
@options = {
|
90
|
-
:versions => ['v1'],
|
91
|
-
:version_options => {:using => :header, :strict => true}
|
92
|
-
}
|
112
|
+
it 'should fail with 406 Not Acceptable if vendor is invalid' do
|
93
113
|
expect {
|
94
|
-
env = subject.call('HTTP_ACCEPT' => '
|
114
|
+
env = subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor-v1+json').last
|
95
115
|
}.to throw_symbol(
|
96
116
|
:error,
|
97
117
|
:status => 406,
|
98
118
|
:headers => {'X-Cascade' => 'pass'},
|
99
|
-
:message =>
|
119
|
+
:message => 'API vendor or version not found'
|
100
120
|
)
|
101
121
|
end
|
102
|
-
|
103
|
-
it 'should return a 200 when proper header is set' do
|
104
|
-
@options = {
|
105
|
-
:versions => ['v1'],
|
106
|
-
:version_options => {:using => :header, :strict => true}
|
107
|
-
}
|
108
|
-
subject.call('HTTP_ACCEPT' => 'application/vnd.testing-v1+json').first.should == 200
|
109
|
-
end
|
110
122
|
end
|
111
|
-
|
112
123
|
end
|
113
124
|
|
114
|
-
context '
|
125
|
+
context 'api.version' do
|
115
126
|
before do
|
116
|
-
@options =
|
117
|
-
:version => ['v1'],
|
118
|
-
:version_options => {:using => :header, :vendor => 'vendor'}
|
119
|
-
}
|
127
|
+
@options[:versions] = ['v1']
|
120
128
|
end
|
121
129
|
|
122
|
-
it 'should
|
123
|
-
status =
|
130
|
+
it 'should be set' do
|
131
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
|
132
|
+
env['api.version'].should eql 'v1'
|
124
133
|
status.should == 200
|
125
134
|
end
|
126
135
|
|
127
|
-
it 'should
|
136
|
+
it 'should be set if format provided' do
|
137
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
|
138
|
+
env['api.version'].should eql 'v1'
|
139
|
+
status.should == 200
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'should fail with 406 Not Acceptable if version is invalid' do
|
128
143
|
expect {
|
129
|
-
env = subject.call('HTTP_ACCEPT' => 'application/vnd.
|
130
|
-
}.to throw_symbol(
|
144
|
+
env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last
|
145
|
+
}.to throw_symbol(
|
146
|
+
:error,
|
147
|
+
:status => 406,
|
148
|
+
:headers => {'X-Cascade' => 'pass'},
|
149
|
+
:message => 'API vendor or version not found'
|
150
|
+
)
|
131
151
|
end
|
132
152
|
end
|
133
153
|
|
134
|
-
|
154
|
+
it 'should succeed if :strict is not set' do
|
155
|
+
subject.call('HTTP_ACCEPT' => '').first.should == 200
|
156
|
+
subject.call({}).first.should == 200
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'should succeed if :strict is set to false' do
|
160
|
+
@options[:version_options][:strict] = false
|
161
|
+
subject.call('HTTP_ACCEPT' => '').first.should == 200
|
162
|
+
subject.call({}).first.should == 200
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'when :strict is set' do
|
135
166
|
before do
|
136
|
-
@options =
|
137
|
-
|
138
|
-
|
139
|
-
|
167
|
+
@options[:versions] = ['v1']
|
168
|
+
@options[:version_options][:strict] = true
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'should fail with 406 Not Acceptable if header is not set' do
|
172
|
+
expect {
|
173
|
+
env = subject.call({}).last
|
174
|
+
}.to throw_symbol(
|
175
|
+
:error,
|
176
|
+
:status => 406,
|
177
|
+
:headers => {'X-Cascade' => 'pass'},
|
178
|
+
:message => 'Accept header must be set'
|
179
|
+
)
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'should fail with 406 Not Acceptable if header is empty' do
|
183
|
+
expect {
|
184
|
+
env = subject.call('HTTP_ACCEPT' => '').last
|
185
|
+
}.to throw_symbol(
|
186
|
+
:error,
|
187
|
+
:status => 406,
|
188
|
+
:headers => {'X-Cascade' => 'pass'},
|
189
|
+
:message => 'Accept header must be set'
|
190
|
+
)
|
140
191
|
end
|
141
192
|
|
142
|
-
it 'should
|
193
|
+
it 'should fail with 406 Not Acceptable if type is a range' do
|
143
194
|
expect {
|
144
|
-
env = subject.call('HTTP_ACCEPT' =>
|
145
|
-
}.to throw_symbol(
|
195
|
+
env = subject.call('HTTP_ACCEPT' => '*/*').last
|
196
|
+
}.to throw_symbol(
|
197
|
+
:error,
|
198
|
+
:status => 406,
|
199
|
+
:headers => {'X-Cascade' => 'pass'},
|
200
|
+
:message => 'Accept header must not contain ranges ("*")'
|
201
|
+
)
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'should fail with 406 Not Acceptable if subtype is a range' do
|
205
|
+
expect {
|
206
|
+
env = subject.call('HTTP_ACCEPT' => 'application/*').last
|
207
|
+
}.to throw_symbol(
|
208
|
+
:error,
|
209
|
+
:status => 406,
|
210
|
+
:headers => {'X-Cascade' => 'pass'},
|
211
|
+
:message => 'Accept header must not contain ranges ("*")'
|
212
|
+
)
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'should succeed if proper header is set' do
|
216
|
+
subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first.should == 200
|
146
217
|
end
|
147
218
|
end
|
148
219
|
end
|