Skip to content

Commit

Permalink
Merge pull request #403 from politician/feature/support-accept-versio…
Browse files Browse the repository at this point in the history
…n-header

add support for versioning using the 'Accept-Version' header
  • Loading branch information
dblock committed May 10, 2013
2 parents 3fd12c1 + ec50901 commit 675cb9e
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Next Release
* [#392](https://github.com/intridea/grape/pull/392): Extracted headers and params from `Endpoint` to `Grape::Request` - [@niedhui](https://github.com/niedhui).
* [#394](https://github.com/intridea/grape/pull/394): Path version no longer overwrites a `version` parameter - [@tmornini](https://github.com/tmornini).
* [#390](https://github.com/intridea/grape/pull/390): Added default value for an `optional` parameter - [@oivoodoo](https://github.com/oivoodoo).
* [#403](https://github.com/intridea/grape/pull/403): Added support for versioning using the 'Accept-Version' header - [@politician](https://github.com/politician).
* Your contribution here.

0.4.1 (4/1/2013)
Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ end

## Versioning

There are three strategies in which clients can reach your API's endpoints: `:path`,
`:header` and `:param`. The default strategy is `:path`.
There are four strategies in which clients can reach your API's endpoints: `:path`,
`:header`, `:accept_version_header` and `:param`. The default strategy is `:path`.

### Path

Expand All @@ -233,6 +233,21 @@ supplied. This behavior is similar to routing in Rails. To circumvent this defau
one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error
is returned when no correct `Accept` header is supplied.

### Accept-Version Header

```ruby
version 'v1', using: :accept_version_header
```

Using this versioning strategy, clients should pass the desired version in the HTTP `Accept-Version` header.

curl -H "Accept-Version=v1" http://localhost:9292/statuses/public_timeline

By default, the first matching version is used when no `Accept-Version` header is
supplied. This behavior is similar to routing in Rails. To circumvent this default behavior,
one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error
is returned when no correct `Accept` header is supplied.

### Param

```ruby
Expand Down
7 changes: 4 additions & 3 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ module Auth
end

module Versioner
autoload :Path, 'grape/middleware/versioner/path'
autoload :Header, 'grape/middleware/versioner/header'
autoload :Param, 'grape/middleware/versioner/param'
autoload :Path, 'grape/middleware/versioner/path'
autoload :Header, 'grape/middleware/versioner/header'
autoload :Param, 'grape/middleware/versioner/param'
autoload :AcceptVersionHeader, 'grape/middleware/versioner/accept_version_header'
end
end

Expand Down
2 changes: 2 additions & 0 deletions lib/grape/middleware/versioner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def using(strategy)
Header
when :param
Param
when :accept_version_header
AcceptVersionHeader
else
raise Grape::Exceptions::InvalidVersionerOption.new(strategy)
end
Expand Down
67 changes: 67 additions & 0 deletions lib/grape/middleware/versioner/accept_version_header.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require 'grape/middleware/base'

module Grape
module Middleware
module Versioner
# This middleware sets various version related rack environment variables
# based on the HTTP Accept-Version header
#
# Example: For request header
# Accept-Version: v1
#
# The following rack env variables are set:
#
# env['api.version] => 'v1'
#
# If version does not match this route, then a 406 is raised with
# X-Cascade header to alert Rack::Mount to attempt the next matched
# route.
class AcceptVersionHeader < Base

def before
potential_version = (env['HTTP_ACCEPT_VERSION'] || '').strip

if strict?
# If no Accept-Version header:
if potential_version.empty?
throw :error, :status => 406, :headers => error_headers, :message => 'Accept-Version header must be set.'
end
end

unless potential_version.empty?
# If the requested version is not supported:
if !versions.any? { |v| v.to_s == potential_version }
throw :error, :status => 406, :headers => error_headers, :message => 'The requested version is not supported.'
end

env['api.version'] = potential_version
end
end

private

def versions
options[:versions] || []
end

def strict?
options[:version_options] && options[:version_options][:strict]
end

# By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
# of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
def cascade?
options[:version_options] && options[:version_options].has_key?(:cascade) ?
!! options[:version_options][:cascade] :
true
end

def error_headers
cascade? ? { 'X-Cascade' => 'pass' } : {}
end

end
end
end
end
17 changes: 17 additions & 0 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ def app; subject end
# pending 'routes if any media type is allowed'
end

describe '.version using accept_version_header' do
it_should_behave_like 'versioning' do
let(:macro_options) do
{
:using => :accept_version_header
}
end
end
end

describe '.represent' do
it 'requires a :with option' do
expect{ subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent)
Expand Down Expand Up @@ -275,6 +285,13 @@ def subject.enable_root_route!
versioned_get "/", "v1", :using => :param
end

it 'Accept-Version header versioned APIs' do
subject.version 'v1', :using => :accept_version_header
subject.enable_root_route!

versioned_get "/", "v1", :using => :accept_version_header
end

it 'unversioned APIs' do
subject.enable_root_route!

Expand Down
121 changes: 121 additions & 0 deletions spec/grape/middleware/versioner/accept_version_header_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
require 'spec_helper'

describe Grape::Middleware::Versioner::AcceptVersionHeader do
let(:app) { lambda{|env| [200, env, env]} }
subject { Grape::Middleware::Versioner::AcceptVersionHeader.new(app, @options || {}) }

before do
@options = {
:version_options => {
:using => :accept_version_header
},
}
end

context 'api.version' do
before do
@options[:versions] = ['v1']
end

it 'is set' do
status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1')
env['api.version'].should eql 'v1'
status.should == 200
end

it 'is set if format provided' do
status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1')
env['api.version'].should eql 'v1'
status.should == 200
end

it 'fails with 406 Not Acceptable if version is not supported' do
expect {
env = subject.call('HTTP_ACCEPT_VERSION' => 'v2').last
}.to throw_symbol(
:error,
:status => 406,
:headers => {'X-Cascade' => 'pass'},
:message => 'The requested version is not supported.'
)
end
end

it 'succeeds if :strict is not set' do
subject.call('HTTP_ACCEPT_VERSION' => '').first.should == 200
subject.call({}).first.should == 200
end

it 'succeeds if :strict is set to false' do
@options[:version_options][:strict] = false
subject.call('HTTP_ACCEPT_VERSION' => '').first.should == 200
subject.call({}).first.should == 200
end

context 'when :strict is set' do
before do
@options[:versions] = ['v1']
@options[:version_options][:strict] = true
end

it 'fails with 406 Not Acceptable if header is not set' do
expect {
env = subject.call({}).last
}.to throw_symbol(
:error,
:status => 406,
:headers => {'X-Cascade' => 'pass'},
:message => 'Accept-Version header must be set.'
)
end

it 'fails with 406 Not Acceptable if header is empty' do
expect {
env = subject.call('HTTP_ACCEPT_VERSION' => '').last
}.to throw_symbol(
:error,
:status => 406,
:headers => {'X-Cascade' => 'pass'},
:message => 'Accept-Version header must be set.'
)
end

it 'succeeds if proper header is set' do
subject.call('HTTP_ACCEPT_VERSION' => 'v1').first.should == 200
end
end

context 'when :strict and :cascade=>false are set' do
before do
@options[:versions] = ['v1']
@options[:version_options][:strict] = true
@options[:version_options][:cascade] = false
end

it 'fails with 406 Not Acceptable if header is not set' do
expect {
env = subject.call({}).last
}.to throw_symbol(
:error,
:status => 406,
:headers => {},
:message => 'Accept-Version header must be set.'
)
end

it 'fails with 406 Not Acceptable if header is empty' do
expect {
env = subject.call('HTTP_ACCEPT_VERSION' => '').last
}.to throw_symbol(
:error,
:status => 406,
:headers => {},
:message => 'Accept-Version header must be set.'
)
end

it 'succeeds if proper header is set' do
subject.call('HTTP_ACCEPT_VERSION' => 'v1').first.should == 200
end
end
end
5 changes: 4 additions & 1 deletion spec/grape/middleware/versioner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@
klass.using(:param).should == Grape::Middleware::Versioner::Param
end

end
it 'recognizes :accept_version_header' do
klass.using(:accept_version_header).should == Grape::Middleware::Versioner::AcceptVersionHeader
end
end
6 changes: 6 additions & 0 deletions spec/support/versioned_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ def versioned_path(options = {})
File.join('/', options[:prefix] || '', options[:path])
when :header
File.join('/', options[:prefix] || '', options[:path])
when :accept_version_header
File.join('/', options[:prefix] || '', options[:path])
else
raise ArgumentError.new("unknown versioning strategy: #{options[:using]}")
end
Expand All @@ -25,6 +27,10 @@ def versioned_headers(options)
{
'HTTP_ACCEPT' => "application/vnd.#{options[:vendor]}-#{options[:version]}+#{options[:format]}"
}
when :accept_version_header
{
'HTTP_ACCEPT_VERSION' => "#{options[:version]}"
}
else
raise ArgumentError.new("unknown versioning strategy: #{options[:using]}")
end
Expand Down

0 comments on commit 675cb9e

Please sign in to comment.