Skip to content

Commit b544913

Browse files
committed
Refactored api structure madness into api route collection.
1 parent 4819b59 commit b544913

File tree

5 files changed

+107
-138
lines changed

5 files changed

+107
-138
lines changed

README.markdown

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,20 +83,17 @@ you simply use the `rescue_from` method inside your API declaration:
8383
rescue_from ArgumentError, NotImplementedError # :all for all errors
8484
end
8585

86-
## API Structure
86+
## Inspecting an API
8787

88-
Grape exposes the API structure as a hash. The hash keys are API versions (`:default` when version is omitted). When namespaces are present,
89-
each value is a hash of namespaces with paths as values. When namespaces are not present, each value is an array of paths. Each path is a hash
90-
containing two keys, `:method` which holds a string containing the path's HTTP request type, and `:path` which holds a string representing the
91-
path. The structure is retrieved via the `structure` method.
88+
Grape exposes arrays of API versions and compiled routes. Each route contains a prefix, version, namespace, method and path.
9289

9390
class TwitterAPI < Grape::API
9491

9592
version 'v1'
9693
get "version" do
9794
api.version
9895
end
99-
96+
10097
version 'v2'
10198
namespace "ns" do
10299
get "version" do
@@ -106,22 +103,15 @@ path. The structure is retrieved via the `structure` method.
106103

107104
end
108105

109-
Yields the following `TwitterAPI::structure`.
110-
111-
{
112-
"v1" => [
113-
{ :method=>"GET", :path=>"version(.:format)"} ],
114-
"v2"=> {
115-
"ns"=>[ { :method=>"GET", :path=>"version(.:format)" }]
116-
}
117-
}
106+
TwitterAPI::versions # yields [ 'v1', 'v2' ]
107+
TwitterAPI::routes # yields an array of Grape::Route objects
118108

119109
## Note on Patches/Pull Requests
120110

121111
* Fork the project.
122112
* Make your feature addition or bug fix.
123113
* Add tests for it. This is important so I don't break it in a future version unintentionally.
124-
* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
114+
* Commit, do not mess with Rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
125115
* Send me a pull request. Bonus points for topic branches.
126116

127117
## Copyright

lib/grape.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module Grape
66
autoload :Endpoint, 'grape/endpoint'
77
autoload :MiddlewareStack, 'grape/middleware_stack'
88
autoload :Client, 'grape/client'
9+
autoload :Route, 'grape/route'
910

1011
module Middleware
1112
autoload :Base, 'grape/middleware/base'

lib/grape/api.rb

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ module Grape
1010
class API
1111
class << self
1212
attr_reader :route_set
13-
attr_reader :structure
13+
attr_reader :versions
14+
attr_reader :routes
1415

1516
def logger
1617
@logger ||= Logger.new($STDOUT)
@@ -71,7 +72,12 @@ def prefix(prefix = nil)
7172
# end
7273
#
7374
def version(*new_versions, &block)
74-
new_versions.any? ? nest(block){ set(:version, new_versions) } : settings[:version]
75+
if new_versions.any?
76+
@versions = versions | new_versions
77+
nest(block) { set(:version, new_versions) }
78+
else
79+
settings[:version]
80+
end
7581
end
7682

7783
# Specify the default format for the API's
@@ -180,52 +186,27 @@ def route(methods, paths, &block)
180186
methods = Array(methods)
181187
paths = ['/'] if paths == []
182188
paths = Array(paths)
183-
endpoint = build_endpoint(&block)
189+
endpoint = build_endpoint(&block)
184190
options = {}
191+
185192
options[:version] = /#{version.join('|')}/ if version
186193

187194
methods.each do |method|
188195
paths.each do |path|
189-
path = Rack::Mount::Strexp.compile(compile_path(path), options, %w( / . ? ), true)
196+
compiled_path = compile_path(path)
197+
path = Rack::Mount::Strexp.compile(compiled_path, options, %w( / . ? ), true)
198+
request_method = (method.to_s.upcase unless method == :any)
199+
routes << Route.new(prefix,
200+
version ? version.join('|') : nil,
201+
namespace,
202+
request_method,
203+
compiled_path)
190204
route_set.add_route(endpoint,
191205
:path_info => path,
192-
:request_method => (method.to_s.upcase unless method == :any)
206+
:request_method => request_method
193207
)
194208
end
195209
end
196-
197-
# create API structure
198-
(version || [ :default ]).each { |v|
199-
200-
ms = []
201-
methods.each { |m|
202-
paths.each { |p|
203-
ms << {
204-
:method => m,
205-
:path => p.to_s
206-
}
207-
}
208-
}
209-
210-
if namespace == '/'
211-
if structure[v].is_a?(Hash)
212-
structure[v][:default] ||= (structure[v][:default] || {}).merge(structure[v])
213-
if (structure[v][:default].is_a?(Hash))
214-
structure[v][:default] = [ structure[v][:default], ms ]
215-
else
216-
structure[v][:default] |= ms
217-
end
218-
else
219-
structure[v] ||= []
220-
structure[v] |= ms
221-
end
222-
else
223-
structure[v] = { :default => structure[v] } if structure[v].is_a?(Array)
224-
structure[v] ||= {}
225-
structure[v][namespace[1..namespace.length - 1].to_s] = ms
226-
end
227-
}
228-
229210
end
230211

231212
def get(*paths, &block); route('GET', paths, &block) end
@@ -272,10 +253,13 @@ def middleware
272253
settings_stack.inject([]){|a,s| a += s[:middleware] if s[:middleware]; a}
273254
end
274255

275-
# API structure contains a hash of API versions to API methods.
276-
# If the API is not versioned, the hash contains a single :current key.
277-
def structure
278-
@structure ||= {}
256+
# An array of API routes.
257+
def routes
258+
@routes ||= []
259+
end
260+
261+
def versions
262+
@versions ||= []
279263
end
280264

281265
protected
@@ -305,7 +289,7 @@ def build_endpoint(&block)
305289
:rescue_options => settings[:rescue_options]
306290
b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
307291
b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
308-
b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix
292+
b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix
309293
b.use Grape::Middleware::Versioner, :versions => (version if version.is_a?(Array)) if version
310294
b.use Grape::Middleware::Formatter, :default_format => default_format || :json
311295
middleware.each{|m| b.use *m }

lib/grape/route.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module Grape
2+
3+
# A compiled route for inspection.
4+
class Route
5+
6+
attr_reader :prefix
7+
attr_reader :version
8+
attr_reader :namespace
9+
attr_reader :method
10+
attr_reader :path
11+
12+
def initialize(prefix, version, namespace, method, path)
13+
@prefix = prefix
14+
@version = version
15+
@namespace = namespace
16+
@method = method
17+
@path = path
18+
end
19+
20+
def to_s
21+
"#{method} #{path}"
22+
end
23+
24+
end
25+
end

spec/grape/api_spec.rb

Lines changed: 48 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -618,94 +618,63 @@ def two
618618
end
619619
end
620620

621-
describe "simple version structure" do
622-
before(:each) do
623-
subject.get :hello do
624-
"No version"
621+
context "routes" do
622+
describe "empty api structure" do
623+
it "returns an empty array of routes" do
624+
subject.routes.should == []
625+
end
626+
end
627+
describe "single method api structure" do
628+
before(:each) do
629+
subject.get :ping do
630+
'pong'
631+
end
625632
end
626-
end
627-
it "versions api" do
628-
get '/hello'
629-
last_response.body.should eql "No version"
630-
end
631-
it "returns an array of versions" do
632-
subject.structure.should ==
633-
{
634-
:default => [ { :method => "GET", :path => "hello" } ],
635-
}
636-
end
637-
end
638-
639-
describe "twitter versions" do
640-
class TwitterAPI < Grape::API
641-
version 'v1'
642-
get "version" do
643-
api.version
644-
end
645-
version 'v2'
646-
namespace "ns" do
647-
get "version" do
633+
it "returns one route" do
634+
subject.routes.size.should == 1
635+
route = subject.routes[0]
636+
route.version.should be_nil
637+
route.path.should == "/ping(.:format)"
638+
route.method.should == "GET"
639+
end
640+
end
641+
describe "api structure with two versions and a namespace" do
642+
class TwitterAPI < Grape::API
643+
# version v1
644+
version 'v1'
645+
get "version" do
648646
api.version
649647
end
648+
# version v2
649+
version 'v2'
650+
prefix 'p'
651+
namespace "n1" do
652+
namespace "n2" do
653+
get "version" do
654+
api.version
655+
end
656+
end
657+
end
650658
end
651-
end
652-
it "should return a single structure with both versions" do
653-
TwitterAPI::structure.should == {
654-
"v1"=>[{:method=>"GET", :path=>"version(.:format)"}],
655-
"v2"=>{
656-
"ns"=>[{:method=>"GET", :path=>"version(.:format)"}]
657-
}}
658-
end
659-
end
660-
661-
describe "nested versions" do
662-
before(:each) do
663-
subject.get :hello do
664-
"No version"
665-
end
666-
subject.version 'v1'
667-
subject.get :hello do
668-
"Version: #{request.env['api.version']}"
659+
it "should return versions" do
660+
TwitterAPI::versions.should == [ 'v1', 'v2' ]
669661
end
670-
subject.version 'v2'
671-
subject.get :hello do
672-
"Version: #{request.env['api.version']} w/o namespace"
662+
it "should set route paths" do
663+
TwitterAPI::routes.size.should == 2
664+
TwitterAPI::routes[0].path.should == "/:version/version(.:format)"
665+
TwitterAPI::routes[1].path.should == "/p/:version/n1/n2/version(.:format)"
673666
end
674-
subject.namespace :ns1 do
675-
get :hello do
676-
"Version: #{request.env['api.version']}"
677-
end
667+
it "should set route versions" do
668+
TwitterAPI::routes[0].version.should == 'v1'
669+
TwitterAPI::routes[1].version.should == 'v2'
678670
end
679-
subject.namespace :ns2 do
680-
get :hello do
681-
"Version: #{request.env['api.version']}"
682-
end
671+
it "should set a nested namespace" do
672+
TwitterAPI::routes[1].namespace.should == "/n1/n2"
683673
end
684-
subject.post :goodbye do
685-
"Version: #{request.env['api.version']} w/o namespace"
674+
it "should set prefix" do
675+
TwitterAPI::routes[1].prefix.should == 'p'
686676
end
687677
end
688-
it "versions api" do
689-
get '/hello'
690-
last_response.body.should eql "No version"
691-
get '/v1/hello'
692-
last_response.body.should eql "Version: v1"
693-
get '/v2/ns1/hello'
694-
last_response.body.should eql "Version: v2"
695-
get '/v2/ns2/hello'
696-
last_response.body.should eql "Version: v2"
697-
end
698-
it "returns an array of versions" do
699-
subject.structure.should ==
700-
{
701-
:default => [ { :method => "GET", :path => "hello" } ],
702-
"v1" => [ { :method => "GET", :path => "hello" } ],
703-
"v2" => {
704-
:default => [ { :method => "GET", :path => "hello" }, { :method => "POST", :path => "goodbye" } ],
705-
"ns1" => [ { :method => "GET", :path => "hello" } ],
706-
"ns2" => [ { :method => "GET", :path => "hello" } ]
707-
}
708-
}
709-
end
710678
end
679+
711680
end

0 commit comments

Comments
 (0)