Skip to content

Commit 0d7f0f2

Browse files
author
Jack Casey
committed
Grape::API versioning based on request parameter.
Added an API versioner which searches for a named parameter (default is 'apiver') to find the specified api version. The name of the parameter can be changed via the 'parameter' option when specifying the version. Include all the same specs as already existed for the other versioners. Update README markdown to explain this new versioning strategy.
1 parent 346fb94 commit 0d7f0f2

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)