Skip to content

Commit 14b477c

Browse files
author
Jerry Cheung
committed
Grape::API versioning based on http accept header
1 parent bc4b63f commit 14b477c

File tree

3 files changed

+81
-106
lines changed

3 files changed

+81
-106
lines changed

Guardfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
guard 'rspec', :version => 2 do
55
watch(%r{^spec/.+_spec\.rb$})
66
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7+
watch(%r{^spec/support/shared_versioning_examples.rb$}) { |m| "spec/" }
78
watch('spec/spec_helper.rb') { "spec/" }
89
end
910

lib/grape/api.rb

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,16 @@ def prefix(prefix = nil)
7272
# end
7373
# end
7474
#
75-
def version(*new_versions, &block)
76-
if new_versions.any?
77-
@versions = versions | new_versions
78-
nest(block) { set(:version, new_versions) }
79-
else
80-
settings[:version]
75+
def version(*args, &block)
76+
if args.any?
77+
options = args.pop if args.last.is_a? Hash
78+
options ||= {}
79+
options = {:using => :header}.merge!(options)
80+
@versions = versions | args
81+
nest(block) do
82+
set(:version, args)
83+
set(:version_options, options)
84+
end
8185
end
8286
end
8387

@@ -226,7 +230,8 @@ def route(methods, paths = ['/'], route_options = {}, &block)
226230
endpoint = build_endpoint(&block)
227231

228232
endpoint_options = {}
229-
endpoint_options[:version] = /#{version.join('|')}/ if version
233+
# TODO: preferably versioning would be handled in a central point
234+
endpoint_options[:version] = /#{settings[:version].join('|')}/ if settings[:version] && settings[:version_options][:using] == :path
230235

231236
route_options ||= {}
232237

@@ -241,15 +246,15 @@ def route(methods, paths = ['/'], route_options = {}, &block)
241246
request_method = (method.to_s.upcase unless method == :any)
242247

243248
routes << Route.new(route_options.merge({
244-
:prefix => prefix,
245-
:version => version ? version.join('|') : nil,
246-
:namespace => namespace,
247-
:method => request_method,
249+
:prefix => prefix,
250+
:version => settings[:version] ? settings[:version].join('|') : nil,
251+
:namespace => namespace,
252+
:method => request_method,
248253
:path => compiled_path,
249254
:params => path_params}))
250255

251-
route_set.add_route(endpoint,
252-
:path_info => path,
256+
route_set.add_route(endpoint,
257+
:path_info => path,
253258
:request_method => request_method
254259
)
255260
end
@@ -350,10 +355,18 @@ def build_endpoint(&block)
350355
:format => settings[:error_format] || :txt,
351356
:rescue_options => settings[:rescue_options],
352357
:rescue_handlers => settings[:rescue_handlers] || {}
353-
b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
358+
359+
b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
354360
b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
355-
b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix
356-
b.use Grape::Middleware::Versioner, :versions => (version if version.is_a?(Array)) if version
361+
b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix
362+
363+
if settings[:version]
364+
b.use Grape::Middleware::Versioner.using(settings[:version_options][:using]), {
365+
:versions => settings[:version],
366+
:version_options => settings[:version_options]
367+
}
368+
end
369+
357370
b.use Grape::Middleware::Formatter, :default_format => default_format || :json
358371
middleware.each{|m| b.use *m }
359372

@@ -368,7 +381,6 @@ def build_endpoint(&block)
368381
}, &block)
369382
endpoint.send :include, helpers
370383
b.run endpoint
371-
372384
b.to_app
373385
end
374386

@@ -389,14 +401,14 @@ def route_set
389401
def compile_path(path)
390402
parts = []
391403
parts << prefix if prefix
392-
parts << ':version' if version
404+
parts << ':version' if settings[:version] && settings[:version_options][:using] == :path
393405
parts << namespace.to_s if namespace
394406
parts << path.to_s if path && '/' != path
395407
parts.last << '(.:format)'
396408
Rack::Mount::Utils.normalize_path(parts.join('/'))
397409
end
398-
end
399-
400-
reset!
410+
end
411+
412+
reset!
401413
end
402414
end

spec/grape/api_spec.rb

Lines changed: 47 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'spec_helper'
2+
require 'shared_versioning_examples'
23

34
describe Grape::API do
45
subject { Class.new(Grape::API) }
@@ -21,84 +22,44 @@ def app; subject end
2122
end
2223
end
2324

24-
describe '.version' do
25-
it 'should set the API version' do
26-
subject.version 'v1'
27-
subject.get :hello do
28-
"Version: #{request.env['api.version']}"
25+
describe '.version using path' do
26+
it_should_behave_like 'versioning' do
27+
let(:macro_options) do
28+
{
29+
:using => :path
30+
}
2931
end
30-
31-
get '/v1/hello'
32-
last_response.body.should eql "Version: v1"
3332
end
34-
35-
it 'should add the prefix before the API version' do
36-
subject.prefix 'api'
37-
subject.version 'v1'
38-
subject.get :hello do
39-
"Version: #{request.env['api.version']}"
40-
end
41-
42-
get '/api/v1/hello'
43-
last_response.body.should eql "Version: v1"
44-
end
45-
46-
it 'should be able to specify version as a nesting' do
47-
subject.version 'v2'
48-
subject.get '/awesome' do
49-
"Radical"
50-
end
51-
52-
subject.version 'v1' do
53-
get '/legacy' do
54-
"Totally"
55-
end
33+
end
34+
35+
describe '.version using header' do
36+
it_should_behave_like 'versioning' do
37+
let(:macro_options) do
38+
{
39+
:using => :header,
40+
:vendor => 'mycompany',
41+
:format => 'json'
42+
}
5643
end
57-
58-
get '/v1/awesome'
59-
last_response.status.should eql 404
60-
get '/v2/awesome'
61-
last_response.status.should eql 200
62-
get '/v1/legacy'
63-
last_response.status.should eql 200
64-
get '/v2/legacy'
65-
last_response.status.should eql 404
6644
end
67-
68-
it 'should be able to specify multiple versions' do
69-
subject.version 'v1', 'v2'
70-
subject.get 'awesome' do
71-
"I exist"
72-
end
73-
74-
get '/v1/awesome'
75-
last_response.status.should eql 200
76-
get '/v2/awesome'
77-
last_response.status.should eql 200
78-
get '/v3/awesome'
79-
last_response.status.should eql 404
45+
46+
# Behavior as defined by rfc2616 when no header is defined
47+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
48+
describe 'no specified accept header' do
49+
# subject.version 'v1', :using => :header
50+
# subject.get '/hello' do
51+
# 'hello'
52+
# end
53+
54+
# it 'should route' do
55+
# get '/hello'
56+
# last_response.status.should eql 200
57+
# end
8058
end
81-
82-
it 'should allow the same endpoint to be implemented for different versions' do
83-
subject.version 'v2'
84-
subject.get 'version' do
85-
request.env['api.version']
86-
end
87-
88-
subject.version 'v1' do
89-
get 'version' do
90-
"version " + request.env['api.version']
91-
end
92-
end
59+
60+
it 'should route if any media type is allowed' do
9361

94-
get '/v2/version'
95-
last_response.status.should == 200
96-
last_response.body.should == 'v2'
97-
get '/v1/version'
98-
last_response.status.should == 200
99-
last_response.body.should == 'version v1'
10062
end
101-
10263
end
10364

10465
describe '.represent' do
@@ -122,7 +83,7 @@ def app; subject end
12283

12384
it 'should come after the prefix and version' do
12485
subject.prefix :rad
125-
subject.version :v1
86+
subject.version :v1, :using => :path
12687

12788
subject.namespace :awesome do
12889
compile_path('hello').should == '/rad/:version/awesome/hello(.:format)'
@@ -162,7 +123,7 @@ def app; subject end
162123

163124
it 'should be callable with nil just to push onto the stack' do
164125
subject.namespace do
165-
version 'v2'
126+
version 'v2', :using => :path
166127
compile_path('hello').should == '/:version/hello(.:format)'
167128
end
168129
subject.send(:compile_path, 'hello').should == '/hello(.:format)'
@@ -536,28 +497,29 @@ def two
536497
end
537498

538499
describe '.scope' do
500+
# TODO: refactor this to not be tied to versioning. How about a generic
501+
# .setting macro?
539502
it 'should scope the various settings' do
540-
subject.version 'v2'
541-
503+
subject.prefix 'new'
504+
542505
subject.scope :legacy do
543-
version 'v1'
544-
506+
prefix 'legacy'
545507
get '/abc' do
546-
version
508+
'abc'
547509
end
548510
end
549511

550512
subject.get '/def' do
551-
version
513+
'def'
552514
end
553515

554-
get '/v2/abc'
516+
get '/new/abc'
555517
last_response.status.should eql 404
556-
get '/v1/abc'
518+
get '/legacy/abc'
557519
last_response.status.should eql 200
558-
get '/v1/def'
520+
get '/legacy/def'
559521
last_response.status.should eql 404
560-
get '/v2/def'
522+
get '/new/def'
561523
last_response.status.should eql 200
562524
end
563525
end
@@ -693,12 +655,12 @@ def two
693655
describe "api structure with two versions and a namespace" do
694656
class TwitterAPI < Grape::API
695657
# version v1
696-
version 'v1'
658+
version 'v1', :using => :path
697659
get "version" do
698660
api.version
699661
end
700662
# version v2
701-
version 'v2'
663+
version 'v2', :using => :path
702664
prefix 'p'
703665
namespace "n1" do
704666
namespace "n2" do

0 commit comments

Comments
 (0)