Skip to content

Commit 759b618

Browse files
author
Michael Bleigh
committed
Merge branch 'header_versioning'
2 parents ece1830 + ecbd664 commit 759b618

File tree

16 files changed

+439
-172
lines changed

16 files changed

+439
-172
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

README.markdown

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Grape APIs are Rack applications that are created by subclassing `Grape::API`. B
1515

1616
```ruby
1717
class Twitter::API < Grape::API
18-
version '1'
18+
version 'v1', :using => :header, :vendor => 'twitter', :format => :json
1919

2020
helpers do
2121
def current_user
@@ -68,10 +68,18 @@ run Twitter::API
6868

6969
And would respond to the following routes:
7070

71-
GET /1/statuses/public_timeline(.json)
72-
GET /1/statuses/home_timeline(.json)
73-
GET /1/statuses/show/:id(.json)
74-
POST /1/statuses/update(.json)
71+
GET /statuses/public_timeline(.json)
72+
GET /statuses/home_timeline(.json)
73+
GET /statuses/show/:id(.json)
74+
POST /statuses/update(.json)
75+
76+
Versioning is handled with HTTP Accept head by default, but can be configures
77+
to [use different
78+
strategies](https://github.com/intridea/grape/wiki/API-Versioning). For
79+
example, to request the above with a version, you would make the following
80+
request:
81+
82+
curl -H Accept=application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline
7583

7684
Serialization takes place automatically. For more detailed usage information, please visit the [Grape Wiki](http://github.com/intridea/grape/wiki).
7785

lib/grape.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ module Auth
2121
autoload :Basic, 'grape/middleware/auth/basic'
2222
autoload :Digest, 'grape/middleware/auth/digest'
2323
end
24+
25+
module Versioner
26+
autoload :Path, 'grape/middleware/versioner/path'
27+
autoload :Header, 'grape/middleware/versioner/header'
28+
end
2429
end
2530

2631
module Util

lib/grape/api.rb

Lines changed: 32 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

@@ -248,15 +252,15 @@ def route(methods, paths = ['/'], route_options = {}, &block)
248252
request_method = (method.to_s.upcase unless method == :any)
249253

250254
routes << Route.new(route_options.merge({
251-
:prefix => prefix,
252-
:version => version ? version.join('|') : nil,
253-
:namespace => namespace,
254-
:method => request_method,
255+
:prefix => prefix,
256+
:version => settings[:version] ? settings[:version].join('|') : nil,
257+
:namespace => namespace,
258+
:method => request_method,
255259
:path => prepared_path,
256260
:params => path_params}))
257261

258-
route_set.add_route(endpoint,
259-
:path_info => path,
262+
route_set.add_route(endpoint,
263+
:path_info => path,
260264
:request_method => request_method
261265
)
262266
end
@@ -357,10 +361,18 @@ def build_endpoint(&block)
357361
:format => settings[:error_format] || :txt,
358362
:rescue_options => settings[:rescue_options],
359363
:rescue_handlers => settings[:rescue_handlers] || {}
360-
b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
364+
365+
b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
361366
b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
362-
b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix
363-
b.use Grape::Middleware::Versioner, :versions => (version if version.is_a?(Array)) if version
367+
b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix
368+
369+
if settings[:version]
370+
b.use Grape::Middleware::Versioner.using(settings[:version_options][:using]), {
371+
:versions => settings[:version],
372+
:version_options => settings[:version_options]
373+
}
374+
end
375+
364376
b.use Grape::Middleware::Formatter, :default_format => default_format || :json
365377
middleware.each{|m| b.use *m }
366378

@@ -375,7 +387,6 @@ def build_endpoint(&block)
375387
}, &block)
376388
endpoint.send :include, helpers
377389
b.run endpoint
378-
379390
b.to_app
380391
end
381392

@@ -396,7 +407,7 @@ def route_set
396407
def prepare_path(path)
397408
parts = []
398409
parts << prefix if prefix
399-
parts << ':version' if version
410+
parts << ':version' if settings[:version] && settings[:version_options][:using] == :path
400411
parts << namespace.to_s if namespace
401412
parts << path.to_s if path && '/' != path
402413
parts.last << '(.:format)'
@@ -405,12 +416,12 @@ def prepare_path(path)
405416

406417
def compile_path(path, anchor = true)
407418
endpoint_options = {}
408-
endpoint_options[:version] = /#{version.join('|')}/ if version
419+
endpoint_options[:version] = /#{settings[:version].join('|')}/ if settings[:version]
409420

410421
Rack::Mount::Strexp.compile(prepare_path(path), endpoint_options, %w( / . ? ), anchor)
411422
end
412-
end
413-
414-
reset!
423+
end
424+
425+
reset!
415426
end
416427
end

lib/grape/middleware/versioner.rb

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1-
require 'grape/middleware/base'
2-
1+
# Versioners set env['api.version'] when a version is defined on an API and
2+
# on the requests. The current methods for determining version are:
3+
#
4+
# :header - version from HTTP Accept header.
5+
# :path - version from uri. e.g. /v1/resource
6+
#
7+
# See individual classes for details.
38
module Grape
49
module Middleware
5-
class Versioner < Base
6-
def default_options
7-
{
8-
:pattern => /.*/i
9-
}
10-
end
11-
12-
def before
13-
pieces = env['PATH_INFO'].split('/')
14-
potential_version = pieces[1]
15-
if potential_version =~ options[:pattern]
16-
if options[:versions] && !options[:versions].include?(potential_version)
17-
throw :error, :status => 404, :message => "404 API Version Not Found"
18-
end
19-
20-
truncated_path = "/#{pieces[2..-1].join('/')}"
21-
env['api.version'] = potential_version
22-
env['PATH_INFO'] = truncated_path
10+
module Versioner
11+
extend self
12+
13+
# @param strategy [Symbol] :path or :header
14+
# @return a middleware class based on strategy
15+
def using(strategy)
16+
case strategy
17+
when :path
18+
Path
19+
when :header
20+
Header
21+
else
22+
raise ArgumentError.new("Unknown :using for versioner: #{strategy}")
2323
end
2424
end
2525
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 HTTP Accept header with the pattern:
8+
# application/vnd.:vendor-:version+:format
9+
#
10+
# Example: For request header
11+
# Accept: application/vnd.mycompany-v1+json
12+
#
13+
# The following rack env variables are set:
14+
#
15+
# env['api.type'] => 'application'
16+
# env['api.subtype'] => 'vnd.mycompany-v1+json'
17+
# env['api.vendor] => 'mycompany'
18+
# env['api.version] => 'v1'
19+
# env['api.format] => 'format'
20+
#
21+
# If version does not match this route, then a 404 is throw with
22+
# X-Cascade header to alert Rack::Mount to attempt the next matched
23+
# route.
24+
class Header < Base
25+
def before
26+
accept = env['HTTP_ACCEPT'] || ""
27+
accept.strip.scan(/^(.+?)\/(.+?)$/) do |type, subtype|
28+
env['api.type'] = type
29+
env['api.subtype'] = subtype
30+
31+
subtype.scan(/vnd\.(.+)?-(.+)?\+(.*)?/) do |vendor, version, format|
32+
if options[:versions] && !options[:versions].include?(version)
33+
throw :error, :status => 404, :headers => {'X-Cascade' => 'pass'}, :message => "404 API Version Not Found"
34+
end
35+
36+
env['api.version'] = version
37+
env['api.vendor'] = vendor
38+
env['api.format'] = format # weird that Grape::Middleware::Formatter also does this
39+
end
40+
end
41+
end
42+
end
43+
end
44+
end
45+
end
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 uri path and removes the version substring from the uri
8+
# path. If the version substring does not match any potential initialized
9+
# versions, a 404 error is thrown.
10+
#
11+
# Example: For a uri path
12+
# /v1/resource
13+
#
14+
# The following rack env variables are set and path is rewritten to
15+
# '/resource':
16+
#
17+
# env['api.version'] => 'v1'
18+
#
19+
class Path < Base
20+
def default_options
21+
{
22+
:pattern => /.*/i
23+
}
24+
end
25+
26+
def before
27+
pieces = env['PATH_INFO'].split('/')
28+
potential_version = pieces[1]
29+
if potential_version =~ options[:pattern]
30+
if options[:versions] && !options[:versions].include?(potential_version)
31+
throw :error, :status => 404, :message => "404 API Version Not Found"
32+
end
33+
34+
truncated_path = "/#{pieces[2..-1].join('/')}"
35+
env['api.version'] = potential_version
36+
env['PATH_INFO'] = truncated_path
37+
end
38+
end
39+
end
40+
end
41+
end
42+
end

lib/grape/route.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def method_missing(method_id, *arguments)
1616
end
1717

1818
def to_s
19-
"#{route_method} #{route_path}"
19+
"Version: #{route_version} #{route_method} #{route_path}"
2020
end
2121

2222
end

lib/grape/util/hash_stack.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ def concat(hash_stack)
8282
@stack.concat hash_stack.stack
8383
self
8484
end
85+
86+
def to_s
87+
@stack.to_s
88+
end
8589
end
8690
end
8791
end

0 commit comments

Comments
 (0)