Skip to content

Commit 3ac258c

Browse files
committed
Merge pull request ruby-grape#175 from agworld/feature/parameter_versioning
Grape::API versioning based on request parameter.
2 parents 346fb94 + 0d7f0f2 commit 3ac258c

File tree

8 files changed

+149
-3
lines changed

8 files changed

+149
-3
lines changed

README.markdown

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ end
110110

111111
## Versioning
112112

113-
There are two strategies in which clients can reach your API's endpoints: `:header`
114-
and `:path`. The default strategy is `:header`.
113+
There are three strategies in which clients can reach your API's endpoints: `:header`, `:path` and `:param`. The default strategy is `:header`.
114+
115+
116+
### Header
115117

116118
version 'v1', :using => :header
117119

@@ -124,6 +126,8 @@ supplied. This behavior is similar to routing in Rails. To circumvent this defau
124126
one could use the `:strict` option. When this option is set to `true`, a `404 Not found` error
125127
is returned when no correct Accept header is supplied.
126128

129+
### Path
130+
127131
version 'v1', :using => :path
128132

129133
Using this versioning strategy, clients should pass the desired version in the URL.
@@ -132,6 +136,20 @@ Using this versioning strategy, clients should pass the desired version in the U
132136

133137
Serialization takes place automatically.
134138

139+
### Param
140+
141+
version 'v1', :using => :param
142+
143+
Using this versioning strategy, clients should pass the desired version as a request parameter, either in the URL query string or in the request body.
144+
145+
curl -H http://localhost:9292/events?apiver=v1
146+
147+
The default name for the query parameter is 'apiver' but can be specified using the :parameter option.
148+
149+
version 'v1', :using => :param, :parameter => "v"
150+
curl -H http://localhost:9292/events?v=v1
151+
152+
135153
## Parameters
136154

137155
Parameters are available through the `params` hash object. This includes `GET` and `POST` parameters,

lib/grape.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module Auth
2626
module Versioner
2727
autoload :Path, 'grape/middleware/versioner/path'
2828
autoload :Header, 'grape/middleware/versioner/header'
29+
autoload :Param, 'grape/middleware/versioner/param'
2930
end
3031
end
3132

lib/grape/middleware/versioner.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ def using(strategy)
1818
Path
1919
when :header
2020
Header
21+
when :param
22+
Param
2123
else
2224
raise ArgumentError.new("Unknown :using for versioner: #{strategy}")
2325
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require 'grape/middleware/base'
2+
3+
module Grape
4+
module Middleware
5+
module Versioner
6+
# This middleware sets various version related rack environment variables
7+
# based on the request parameters and removes that parameter from the
8+
# request parameters for subsequent middleware and API.
9+
# If the version substring does not match any potential initialized
10+
# versions, a 404 error is thrown.
11+
# If the version substring is not passed the version (highest mounted)
12+
# version will be used.
13+
#
14+
# Example: For a uri path
15+
# /resource?apiver=v1
16+
#
17+
# The following rack env variables are set and path is rewritten to
18+
# '/resource':
19+
#
20+
# env['api.version'] => 'v1'
21+
class Param < Base
22+
def default_options
23+
{
24+
:parameter => "apiver"
25+
}
26+
end
27+
28+
def before
29+
paramkey = options[:parameter]
30+
potential_version = request.params[paramkey]
31+
32+
unless potential_version.nil?
33+
if options[:versions] && !options[:versions].include?(potential_version)
34+
throw :error, :status => 404, :message => "404 API Version Not Found", :headers => {'X-Cascade' => 'pass'}
35+
end
36+
env['api.version'] = potential_version
37+
env['rack.request.query_hash'].delete(paramkey)
38+
end
39+
end
40+
41+
end
42+
end
43+
end
44+
end

spec/grape/api_spec.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ def app; subject end
3131
end
3232
end
3333

34+
describe '.version using param' do
35+
it_should_behave_like 'versioning' do
36+
let(:macro_options) do
37+
{
38+
:using => :param,
39+
:parameter => "apiver"
40+
}
41+
end
42+
end
43+
end
44+
3445
describe '.version using header' do
3546
it_should_behave_like 'versioning' do
3647
let(:macro_options) do
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
require 'spec_helper'
2+
3+
describe Grape::Middleware::Versioner::Param do
4+
5+
let(:app) { lambda{|env| [200, env, env['api.version']]} }
6+
subject { Grape::Middleware::Versioner::Param.new(app, @options || {}) }
7+
8+
it 'should set the API version based on the default param (apiver)' do
9+
env = Rack::MockRequest.env_for("/awesome", {:params => {"apiver" => "v1"}})
10+
subject.call(env)[1]["api.version"].should == 'v1'
11+
end
12+
13+
it 'should cut (only) the version out of the params', :focus => true do
14+
env = Rack::MockRequest.env_for("/awesome", {:params => {"apiver" => "v1", "other_param" => "5"}})
15+
subject.call(env)[1]['rack.request.query_hash']["apiver"].should be_nil
16+
subject.call(env)[1]['rack.request.query_hash']["other_param"].should == "5"
17+
end
18+
19+
it 'should provide a nil version if no version is given' do
20+
env = Rack::MockRequest.env_for("/")
21+
subject.call(env).last.should be_nil
22+
end
23+
24+
context 'with specified parameter name' do
25+
before{ @options = {:parameter => ['v']}}
26+
it 'should set the API version based on the custom parameter name' do
27+
env = Rack::MockRequest.env_for("/awesome", {:params => {"v" => "v1"}})
28+
s = subject.call(env)[1]["api.version"] == "v1"
29+
end
30+
it 'should not set the API version based on the default param' do
31+
env = Rack::MockRequest.env_for("/awesome", {:params => {"apiver" => "v1"}})
32+
s = subject.call(env)[1]["api.version"] == nil
33+
end
34+
end
35+
36+
context 'with specified versions' do
37+
before{ @options = {:versions => ['v1', 'v2']}}
38+
it 'should throw an error if a non-allowed version is specified' do
39+
env = Rack::MockRequest.env_for("/awesome", {:params => {"apiver" => "v3"}})
40+
catch(:error){subject.call(env)}[:status].should == 404
41+
end
42+
43+
it 'should allow versions that have been specified' do
44+
env = Rack::MockRequest.env_for("/awesome", {:params => {"apiver" => "v1"}})
45+
subject.call(env)[1]["api.version"].should == 'v1'
46+
end
47+
end
48+
49+
it 'should return a 200 when no version is set (matches the first version found)' do
50+
@options = {
51+
:versions => ['v1'],
52+
:version_options => {:using => :header}
53+
}
54+
env = Rack::MockRequest.env_for("/awesome", {:params => {}})
55+
subject.call(env).first.should == 200
56+
end
57+
58+
end

spec/grape/middleware/versioner_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@
99
it 'should recognize :header' do
1010
klass.using(:header).should == Grape::Middleware::Versioner::Header
1111
end
12+
13+
it 'should recognize :param' do
14+
klass.using(:param).should == Grape::Middleware::Versioner::Param
15+
end
1216
end

spec/support/versioned_helpers.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ def versioned_path(options = {})
66
case options[:using]
77
when :path
88
File.join('/', options[:prefix] || '', options[:version], options[:path])
9+
when :param
10+
File.join('/', options[:prefix] || '', options[:path])
911
when :header
1012
File.join('/', options[:prefix] || '', options[:path])
1113
else
@@ -17,6 +19,8 @@ def versioned_headers(options)
1719
case options[:using]
1820
when :path
1921
{} # no-op
22+
when :param
23+
{} # no-op
2024
when :header
2125
{
2226
'HTTP_ACCEPT' => "application/vnd.#{options[:vendor]}-#{options[:version]}+#{options[:format]}"
@@ -29,6 +33,10 @@ def versioned_headers(options)
2933
def versioned_get(path, version_name, version_options = {})
3034
path = versioned_path(version_options.merge(:version => version_name, :path => path))
3135
headers = versioned_headers(version_options.merge(:version => version_name))
32-
get path, {}, headers
36+
params = {}
37+
if version_options[:using] == :param
38+
params = { version_options[:parameter] => version_name }
39+
end
40+
get path, params, headers
3341
end
3442

0 commit comments

Comments
 (0)